PowerShell funzionale con classi — non è un ossimoro, te lo garantisco

Ciao, Habr! Ti presento la traduzione di un articolo "PowerShell funzionale con classi.
Prometto che non è un ossimoro"
di Christopher Kuech.

Le paradigmi di programmazione orientata agli oggetti e funzionale possono sembrare in contrasto, ma entrambe sono equamente supportate in PowerShell. Quasi tutti i linguaggi di programmazione, sia funzionali che non, hanno strumenti per un avanzato binding di nomi e valori; le classi, simili a struct e record, sono solo un approccio. Se limitiamo l'uso delle classi al binding di nomi e valori e evitiamo concetti di programmazione orientata agli oggetti 'pesanti', come l'ereditarietà, il polimorfismo o la mutabilità, possiamo sfruttarne i vantaggi senza complicare il nostro codice. Inoltre, aggiungendo metodi di conversione di tipo immutabili, possiamo arricchire il nostro codice funzionale con classi.

La magia dei cast

I cast sono una delle funzionalità più potenti in PowerShell. Quando assegni un valore a un cast, ti affidi alla capacità di inizializzazione e validazione implicita fornita dall'ambiente nella tua applicazione. Ad esempio, un semplice cast di una stringa in [xml] passerà attraverso il codice del parser e genererà un albero XML completo. Possiamo utilizzare classi nel nostro codice con lo stesso scopo.

Cast delle Hashtable

Se non hai un costruttore, puoi continuare a utilizzare il cast delle Hashtable come tipo della tua classe. Non dimenticare di usare gli attributi di validazione per sfruttare appieno questo modello. Inoltre, possiamo utilizzare le proprietà tipizzate della classe per attivare logiche di inizializzazione e validazione ancora più profonde.

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
}

Inoltre, il cast aiuta a ottenere un output pulito. Confronta l'output dell'array di hash table Cluster passato a Format-Table con ciò che otterresti se prima castassi queste hash table in una classe. Le proprietà della classe sono sempre elencate nell'ordine in cui sono definite. Non dimenticare di aggiungere la parola chiave hidden prima di tutte le proprietà che non devono essere visibili nell'output.

PowerShell funzionale con classi — non è un ossimoro, te lo garantisco

Cast dei valori

Se hai un costruttore con un solo argomento, il cast del valore al tuo tipo di classe passerà il valore a quel tuo costruttore, in cui puoi inizializzare l'istanza della tua classe.

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"

Cast in stringa

Puoi anche sovrascrivere il metodo della classe [string] ToString() per definire la logica della rappresentazione stringa dell'oggetto, ad esempio utilizzando l'interpolazione delle stringhe.

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 "Abbiamo appena creato un modello per '$cluster'"

Tipo di istanze serializzate

Il tipo consente una deserializzazione sicura. Gli esempi seguenti genereranno un errore se i dati non soddisfano la nostra specifica in Cluster

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

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

Tipi nel tuo codice funzionale

I programmi funzionali definiscono prima le strutture dati, poi implementano il programma come una sequenza di trasformazioni su strutture dati immutabili. Contrariamente a quanto si possa pensare, le classi aiutano davvero a scrivere codice funzionale grazie ai metodi di conversione dei tipi.

Sto scrivendo in Powershell in modo funzionale?

Molti provenienti da C# o con esperienze simili scrivono in PowerShell, che è simile a C#. In questo modo, rinunciate all'uso dei concetti di programmazione funzionale e probabilmente otterrete dei vantaggi immergendovi maggiormente nella programmazione orientata agli oggetti in PowerShell o studiando meglio la programmazione funzionale.

Se vi affidate profondamente alla trasformazione di dati immutabili usando pipeline (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, ecc. — avete uno stile più funzionale, e l'uso delle classi in PowerShell in stile funzionale vi sarà utile.

Utilizzo funzionale delle classi

Le mappature, sebbene utilizzino una sintassi alternativa, sono semplicemente una mappatura tra due domini. Nella pipeline, è possibile mappare un array di valori utilizzando ForEach-Object.

Nell'esempio seguente, il costruttore Node viene eseguito ogni volta che avviene un cast a Datum, e questo ci consente di non dover scrivere una quantità considerevole di codice. Di conseguenza, la nostra pipeline si concentra sulla richiesta dichiarativa di dati e aggregazione, mentre le nostre classi si occupano del parsing e della convalida dei dati.

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

Imballaggio di classe per il riutilizzo

Niente è così buono come sembra

Purtroppo, le classi non possono essere esportate dai moduli nello stesso modo delle funzioni o delle variabili; ma ci sono alcuni accorgimenti. Supponiamo che le tue classi siano definite nel file ./my-classes.ps1

  • Puoi fare il dot-sourcing del file con le classi: ./my-classes.ps1. Questo eseguirà my-classes.ps1 nel tuo attuale ambito e definirà tutte le classi dal file.

  • Puoi creare un modulo PowerShell che esporta tutte le tue API personalizzate (cmdlet) e impostare la variabile ScriptsToProcess = "./my-classes.ps1" nel manifesto del tuo modulo, con lo stesso risultato: ./my-classes.ps1 verrà eseguito nel tuo ambiente.

Qualunque opzione tu scelga, non dimenticare che il sistema di tipi di PowerShell non può gestire tipi omonimi caricati da differenti fonti.
Anche se hai caricato due classi identiche con le stesse proprietà da fonti diverse, rischi di incorrere in problemi.

La strada da percorrere

Il modo migliore per evitare problemi con la risoluzione dei tipi è non esporre le tue classi agli utenti. Invece di aspettarti che l'utente importi un tipo specifico dalla classe, esporta dalla tua modulazione una funzione che libera dalla necessità di riferirsi direttamente alla classe. Riguardo a Cluster, possiamo esportare la funzione New-Cluster, che supporterà insiemi di parametri più amichevoli per l'utente e restituirà 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

Cosa leggere di più

Informazioni sulle Classi
PowerShell Difensivo
Programmazione Funzionale in PowerShell

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster