Funktionel Powershell med klasser er ikke en oxymoron, det garanterer jeg

Hej Habr! Jeg præsenterer for din opmærksomhed oversættelsen af ​​artiklen "Funktionel PowerShell med klasser.
Jeg lover, at det ikke er en oxymoron"
af Christopher Kuech.

Objektorienterede og funktionelle programmeringsparadigmer kan virke i modstrid med hinanden, men begge understøttes lige meget i Powershell. Næsten alle programmeringssprog, funktionelle eller ej, har faciliteter til udvidet navn-værdibinding; Klasser, ligesom strukturer og optegnelser, er kun én tilgang. Hvis vi begrænser vores brug af klasser til binding af navne og værdier og undgår tunge objektorienterede programmeringskoncepter såsom arv, polymorfi eller mutabilitet, kan vi drage fordel af deres fordele uden at komplicere vores kode. Yderligere, ved at tilføje uforanderlige typekonverteringsmetoder, kan vi berige vores funktionelle kode med klasser.

Kasternes magi

Kaster er en af ​​de mest kraftfulde funktioner i Powershell. Når du kaster en værdi, er du afhængig af de implicitte initialiserings- og valideringsmuligheder, som miljøet tilføjer til din applikation. For eksempel vil blot caste en streng i [xml] køre den gennem parserkoden og generere et komplet xml-træ. Vi kan bruge klasser i vores kode til samme formål.

Cast hashtabeller

Hvis du ikke har en konstruktør, kan du fortsætte uden en ved at caste en hashtabel til din klassetype. Glem ikke at bruge valideringsattributterne for at få det fulde udbytte af dette mønster. Samtidig kan vi bruge klassens indtastede egenskaber til at køre endnu dybere initialiserings- og valideringslogik.

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
}

Derudover er støbning med til at få et rent output. Sammenlign outputtet fra Cluster-hashtable-arrayet, der er sendt til Format-Table, med det, du får, hvis du først caster disse hashtabeller til en klasse. Egenskaberne for en klasse er altid opført i den rækkefølge, som de er defineret der. Glem ikke at tilføje det skjulte søgeord før alle de egenskaber, som du ikke ønsker skal være synlige i resultaterne.

Funktionel Powershell med klasser er ikke en oxymoron, det garanterer jeg

Kast af betydninger

Hvis du har en konstruktør med ét argument, vil casting af en værdi til din klassetype overføre værdien til din konstruktør, hvor du kan initialisere en forekomst af din klasse

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"

Kast til stregen

Du kan også tilsidesætte klassemetoden [string] ToString() for at definere logikken bag objektets strengrepræsentation, såsom at bruge strenginterpolation.

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'"

Cast serialiserede forekomster

Cast tillader sikker deserialisering. Eksemplerne nedenfor vil mislykkes, hvis dataene ikke opfylder vores specifikation i Cluster

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

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

Indsætter din funktionelle kode

Funktionelle programmer definerer først datastrukturer og implementerer derefter programmet som en sekvens af transformationer over uforanderlige datastrukturer. På trods af det modstridende indtryk hjælper klasser dig virkelig med at skrive funktionel kode takket være typekonverteringsmetoder.

Er den Powershell, jeg skriver, funktionel?

Mange mennesker, der kommer fra C# eller lignende baggrunde, skriver Powershell, som ligner C#. Ved at gøre dette bevæger du dig væk fra at bruge funktionelle programmeringskoncepter og vil sandsynligvis have gavn af at dykke meget ned i objektorienteret programmering i Powershell eller lære mere om funktionel programmering.

Hvis du er meget afhængig af at transformere uforanderlige data ved hjælp af pipelines (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object osv. - har du en mere funktionel stil, og du vil drage fordel af at bruge Powershell klasser i en funktionel stil.

Funktionel brug af klasser

Kaster, selvom de bruger en alternativ syntaks, er blot en kortlægning mellem to domæner. I pipelinen kan du kortlægge en række værdier ved hjælp af ForEach-Object.

I eksemplet nedenfor udføres Node-konstruktøren hver gang en Datum castes, og det giver os mulighed for ikke at skrive en rimelig mængde kode. Som et resultat heraf fokuserer vores pipeline på deklarativ dataforespørgsel og aggregering, mens vores klasser tager sig af dataparsing og 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

Emballageklasse til genbrug

Intet er så godt, som det ser ud til

Desværre kan klasser ikke eksporteres af moduler på samme måde som funktioner eller variabler; men der er nogle tricks. Lad os sige, at dine klasser er defineret i filen ./my-classes.ps1

  • Du kan dotsource en fil med klasser:. ./mine-klasser.ps1. Dette vil udføre my-classes.ps1 i dit nuværende omfang og definere alle klasserne fra filen der.

  • Du kan oprette et Powershell-modul, der eksporterer alle dine brugerdefinerede API'er (cmdlets) og indstille ScriptsToProcess = "./my-classes.ps1"-variablen i dit modulmanifest, med samme resultat: ./my-classes.ps1 vil køre i dit miljø.

Uanset hvilken mulighed du vælger, skal du huske på, at Powershells typesystem ikke kan løse typer af samme navn indlæst fra forskellige steder.
Selvom du har indlæst to identiske klasser med de samme egenskaber fra forskellige steder, risikerer du at løbe ind i problemer.

Vejen frem

Den bedste måde at undgå typeopløsningsproblemer på er aldrig at udsætte dine klasser for brugere. I stedet for at forvente, at brugeren importerer en klassedefineret type, eksporter du en funktion fra dit modul, der eliminerer behovet for at få direkte adgang til klassen. For Cluster kan vi eksportere en New-Cluster-funktion, der understøtter brugervenlige parametersæt og returnerer en 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

Hvad skal man ellers læse

Om klasser
Defensiv PowerShell
Funktionel programmering i PowerShell

Kilde: www.habr.com

Tilføj en kommentar