Powershell funzionale con classi non è un ossimoro, te lo garantisco

Ehi Habr! Presento alla vostra attenzione la traduzione dell'articolo "PowerShell funzionale con classi.
Prometto che non è un ossimoro"
di Christopher Kuech.

I paradigmi di programmazione orientata agli oggetti e funzionale possono sembrare in contrasto tra loro, ma entrambi sono ugualmente supportati in Powershell. Quasi tutti i linguaggi di programmazione, funzionali o meno, dispongono di funzionalità per l'associazione estesa nome-valore; Le classi, come le strutture e i record, rappresentano solo un approccio. Se limitiamo il nostro uso delle classi all'associazione di nomi e valori ed evitiamo concetti pesanti di programmazione orientata agli oggetti come ereditarietà, polimorfismo o mutabilità, possiamo sfruttare i loro vantaggi senza complicare il nostro codice. Inoltre, aggiungendo metodi di conversione di tipo immutabile, possiamo arricchire il nostro codice funzionale con le classi.

La magia delle caste

Le caste sono una delle funzionalità più potenti di Powershell. Quando esegui il cast di un valore, fai affidamento sulle funzionalità implicite di inizializzazione e convalida che l'ambiente aggiunge alla tua applicazione. Ad esempio, il semplice cast di una stringa in [xml] la eseguirà attraverso il codice del parser e genererà un albero xml completo. Possiamo utilizzare le classi nel nostro codice per lo stesso scopo.

Trasmetti tabelle hash

Se non disponi di un costruttore, puoi continuare senza uno eseguendo il cast di una tabella hash per il tuo tipo di classe. Non dimenticare di utilizzare gli attributi di convalida per sfruttare appieno questo modello. Allo stesso tempo, possiamo utilizzare le proprietà tipizzate della classe per eseguire una logica di inizializzazione e convalida ancora più profonda.

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 casting aiuta a ottenere un risultato pulito. Confronta l'output dell'array hashtable del Cluster passato a Format-Table con quello che ottieni se prima trasmetti queste hashtable in una classe. Le proprietà di una classe sono sempre elencate nell'ordine in cui sono definite lì. Non dimenticare di aggiungere la parola chiave nascosta prima di tutte quelle proprietà che non vuoi che siano visibili nei risultati.

Powershell funzionale con classi non è un ossimoro, te lo garantisco

Cast di significati

Se hai un costruttore con un argomento, il cast di un valore al tuo tipo di classe passerà il valore al tuo costruttore, dove potrai inizializzare un'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"

Trasmetti alla linea

È inoltre possibile sovrascrivere il metodo della classe [string] ToString() per definire la logica dietro la rappresentazione della 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 "We just created a model for '$cluster'"

Trasmetti istanze serializzate

Cast consente la deserializzazione sicura. Gli esempi seguenti falliranno se i dati non soddisfano le nostre specifiche in Cluster

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

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

Caste nel tuo codice funzionale

I programmi funzionali definiscono prima le strutture dati, quindi implementano il programma come una sequenza di trasformazioni su strutture dati immutabili. Nonostante l'impressione contraddittoria, le classi aiutano davvero a scrivere codice funzionale grazie ai metodi di conversione dei tipi.

Il Powershell che sto scrivendo è funzionale?

Molte persone provenienti da C# o background simili scrivono Powershell, che è simile a C#. In questo modo, ti stai allontanando dall'utilizzo dei concetti di programmazione funzionale e probabilmente trarrai vantaggio dall'immersione approfondita nella programmazione orientata agli oggetti in Powershell o dall'apprendimento di più sulla programmazione funzionale.

Se fai molto affidamento sulla trasformazione di dati immutabili utilizzando pipeline (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, ecc., hai uno stile più funzionale e trarrai vantaggio dall'utilizzo di Powershell lezioni in uno stile funzionale.

Uso funzionale delle classi

Le caste, sebbene utilizzino una sintassi alternativa, sono solo una mappatura tra due domini. Nella pipeline, puoi mappare un array di valori utilizzando ForEach-Object.

Nell'esempio seguente, il costruttore Node viene eseguito ogni volta che viene lanciato un Datum, e questo ci dà l'opportunità di non scrivere una discreta quantità di codice. Di conseguenza, la nostra pipeline si concentra sull'interrogazione e sull'aggregazione dichiarativa dei dati, mentre le nostre classi si occupano dell'analisi 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

Classe di imballaggio per il riutilizzo

Niente è bello come sembra

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

  • Puoi dotsource un file con le classi:. ./le-mie-classi.ps1. Questo eseguirà my-classes.ps1 nel tuo ambito corrente e definirà tutte le classi dal file lì.

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

Qualunque opzione tu scelga, tieni presente che il sistema di tipi di Powershell non può risolvere tipi con lo stesso nome caricati da posizioni diverse.
Anche se caricassi due classi identiche con le stesse proprietà da posti diversi, rischieresti di incorrere in problemi.

Strada da seguire

Il modo migliore per evitare problemi di risoluzione dei tipi è non esporre mai le proprie classi agli utenti. Invece di aspettarti che l'utente importi un tipo definito dalla classe, esporta una funzione dal tuo modulo che elimina la necessità di accedere direttamente alla classe. Per Cluster, possiamo esportare una funzione New-Cluster che supporterà set di parametri intuitivi e restituirà 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

Cos'altro leggere

Informazioni sulle classi
PowerShell difensivo
Programmazione funzionale in PowerShell

Fonte: habr.com

Aggiungi un commento