Funktionell Powershell med klasser är ingen oxymoron, det garanterar jag

Hej, Habr! Jag presenterar för din uppmärksamhet en översättning av artikeln "Funktionell PowerShell med klasser.
Jag lovar att det inte är en oxymoron"
av Christopher Kuech.

Objektorienterade och funktionella programmeringsparadigm kan verka i strid med varandra, men båda stöds lika i Powershell. Nästan alla programmeringsspråk, funktionella eller inte, har möjligheter för utökad namn-värde-bindning; Klasser, som strukturer och rekord, är bara ett tillvägagångssätt. Om vi ​​begränsar vår användning av klasser till bindning av namn och värden, och undviker tunga objektorienterade programmeringskoncept som arv, polymorfism eller föränderlighet, kan vi dra fördel av deras fördelar utan att komplicera vår kod. Vidare, genom att lägga till oföränderliga typkonverteringsmetoder, kan vi berika vår funktionella kod med klasser.

Kasternas magi

Kaster är en av de mest kraftfulla funktionerna i Powershell. När du kastar ett värde förlitar du dig på de implicita initierings- och valideringsmöjligheter som miljön lägger till din applikation. Om du till exempel bara castar en sträng i [xml] körs den genom parserkoden och genererar ett komplett xml-träd. Vi kan använda klasser i vår kod för samma ändamål.

Cast hashtabeller

Om du inte har en konstruktor kan du fortsätta utan en genom att casta en hashtabell till din klasstyp. Glöm inte att använda valideringsattributen för att dra full nytta av detta mönster. Samtidigt kan vi använda klassens typegenskaper för att köra ännu djupare initierings- och 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
}

Dessutom hjälper gjutning till att få en ren utgång. Jämför utdata från Cluster-hashtable-arrayen som skickas till Format-Table med vad du får om du först castar dessa hashtabeller till en klass. Egenskaperna för en klass listas alltid i den ordning som de definieras där. Glöm inte att lägga till det dolda sökordet före alla de egenskaper som du inte vill ska synas i resultaten.

Funktionell Powershell med klasser är ingen oxymoron, det garanterar jag

Kast av betydelser

Om du har en konstruktor med ett argument, kommer ett värde till din klasstyp att skicka värdet till din konstruktor, där du kan initiera en instans 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"

Kasta till linje

Du kan också åsidosätta klassmetoden [string] ToString() för att definiera logiken bakom objektets strängrepresentation, till exempel att använda stränginterpolation.

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 serialiserade instanser

Cast tillåter säker deserialisering. Exemplen nedan kommer att misslyckas 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"

Kastar in din funktionskod

Funktionella program definierar först datastrukturer och implementerar sedan programmet som en sekvens av transformationer över oföränderliga datastrukturer. Trots det motsägelsefulla intrycket hjälper klasser dig verkligen att skriva funktionell kod tack vare typkonverteringsmetoder.

Är Powershell jag skriver funktionellt?

Många människor som kommer från C# eller liknande bakgrunder skriver Powershell, som liknar C#. Genom att göra detta går du bort från att använda funktionella programmeringskoncept och skulle sannolikt ha nytta av att dyka ner mycket i objektorienterad programmering i Powershell eller lära dig mer om funktionell programmering.

Om du är mycket beroende av att transformera oföränderlig data med hjälp av pipelines (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, etc. - du har en mer funktionell stil och du kommer att dra nytta av att använda Powershell klasser i funktionell stil.

Funktionell användning av klasser

Kaster, även om de använder en alternativ syntax, är bara en mappning mellan två domäner. I pipelinen kan du kartlägga en rad värden med ForEach-Object.

I exemplet nedan exekveras nodkonstruktorn varje gång ett datum castas, och detta ger oss möjlighet att inte skriva en hel del kod. Som ett resultat av detta fokuserar vår pipeline på deklarativ dataförfrågning och aggregering, medan våra klasser tar hand om dataanalys 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

Förpackningsklass 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 några knep. Låt oss säga att dina klasser är definierade i filen ./my-classes.ps1

  • Du kan dotsourca en fil med klasser:. ./mina-klasser.ps1. Detta kommer att köra my-classes.ps1 i ditt nuvarande scope och definiera alla klasser från filen där.

  • Du kan skapa en Powershell-modul som exporterar alla dina anpassade API:er (cmdlets) och ställa in variabeln ScriptsToProcess = "./my-classes.ps1" i ditt modulmanifest, med samma resultat: ./my-classes.ps1 kommer att köras i din miljö.

Vilket alternativ du än väljer, kom ihåg att Powershells typsystem inte kan lösa typer med samma namn som laddas från olika platser.
Även om du laddade två identiska klasser med samma egenskaper från olika ställen riskerar du att stöta på problem.

Vägen framåt

Det bästa sättet att undvika typupplösningsproblem är att aldrig exponera dina klasser för användare. Istället för att förvänta sig att användaren ska importera en klassdefinierad typ, exportera en funktion från din modul som eliminerar behovet av att komma åt klassen direkt. För Cluster kan vi exportera en New-Cluster-funktion som stöder användarvänliga parameteruppsättningar och returnerar ett 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

Vad mer att läsa

Om klasser
Defensiv PowerShell
Funktionell programmering i PowerShell

Källa: will.com

Lägg en kommentar