O Powershell funcional con clases non é un oxímoron, así o garante

Ola Habr! Presento á súa atención a tradución do artigo "PowerShell funcional con clases.
Prometo que non é un oxímoron"
por Christopher Kuech.

Os paradigmas de programación orientada a obxectos e funcionais poden parecer en desacordo entre si, pero ambos son compatibles por igual en Powershell. Case todas as linguaxes de programación, funcionais ou non, teñen facilidades para a vinculación estendida de nome-valor; As clases, como as estruturas e os rexistros, son só un enfoque. Se limitamos o noso uso de Clases á vinculación de nomes e valores e evitamos conceptos pesados ​​de programación orientada a obxectos como a herdanza, o polimorfismo ou a mutabilidade, podemos aproveitar as súas vantaxes sen complicar o noso código. Ademais, engadindo métodos de conversión de tipos inmutables, podemos enriquecer o noso código funcional con Clases.

A maxia das castas

As castas son unha das características máis poderosas de Powershell. Cando emites un valor, confía nas capacidades implícitas de inicialización e validación que o ambiente engade á súa aplicación. Por exemplo, simplemente lanzar unha cadea en [xml] executarase a través do código do analizador e xerará unha árbore xml completa. Podemos usar as clases no noso código co mesmo propósito.

Cast hashtables

Se non tes un construtor, podes continuar sen un lanzando unha táboa hash ao teu tipo de clase. Non esquezas usar os atributos de validación para aproveitar ao máximo este patrón. Ao mesmo tempo, podemos usar as propiedades escritas da clase para executar unha lóxica de inicialización e validación aínda máis 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
}

Ademais, a fundición axuda a obter unha saída limpa. Compare a saída da matriz de táboas hash de Cluster pasada a Format-Table co que obtén se primeiro lanza estas táboas hash nunha clase. As propiedades dunha clase aparecen sempre listadas na orde na que se definen alí. Non esquezas engadir a palabra clave oculta antes de todas aquelas propiedades que non queres que sexan visibles nos resultados.

O Powershell funcional con clases non é un oxímoron, así o garante

Reparto de significados

Se tes un construtor cun argumento, emitir un valor ao teu tipo de clase pasará o valor ao teu construtor, onde podes inicializar unha instancia da túa 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"

Emitir a liña

Tamén pode anular o método de clase [cadea] ToString() para definir a lóxica detrás da representación de cadeas do obxecto, como usar a interpolación de cadeas.

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 seriadas

Cast permite a deserialización segura. Os exemplos seguintes fallarán se os datos non cumpren as nosas especificacións en Cluster

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

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

Castas no teu código funcional

Os programas funcionais primeiro definen estruturas de datos, despois implementan o programa como unha secuencia de transformacións sobre estruturas de datos inmutables. A pesar da impresión contraditoria, as clases realmente axudan a escribir código funcional grazas aos métodos de conversión de tipos.

É funcional o Powershell que estou escribindo?

Moita xente procedente de C# ou de fondos similares está a escribir Powershell, que é semellante a C#. Ao facelo, estás afastando do uso de conceptos de programación funcional e probablemente se beneficiarías de mergullar moito na programación orientada a obxectos en Powershell ou de aprender máis sobre a programación funcional.

Se confía moito na transformación de datos inmutables mediante canalizacións (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, etc. - tes un estilo máis funcional e beneficiarase de usar Powershell clases nun estilo funcional.

Uso funcional das clases

As castas, aínda que usan unha sintaxe alternativa, son só un mapeo entre dous dominios. No proceso, pode mapear unha matriz de valores usando ForEach-Object.

No seguinte exemplo, o construtor Node execútase cada vez que se lanza un Datum, e isto dános a oportunidade de non escribir unha boa cantidade de código. Como resultado, o noso pipeline céntrase na consulta e agregación de datos declarativos, mentres que as nosas clases encárganse da análise e 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 embalaxe para reutilización

Nada é tan bo como parece

Desafortunadamente, as clases non se poden exportar por módulos do mesmo xeito que as funcións ou as variables; pero hai algúns trucos. Digamos que as túas clases están definidas no ficheiro ./my-classes.ps1

  • Podes dotsource un ficheiro con clases:. ./as-as-clases.ps1. Isto executará my-classes.ps1 no seu ámbito actual e definirá todas as clases do ficheiro alí.

  • Podes crear un módulo Powershell que exporte todas as túas API personalizadas (cmdlets) e establecer a variable ScriptsToProcess = "./my-classes.ps1" no manifesto do teu módulo, co mesmo resultado: ./my-classes.ps1 executarase en o teu ambiente.

Sexa cal sexa a opción que elixas, ten en conta que o sistema de tipos de Powershell non pode resolver tipos do mesmo nome cargados desde diferentes lugares.
Aínda que cargases dúas clases idénticas coas mesmas propiedades desde lugares diferentes, corres o risco de ter problemas.

O camiño a seguir

A mellor forma de evitar problemas de resolución de tipos é non expor nunca as túas clases aos usuarios. En lugar de esperar que o usuario importe un tipo definido por clase, exporte unha función do módulo que elimine a necesidade de acceder á clase directamente. Para Clúster, podemos exportar unha función New-Cluster que admita conxuntos de parámetros amigables e 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 máis ler

Sobre as clases
PowerShell defensivo
Programación funcional en PowerShell

Fonte: www.habr.com

Engadir un comentario