Funktionales Powershell mit Klassen ist kein Widerspruch in sich, das garantiere ich

Hey Habr! Ich präsentiere Ihnen die Übersetzung des Artikels „Funktionale PowerShell mit Klassen.
Ich verspreche, es ist kein Widerspruch in sich.“
von Christopher Kuech.

Objektorientierte und funktionale Programmierparadigmen scheinen im Widerspruch zueinander zu stehen, aber beide werden in Powershell gleichermaßen unterstützt. Fast alle Programmiersprachen, ob funktionsfähig oder nicht, verfügen über Funktionen zur erweiterten Name-Wert-Bindung; Klassen wie Strukturen und Datensätze sind nur ein Ansatz. Wenn wir die Verwendung von Klassen auf die Bindung von Namen und Werten beschränken und aufwendige objektorientierte Programmierkonzepte wie Vererbung, Polymorphismus oder Veränderlichkeit vermeiden, können wir ihre Vorteile nutzen, ohne unseren Code zu komplizieren. Darüber hinaus können wir durch das Hinzufügen unveränderlicher Typkonvertierungsmethoden unseren Funktionscode mit Klassen anreichern.

Die Magie der Kasten

Kasten sind eine der mächtigsten Funktionen in Powershell. Wenn Sie einen Wert umwandeln, verlassen Sie sich auf die impliziten Initialisierungs- und Validierungsfunktionen, die die Umgebung Ihrer Anwendung hinzufügt. Wenn Sie beispielsweise einfach eine Zeichenfolge in [xml] umwandeln, wird diese durch den Parsercode geführt und ein vollständiger XML-Baum generiert. Für den gleichen Zweck können wir Klassen in unserem Code verwenden.

Cast-Hashtabellen

Wenn Sie keinen Konstruktor haben, können Sie auch ohne fortfahren, indem Sie eine Hashtabelle in Ihren Klassentyp umwandeln. Vergessen Sie nicht, die Validierungsattribute zu verwenden, um dieses Muster optimal nutzen zu können. Gleichzeitig können wir die typisierten Eigenschaften der Klasse verwenden, um eine noch tiefergehende Initialisierungs- und Validierungslogik auszuführen.

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
}

Darüber hinaus trägt das Casting dazu bei, eine saubere Ausgabe zu erhalten. Vergleichen Sie die Ausgabe des an Format-Table übergebenen Cluster-Hashtable-Arrays mit dem, was Sie erhalten, wenn Sie diese Hashtables zuerst in eine Klasse umwandeln. Die Eigenschaften einer Klasse werden immer in der Reihenfolge aufgelistet, in der sie dort definiert sind. Vergessen Sie nicht, das Schlüsselwort „hidden“ vor allen Eigenschaften einzufügen, die in den Ergebnissen nicht sichtbar sein sollen.

Funktionales Powershell mit Klassen ist kein Widerspruch in sich, das garantiere ich

Besetzung von Bedeutungen

Wenn Sie einen Konstruktor mit einem Argument haben, wird durch die Umwandlung eines Werts in Ihren Klassentyp der Wert an Ihren Konstruktor übergeben, wo Sie eine Instanz Ihrer Klasse initialisieren können

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"

An die Linie gegossen

Sie können auch die Klassenmethode [string] ToString() überschreiben, um die Logik hinter der String-Darstellung des Objekts zu definieren, beispielsweise durch die Verwendung von String-Interpolation.

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

Serialisierte Instanzen umwandeln

Cast ermöglicht eine sichere Deserialisierung. Die folgenden Beispiele schlagen fehl, wenn die Daten nicht unserer Spezifikation im Cluster entsprechen

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

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

Kasten in Ihrem Funktionscode

Funktionale Programme definieren zunächst Datenstrukturen und implementieren das Programm dann als Folge von Transformationen über unveränderliche Datenstrukturen. Trotz des widersprüchlichen Eindrucks helfen Klassen dank Typkonvertierungsmethoden wirklich beim Schreiben von Funktionscode.

Ist die Powershell, die ich schreibe, funktionsfähig?

Viele Leute mit C#- oder ähnlichem Hintergrund schreiben Powershell, das C# ähnelt. Auf diese Weise entfernen Sie sich von der Verwendung funktionaler Programmierkonzepte und würden wahrscheinlich davon profitieren, wenn Sie sich intensiv mit der objektorientierten Programmierung in Powershell befassen oder mehr über funktionale Programmierung lernen würden.

Wenn Sie sich stark auf die Transformation unveränderlicher Daten mithilfe von Pipelines (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object usw. verlassen, verfügen Sie über einen funktionaleren Stil und werden von der Verwendung von Powershell profitieren Unterricht in einem funktionalen Stil.

Funktionale Verwendung von Klassen

Obwohl Kasten eine alternative Syntax verwenden, sind sie lediglich eine Zuordnung zwischen zwei Domänen. In der Pipeline können Sie mit ForEach-Object ein Array von Werten zuordnen.

Im folgenden Beispiel wird der Node-Konstruktor jedes Mal ausgeführt, wenn ein Datum umgewandelt wird, und dies gibt uns die Möglichkeit, nicht viel Code zu schreiben. Daher konzentriert sich unsere Pipeline auf die deklarative Datenabfrage und -aggregation, während sich unsere Klassen um die Datenanalyse und -validierung kümmern.

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

Verpackungsklasse zur Wiederverwendung

Nichts ist so gut, wie es scheint

Leider können Klassen nicht auf die gleiche Weise wie Funktionen oder Variablen von Modulen exportiert werden. aber es gibt einige Tricks. Nehmen wir an, Ihre Klassen sind in der Datei ./my-classes.ps1 definiert

  • Sie können eine Datei mit den Klassen dotsourcen:. ./my-classes.ps1. Dadurch wird my-classes.ps1 in Ihrem aktuellen Bereich ausgeführt und alle Klassen aus der Datei dort definiert.

  • Sie können ein Powershell-Modul erstellen, das alle Ihre benutzerdefinierten APIs (Cmdlets) exportiert, und die Variable ScriptsToProcess = „./my-classes.ps1“ in Ihrem Modulmanifest festlegen, mit dem gleichen Ergebnis: ./my-classes.ps1 wird ausgeführt in Ihre Umgebung.

Für welche Option Sie sich auch entscheiden, bedenken Sie, dass das Typsystem von Powershell keine Typen mit demselben Namen auflösen kann, die von verschiedenen Orten geladen wurden.
Selbst wenn Sie zwei identische Klassen mit denselben Eigenschaften von verschiedenen Orten geladen haben, besteht die Gefahr, dass Probleme auftreten.

Weiter so

Der beste Weg, Probleme bei der Typauflösung zu vermeiden, besteht darin, Ihre Klassen niemals Benutzern zugänglich zu machen. Anstatt zu erwarten, dass der Benutzer einen klassendefinierten Typ importiert, exportieren Sie eine Funktion aus Ihrem Modul, die den direkten Zugriff auf die Klasse überflüssig macht. Für Cluster können wir eine New-Cluster-Funktion exportieren, die benutzerfreundliche Parametersätze unterstützt und einen Cluster zurückgibt.

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

Was gibt es sonst noch zu lesen?

Über Klassen
Defensive PowerShell
Funktionale Programmierung in PowerShell

Source: habr.com

Kommentar hinzufügen