Functionele Powershell met klassen is geen oxymoron, dat garandeer ik

Hallo, Habr! Ik presenteer onder uw aandacht een vertaling van het artikel "Functionele PowerShell met klassen.
Ik beloof dat het geen oxymoron is"
van Christopher Kuech.

Objectgeoriënteerde en functionele programmeerparadigma's lijken misschien op gespannen voet met elkaar, maar beide worden in Powershell in gelijke mate ondersteund. Bijna alle programmeertalen, functioneel of niet, hebben faciliteiten voor uitgebreide naam-waardebinding; Klassen, zoals structs en records, zijn slechts één benadering. Als we ons gebruik van klassen beperken tot het binden van namen en waarden, en zware objectgeoriënteerde programmeerconcepten zoals overerving, polymorfisme of veranderlijkheid vermijden, kunnen we profiteren van hun voordelen zonder onze code ingewikkeld te maken. Verder kunnen we, door onveranderlijke typeconversiemethoden toe te voegen, onze functionele code verrijken met klassen.

De magie van kasten

Kasten zijn een van de krachtigste functies in Powershell. Wanneer u een waarde cast, vertrouwt u op de impliciete initialisatie- en validatiemogelijkheden die de omgeving aan uw applicatie toevoegt. Als u bijvoorbeeld eenvoudigweg een string in [xml] cast, wordt deze door de parsercode geleid en wordt een volledige XML-boom gegenereerd. We kunnen klassen in onze code voor hetzelfde doel gebruiken.

Cast hashtabellen

Als u geen constructor heeft, kunt u zonder een constructor doorgaan door een hashtabel naar uw klassetype te casten. Vergeet niet de validatiekenmerken te gebruiken om optimaal van dit patroon te profiteren. Tegelijkertijd kunnen we de getypte eigenschappen van de klasse gebruiken om nog diepere initialisatie- en validatielogica uit te voeren.

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
}

Bovendien helpt casten om een ​​zuivere uitvoer te krijgen. Vergelijk de uitvoer van de Cluster-hashtabelarray die aan Format-Table wordt doorgegeven met wat u krijgt als u deze hashtabellen eerst in een klasse cast. De eigenschappen van een klasse worden altijd weergegeven in de volgorde waarin ze daar zijn gedefinieerd. Vergeet niet het verborgen zoekwoord toe te voegen vóór al die eigenschappen die u niet zichtbaar wilt maken in de resultaten.

Functionele Powershell met klassen is geen oxymoron, dat garandeer ik

Cast van betekenissen

Als u een constructor met één argument heeft, zal het casten van een waarde naar uw klassetype de waarde doorgeven aan uw constructor, waar u een exemplaar van uw klasse kunt initialiseren

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"

Werp naar de lijn

U kunt ook de klassemethode [string] ToString() overschrijven om de logica achter de tekenreeksrepresentatie van het object te definiëren, zoals het gebruik van tekenreeksinterpolatie.

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

Geserialiseerde exemplaren casten

Cast maakt veilige deserialisatie mogelijk. De onderstaande voorbeelden mislukken als de gegevens niet voldoen aan onze specificatie in Cluster

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

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

Kasten in uw functionele code

Functionele programma's definiëren eerst datastructuren en implementeren het programma vervolgens als een reeks transformaties over onveranderlijke datastructuren. Ondanks de tegenstrijdige indruk helpen klassen je echt bij het schrijven van functionele code dankzij typeconversiemethoden.

Is de Powershell die ik schrijf functioneel?

Veel mensen met een C#- of vergelijkbare achtergrond schrijven Powershell, vergelijkbaar met C#. Door dit te doen, stapt u af van het gebruik van functionele programmeerconcepten en zou u waarschijnlijk baat hebben bij een duik in objectgeoriënteerd programmeren in Powershell of als u meer leert over functioneel programmeren.

Als u sterk afhankelijk bent van het transformeren van onveranderlijke gegevens met behulp van pijplijnen (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, enz. - heeft u een meer functionele stijl en zult u profiteren van het gebruik van Powershell lessen in een functionele stijl.

Functioneel gebruik van klassen

Kasten, hoewel ze een alternatieve syntaxis gebruiken, zijn slechts een afbeelding tussen twee domeinen. In de pijplijn kunt u een reeks waarden in kaart brengen met behulp van ForEach-Object.

In het onderstaande voorbeeld wordt de Node-constructor elke keer uitgevoerd wanneer een Datum wordt gegoten, en dit geeft ons de mogelijkheid om niet een behoorlijke hoeveelheid code te schrijven. Als gevolg hiervan richt onze pijplijn zich op het opvragen en aggregeren van declaratieve gegevens, terwijl onze klassen zorgen voor het parseren en valideren van gegevens.

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

Verpakkingsklasse voor hergebruik

Niets is zo goed als het lijkt

Helaas kunnen klassen niet op dezelfde manier door modules worden geëxporteerd als functies of variabelen; maar er zijn enkele trucjes. Stel dat uw klassen zijn gedefinieerd in het bestand ./my-classes.ps1

  • Je kunt een bestand dotsourcen met klassen:. ./mijn-klassen.ps1. Hierdoor wordt my-classes.ps1 in uw huidige bereik uitgevoerd en worden alle klassen uit het bestand daar gedefinieerd.

  • U kunt een Powershell-module maken die al uw aangepaste API's (cmdlets) exporteert en de variabele ScriptsToProcess = "./my-classes.ps1" in uw modulemanifest instellen, met hetzelfde resultaat: ./my-classes.ps1 wordt uitgevoerd in jouw omgeving.

Welke optie u ook kiest, houd er rekening mee dat het typesysteem van Powershell geen typen met dezelfde naam kan omzetten die vanaf verschillende plaatsen zijn geladen.
Zelfs als u twee identieke klassen met dezelfde eigenschappen vanaf verschillende plaatsen laadt, loopt u het risico op problemen te stuiten.

De weg vooruit

De beste manier om problemen met typeresolutie te voorkomen, is door uw klassen nooit aan gebruikers bloot te stellen. In plaats van te verwachten dat de gebruiker een door de klasse gedefinieerd type importeert, exporteert u een functie uit uw module die de noodzaak elimineert om rechtstreeks toegang te krijgen tot de klasse. Voor Cluster kunnen we een New-Cluster-functie exporteren die gebruiksvriendelijke parametersets ondersteunt en een Cluster retourneert.

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

Wat nog te lezen?

Over klassen
Defensieve PowerShell
Functioneel programmeren in PowerShell

Bron: www.habr.com

Voeg een reactie