PowerShell funcional con clases no es una contradicción, te lo aseguro.

¡Hola, Habr! Les presento la traducción del artículo "Poder funcional de PowerShell con clases."
Te prometo que no es un oxímoron"
del autor Christopher Kuech.

Las paradigmas de programación orientada a objetos y funcional pueden parecer estar en desacuerdo entre sí, pero ambas son igualmente compatibles en PowerShell. Prácticamente todos los lenguajes de programación, tanto funcionales como no funcionales, cuentan con mecanismos de enlace avanzado de nombres y valores; las clases, al igual que las estructuras y los registros, son solo un enfoque. Si limitamos el uso de las clases al enlace de nombres y valores y evitamos conceptos de programación orientada a objetos "pesados" como la herencia, el polimorfismo o la mutabilidad, podremos aprovechar sus beneficios sin complicar nuestro código. Además, al agregar métodos de transformación de tipos inmutables, podemos enriquecer nuestro código funcional con clases.

La magia del casting

Los casts son una de las características más poderosas de PowerShell. Al aplicar un valor a un cast, dependemos de la capacidad que el entorno agrega a tu aplicación para una inicialización y validación implícitas. Por ejemplo, un simple cast de cadena a [xml] pasará por el código del analizador y generará un árbol XML completo. Podemos usar clases en nuestro código con el mismo propósito.

Casting de hash tables

Si no tienes un constructor, puedes seguir adelante sin él, utilizando el casting de hash table al tipo de tu clase. No olvides aprovechar los atributos de validación para sacar el máximo provecho de este patrón. Además, podemos usar propiedades tipadas de la clase para activar 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 una salida clara. Compara la salida de un array de hash tables Cluster pasado a Format-Table con lo que obtendrías si primero castas estas hash tables a una clase. Las propiedades de la clase siempre se enumeran en el orden en que están definidas. No olvides agregar la palabra clave hidden antes de todas aquellas propiedades que no deben ser visibles en la salida.

PowerShell funcional con clases no es una contradicción, te lo aseguro.

Casting de valores

Si tiene un constructor con un argumento, el casting del valor a su tipo de clase pasará el valor a ese constructor, donde podrá inicializar la 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"

Casting a string

También puede sobrescribir el método de la clase [string] ToString() para definir la lógica de representación en forma de cadena del objeto, por ejemplo, utilizando la 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 "Acabamos de crear un modelo para '$cluster'"

Casting de instancias serializadas

El casting permite una deserialización segura. Los ejemplos a continuación resultarán en un error 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"

Castings en su 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 ayudan a escribir código funcional gracias a los métodos de conversión de tipos.

¿Estoy escribiendo Powershell de manera funcional?

Muchas personas que vienen de C# o tienen un pasado similar escriben Powershell que se asemeja a C#. Al hacerlo, están renunciando al uso de conceptos de programación funcional y probablemente se beneficiarán al profundizar en la programación orientada a objetos en Powershell o al aprender mejor la programación funcional.

Si depende en gran medida de la transformación de datos inmutables utilizando tuberías (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, etc., tiene un estilo más funcional y el uso de clases de Powershell en un estilo funcional le ayudará.

Uso funcional de clases

Las clases, aunque utilizan una sintaxis alternativa, son solo un mapeo entre dos dominios. En el pipeline, se puede mapear un arreglo de valores utilizando ForEach-Object.

En el siguiente ejemplo, el constructor Node se ejecuta cada vez que se realiza un cast a Datum, lo que nos permite no escribir una gran cantidad de código. Como resultado, nuestro pipeline se centra en la consulta declarativa de datos y la agregación, mientras que nuestras clases se encargan del análisis de datos y la validación.

# Пример комбинирования классов с конвейерами для 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

Empaquetado de clases para reutilización

Nada es tan bueno como parece

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

  • Puede hacer un dot-sourcing del archivo de clases: . ./my-classes.ps1. Esto ejecutará my-classes.ps1 en su ámbito actual y definirá todas las clases del archivo ahí.

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

Cualquiera que sea la opción que elija, no olvide que el sistema de tipos de Powershell no puede resolver tipos con el mismo nombre que se cargan desde diferentes ubicaciones.
Incluso si ha cargado desde diferentes ubicaciones dos clases idénticas con las mismas propiedades, corre el riesgo de enfrentar problemas.

El camino a seguir

La mejor manera de evitar problemas con la resolución de tipos es nunca exponer a los usuarios sus clases. En lugar de esperar que el usuario importe un tipo definido en una clase, exporte desde su módulo una función que elimine la necesidad de acceder directamente a la clase. En el caso de Cluster, podemos exportar la función New-Cluster, que supportará conjuntos de parámetros amigables para el usuario 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

Qué leer a continuación

Acerca de Clases
Poder Defensivo de PowerShell
Programación Funcional en PowerShell

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster