Powershell funcional con clases no es un oxímoron, te lo garantizo

¡Hola, Habr! Presento a su atención la traducción del artículo. "PowerShell funcional con clases.
Prometo que no es un oxímoron"
Por Christopher Kuech.

Los paradigmas de programación funcional y orientado a objetos pueden parecer contradictorios, pero ambos son igualmente compatibles con Powershell. Casi todos los lenguajes de programación, funcionales o no, tienen funciones para la vinculación extendida de nombre-valor; Las clases, como las estructuras y los registros, son sólo un enfoque. Si limitamos nuestro uso de Clases a la vinculación de nombres y valores, y evitamos conceptos pesados ​​de programación orientada a objetos como herencia, polimorfismo o mutabilidad, podemos aprovechar sus beneficios sin complicar nuestro código. Además, al agregar métodos de conversión de tipos inmutables, podemos enriquecer nuestro código funcional con Clases.

La magia de las castas

Las castas son una de las características más poderosas de Powershell. Cuando emite un valor, confía en las capacidades implícitas de inicialización y validación que el entorno agrega a su aplicación. Por ejemplo, simplemente convertir una cadena en [xml] la ejecutará a través del código del analizador y generará un árbol xml completo. Podemos usar Clases en nuestro código para el mismo propósito.

tablas hash emitidas

Si no tiene un constructor, puede continuar sin uno lanzando una tabla hash a su tipo de clase. No olvide utilizar los atributos de validación para aprovechar al máximo este patrón. Al mismo tiempo, podemos usar las propiedades escritas de la clase para ejecutar una lógica de inicialización y validación aún más profunda.

class Cluster {
    [ValidatePattern("^[A-z]+$")]
    [string] $Service
    [ValidateSet("TEST", "STAGE", "CANARY", "PROD")]
    [string] $FlightingRing
    [ValidateSet("EastUS", "WestUS", "NorthEurope")]
    [string] $Region
    [ValidateRange(0, 255)]
    [int] $Index
}

[Cluster]@{
    Service       = "MyService"
    FlightingRing = "PROD"
    Region        = "EastUS"
    Index         = 2
}

Además, el casting ayuda a obtener un resultado limpio. Compare el resultado de la matriz de tabla hash del clúster pasada a Format-Table con lo que obtiene si primero convierte estas tablas hash en una clase. Las propiedades de una clase siempre se enumeran en el orden en que se definen allí. No olvides agregar la palabra clave oculta antes de todas aquellas propiedades que no quieras que sean visibles en los resultados.

Powershell funcional con clases no es un oxímoron, te lo garantizo

Elenco de significados

Si tiene un constructor con un argumento, enviar un valor a su tipo de clase pasará el valor a su constructor, donde puede inicializar una instancia de su clase.

class Cluster {
    [ValidatePattern("^[A-z]+$")]
    [string] $Service
    [ValidateSet("TEST", "STAGE", "CANARY", "PROD")]
    [string] $FlightingRing
    [ValidateSet("EastUS", "WestUS", "NorthEurope")]
    [string] $Region
    [ValidateRange(0, 255)]
    [int] $Index

    Cluster([string] $id) {
        $this.Service, $this.FlightingRing, $this.Region, $this.Index = $id -split "-"
    }
}

[Cluster]"MyService-PROD-EastUS-2"

Lanzar a la línea

También puede anular el método de clase [cadena] ToString() para definir la lógica detrás de la representación de cadenas del objeto, como el uso de interpolación de cadenas.

class Cluster {
    [ValidatePattern("^[A-z]+$")]
    [string] $Service
    [ValidateSet("TEST", "STAGE", "CANARY", "PROD")]
    [string] $FlightingRing
    [ValidateSet("EastUS", "WestUS", "NorthEurope")]
    [string] $Region
    [ValidateRange(0, 255)]
    [int] $Index

    [string] ToString() {
        return $this.Service, $this.FlightingRing, $this.Region, $this.Index -join "-"
    }
}

$cluster = [Cluster]@{
    Service       = "MyService"
    FlightingRing = "PROD"
    Region        = "EastUS"
    Index         = 2
}

Write-Host "We just created a model for '$cluster'"

Transmitir instancias serializadas

Cast permite una deserialización segura. Los siguientes ejemplos fallarán si los datos no cumplen con nuestra especificación en Cluster

# Валидация сериализованных данных

[Cluster]$cluster = Get-Content "./my-cluster.json" | ConvertFrom-Json
[Cluster[]]$clusters = Import-Csv "./my-clusters.csv"

Castas en tu código funcional

Los programas funcionales primero definen estructuras de datos y luego implementan el programa como una secuencia de transformaciones sobre estructuras de datos inmutables. A pesar de la impresión contradictoria, las clases realmente te ayudan a escribir código funcional gracias a los métodos de conversión de tipos.

¿Es funcional el Powershell que estoy escribiendo?

Mucha gente que viene de C# o de entornos similares está escribiendo Powershell, que es similar a C#. Al hacer esto, dejará de utilizar conceptos de programación funcional y probablemente se beneficiará si profundiza en la programación orientada a objetos en Powershell o si aprende más sobre programación funcional.

Si depende en gran medida de la transformación de datos inmutables mediante canalizaciones (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, etc., tiene un estilo más funcional y se beneficiará del uso de Powershell. Clases en un estilo funcional.

Uso funcional de clases.

Las castas, aunque utilizan una sintaxis alternativa, son sólo una correlación entre dos dominios. En el proceso, puede asignar una matriz de valores utilizando ForEach-Object.

En el siguiente ejemplo, el constructor de Nodo se ejecuta cada vez que se lanza un Datum, y esto nos da la oportunidad de no escribir una buena cantidad de código. Como resultado, nuestra canalización se centra en la consulta y agregación de datos declarativos, mientras que nuestras clases se encargan del análisis y la validación de datos.

# Пример комбинирования классов с конвейерами для separation of concerns в конвейерах

class Node {
    [ValidateLength(3, 7)]
    [string] $Name
    [ValidateSet("INT", "PPE", "PROD")]
    [string] $FlightingRing
    [ValidateSet("EastUS", "WestUS", "NorthEurope", "WestEurope")]
    [string] $Region
    Node([string] $Name) {
        $Name -match "([a-z]+)(INT|PPE|PROD)([a-z]+)"
        $_, $this.Service, $this.FlightingRing, $this.Region = $Matches
        $this.Name = $Name
    }
}

class Datum {
    [string] $Name
    [int] $Value
    [Node] $Computer
    [int] Severity() {
        $this.Name -match "[0-9]+$"
        return $Matches[0]
    }
}

Write-Host "Urgent Security Audit Issues:"
Import-Csv "./audit-results.csv" `
    | ForEach-Object {[Datum]$_} `
    | Where-Object Value -gt 0 `
    | Group-Object {$_.Severity()} `
    | Where-Object Name -lt 2 `
    | ForEach-Object Group `
    | ForEach-Object Computer `
    | Where-Object FlightingRing -eq "PROD" `
    | Sort-Object Name, Region -Unique

Clase de embalaje para reutilización.

Nada es tan bueno como parece

Desafortunadamente, las clases no se pueden exportar por módulos de la misma manera que las funciones o variables; pero hay algunos trucos. Digamos que sus clases están definidas en el archivo ./my-classes.ps1

  • Puede obtener un archivo con clases :. ./mis-clases.ps1. Esto ejecutará my-classes.ps1 en su alcance actual y definirá todas las clases del archivo allí.

  • Puede crear un módulo de Powershell que exporte todas sus API personalizadas (cmdlets) y configurar la variable ScriptsToProcess = "./my-classes.ps1" en el manifiesto de su módulo, con el mismo resultado: ./my-classes.ps1 se ejecutará en tu entorno.

Cualquiera que sea la opción que elija, tenga en cuenta que el sistema de tipos de Powershell no puede resolver tipos con el mismo nombre cargados desde diferentes lugares.
Incluso si cargó dos clases idénticas con las mismas propiedades desde diferentes lugares, corre el riesgo de tener problemas.

Camino a seguir

La mejor manera de evitar problemas de resolución de tipos es nunca exponer sus clases a los usuarios. En lugar de esperar que el usuario importe un tipo definido por clase, exporte una función desde su módulo que elimine la necesidad de acceder a la clase directamente. Para Cluster, podemos exportar una función New-Cluster que admitirá conjuntos de parámetros fáciles de usar y devolverá un Cluster.

class Cluster {
    [ValidatePattern("^[A-z]+$")]
    [string] $Service
    [ValidateSet("TEST", "STAGE", "CANARY", "PROD")]
    [string] $FlightingRing
    [ValidateSet("EastUS", "WestUS", "NorthEurope")]
    [string] $Region
    [ValidateRange(0, 255)]
    [int] $Index
}

function New-Cluster {
    [OutputType([Cluster])]
    Param(
        [Parameter(Mandatory, ParameterSetName = "Id", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Id,
        [Parameter(Mandatory, ParameterSetName = "Components")]
        [string] $Service,
        [Parameter(Mandatory, ParameterSetName = "Components")]
        [string] $FlightingRing,
        [Parameter(Mandatory, ParameterSetName = "Components")]
        [string] $Region,
        [Parameter(Mandatory, ParameterSetName = "Components")]
        [int] $Index
    )

    if ($Id) {
        $Service, $FlightingRing, $Region, $Index = $Id -split "-"
    }

    [Cluster]@{
        Service       = $Service
        FlightingRing = $FlightingRing
        Region        = $Region
        Index         = $Index
    }
}

Export-ModuleMember New-Cluster

que mas leer

Acerca de las clases
PowerShell defensivo
Programación funcional en PowerShell

Fuente: habr.com

Añadir un comentario