Funktionell PowerShell med klasser – jag garanterar att det inte är ett oxymoron

Hej, Habr! Jag presenterar översättningen av artikeln "Funktionell PowerShell med Klasser."
Jag lovar att det inte är en oxymoron"
författaren Christopher Kuech.

Objektorienterade och funktionella programmeringsparadigm kan verka oförenliga, men båda stöds i lika hög grad i PowerShell. Nästan alla programmeringsspråk, både funktionella och icke-funktionella, har medel för avancerad bindning av namn och värden; Klasser, likt structar och records, är bara en metod. Om vi begränsar användningen av Klasser till bindning av namn och värden och undviker "tunga" objektorienterade programmeringskoncept som arv, polymorfism eller mutabilitet, kan vi utnyttja deras fördelar utan att komplicera vår kod. Genom att dessutom lägga till oföränderliga metoder för typkonvertering kan vi berika vår funktionella kod med Klasser.

Kraften av casts

Casts är en av de mest kraftfulla funktionerna i PowerShell. När du tillämpar en cast på ett värde, förlitar du dig på de implicit initierings- och valideringsmöjligheter som läggs till av miljön till din applikation. Till exempel, en enkel cast av en sträng till [xml] kommer att köra den genom parserkoden och generera ett komplett xml-träd. Vi kan använda Klasser i vår kod med samma syfte.

Cast av hash-tabeller

Om du inte har en konstruktor kan du fortsätta utan en, genom att använda cast av hash-tabeller till din klass typ. Glöm inte att använda valideringsattribut för att fullt ut dra nytta av detta mönster. Samtidigt kan vi använda typade klass-egenskaper för att utföra ännu mer djupgående logik för initiering och validering.

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
}

Dessutom hjälper casting till att få ett rent utdata. Jämför utdata av arrayen av hash-tabeller Cluster som skickas till Format-Table med vad som skulle hända om dessa hash-tabeller först casts till klass. Klassens egenskaper listas alltid i den ordning de definieras. Glöm inte att lägga till nyckelordet hidden före alla egenskaper som inte ska synas i utdata.

Funktionell PowerShell med klasser – jag garanterar att det inte är ett oxymoron

Casting av värden

Om du har en konstruktor med ett argument, kommer typomvandlingen av värdet till din klass att överföra värdet till din konstruktor, där du kan initiera instansen av din klass.

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"

Typomvandling till sträng

Du kan också skriva över metoden [string] ToString() för att definiera logiken för strängrepresentationen av objektet, till exempel genom att använda stränginterpolering.

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 "Vi har just skapat en modell för '$cluster'"

Typomvandling av serialiserade instanser

Typomvandling möjliggör säker deserialisering. Exemplen nedan kommer att avslutas med ett fel om data inte uppfyller vår specifikation i Cluster.

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

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

Typomvandlingar i din funktionella kod

Funktionella program definierar först datatyper, och implementerar sedan programmet som en sekvens av transformationer över oföränderliga datatyper. Trots det motstridiga intrycket hjälper klasser faktiskt till att skriva funktionell kod, tack vare metoder för typkonvertering.

Skriver jag funktionell Powershell?

Många som kommer från C# eller liknande bakgrund skriver Powershell som liknar C#. Genom att göra det avstår du från att använda koncept från funktionell programmering och får troligtvis mest nytta av att fördjupa dig i objektorienterad programmering i Powershell, eller bättre förstå funktionell programmering.

Om du starkt litar på transformation av oföränderliga data, genom att använda pipelines (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object osv. — har du en mer funktionell stil, och klasser i Powershell kan hjälpa till i funktionell stil.

Funktionell användning av klasser

Klassar, även om de använder alternativ syntax, är bara en mappning mellan två domäner. I röret kan du mappa en array av värden med hjälp av ForEach-Object.

I exemplet nedan körs konstruktorn Node varje gång en kastning till Datum sker, och detta ger oss möjlighet att inte skriva en hel del kod. Som ett resultat fokuserar vårt rör på att deklarativt fråga data och aggregatera, medan våra klasser tar hand om dataparsering och validering.

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

Paketera en klass för återanvändning

Ingenting är så bra som det verkar

Tyvärr kan klasser inte exporteras av moduler på samma sätt som funktioner eller variabler; men det finns vissa knep. Anta att dina klasser är definierade i filen ./my-classes.ps1

  • Du kan göra en dot-sourcing av filen med klasser: . ./my-classes.ps1. Detta kommer att köra my-classes.ps1 i ditt aktuella sammanhang och definiera alla klasser från filen där.

  • Du kan skapa en Powershell-modul som exporterar alla dina användardefinierade API:er (kommandon) och sätta variabeln ScriptsToProcess = "./my-classes.ps1" i manifestet för din modul, med samma resultat: ./my-classes.ps1 kommer att köras i din miljö.

Vilken variant du än väljer, glöm inte att PowerShells typ system inte kan hantera identiska typer som laddas från olika platser.
Även om du har laddat två identiska klasser med samma egenskaper från olika platser, riskerar du att stöta på problem.

Vägen framåt

Det bästa sättet att undvika problem med typupplösning är att aldrig exponera dina klasser för användare. Istället för att förvänta dig att användaren importerar en viss typ definierad i klassen, exportera en funktion från din modul som frigör behovet av att direkt referera till klassen. I relation till Cluster kan vi exportera funktionen New-Cluster, som stöder användarvänliga uppsättningar av parametrar och returnerar Cluster.

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

funktion 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

Vad mer att läsa

Om klasser
Defensiv PowerShell
Funktionell programmering i PowerShell

Källa: habr.com

Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar 🔥 Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster