Funksjonell Powershell med klasser er ikke en oksymoron, jeg garanterer det

Hei, Habr! Jeg presenterer for din oppmerksomhet en oversettelse av artikkelen "Funksjonell PowerShell med klasser.
Jeg lover at det ikke er en oksymoron"
av Christopher Kuech.

Objektorienterte og funksjonelle programmeringsparadigmer kan virke i strid med hverandre, men begge er like støttet i Powershell. Nesten alle programmeringsspråk, funksjonelle eller ikke, har fasiliteter for utvidet navn-verdi-binding; Klasser, som strukturer og poster, er bare én tilnærming. Hvis vi begrenser vår bruk av klasser til binding av navn og verdier, og unngår tunge objektorienterte programmeringskonsepter som arv, polymorfisme eller mutabilitet, kan vi dra nytte av fordelene deres uten å komplisere koden vår. Videre, ved å legge til uforanderlige konverteringsmetoder, kan vi berike vår funksjonelle kode med klasser.

Magien til kaster

Kaster er en av de kraftigste funksjonene i Powershell. Når du kaster en verdi, er du avhengig av de implisitte initialiserings- og valideringsmulighetene miljøet legger til applikasjonen din. For eksempel, bare casting av en streng i [xml] vil kjøre den gjennom parserkoden og generere et komplett xml-tre. Vi kan bruke klasser i koden vår til samme formål.

Cast hashtabeller

Hvis du ikke har en konstruktør, kan du fortsette uten en ved å caste en hashtabell til klassetypen din. Ikke glem å bruke valideringsattributtene for å dra full nytte av dette mønsteret. Samtidig kan vi bruke klassens typeegenskaper til å kjøre enda dypere initialiserings- og valideringslogikk.

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
}

I tillegg bidrar støping til å få et rent resultat. Sammenlign utdataene fra Cluster-hashtable-matrisen som sendes til Format-Table med det du får hvis du først kaster disse hashtabellene inn i en klasse. Egenskapene til en klasse er alltid oppført i den rekkefølgen de er definert der. Ikke glem å legge til det skjulte søkeordet før alle de egenskapene du ikke vil skal være synlige i resultatene.

Funksjonell Powershell med klasser er ikke en oksymoron, jeg garanterer det

Kast av betydninger

Hvis du har en konstruktør med ett argument, vil casting av en verdi til klassetypen sende verdien til konstruktøren din, hvor du kan initialisere en forekomst av klassen din

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 strek

Du kan også overstyre klassemetoden [string] ToString() for å definere logikken bak objektets strengrepresentasjon, for eksempel bruk av strenginterpolasjon.

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 serialiserte forekomster

Cast gir sikker deserialisering. Eksemplene nedenfor vil mislykkes hvis dataene ikke oppfyller spesifikasjonene våre i Cluster

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

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

Kaster inn funksjonskoden din

Funksjonelle programmer definerer først datastrukturer, og implementerer deretter programmet som en sekvens av transformasjoner over uforanderlige datastrukturer. Til tross for det motstridende inntrykket, hjelper klasser deg virkelig med å skrive funksjonell kode takket være typekonverteringsmetoder.

Er Powershell jeg skriver funksjonelt?

Mange mennesker som kommer fra C# eller lignende bakgrunn skriver Powershell, som ligner på C#. Ved å gjøre dette går du bort fra å bruke funksjonelle programmeringskonsepter og vil sannsynligvis ha nytte av å dykke tungt inn i objektorientert programmering i Powershell eller lære mer om funksjonell programmering.

Hvis du er avhengig av å transformere uforanderlige data ved hjelp av pipelines (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, etc. - du har en mer funksjonell stil og du vil dra nytte av å bruke Powershell klasser i funksjonell stil.

Funksjonell bruk av klasser

Kaster, selv om de bruker en alternativ syntaks, er bare en kartlegging mellom to domener. I pipelinen kan du kartlegge en rekke verdier ved å bruke ForEach-Object.

I eksemplet nedenfor kjøres Node-konstruktøren hver gang en Datum castes, og dette gir oss muligheten til å ikke skrive en god del kode. Som et resultat fokuserer vår pipeline på deklarativ dataspørring og aggregering, mens klassene våre tar seg av 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

Emballasjeklasse for gjenbruk

Ingenting er så bra som det ser ut til

Klasser kan dessverre ikke eksporteres av moduler på samme måte som funksjoner eller variabler; men det er noen triks. La oss si at klassene dine er definert i filen ./my-classes.ps1

  • Du kan dotsource en fil med klasser:. ./mine-klasser.ps1. Dette vil kjøre my-classes.ps1 i ditt nåværende omfang og definere alle klassene fra filen der.

  • Du kan lage en Powershell-modul som eksporterer alle dine egendefinerte API-er (cmdlets) og angi ScriptsToProcess = "./my-classes.ps1"-variabelen i modulmanifestet, med samme resultat: ./my-classes.ps1 vil kjøre i miljøet ditt.

Uansett hvilket alternativ du velger, husk at Powershells typesystem ikke kan løse typer med samme navn lastet fra forskjellige steder.
Selv om du lastet to identiske klasser med de samme egenskapene fra forskjellige steder, risikerer du å få problemer.

Veien forover

Den beste måten å unngå problemer med typeoppløsning er å aldri eksponere klassene dine for brukere. I stedet for å forvente at brukeren importerer en klassedefinert type, eksporterer du en funksjon fra modulen din som eliminerer behovet for å få tilgang til klassen direkte. For Cluster kan vi eksportere en New-Cluster-funksjon som vil støtte brukervennlige parametersett og returnere 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

Hva annet å lese

Om klasser
Defensiv PowerShell
Funksjonell programmering i PowerShell

Kilde: www.habr.com

Legg til en kommentar