Funkcjonalny PowerShell z klasami – to nie oksymoron, gwarantuję to

Cześć, Habr! Przedstawiam Wam tłumaczenie artykułu "Funkcjonalny PowerShell z klasami.
Obiecuję, że to nie jest oksymoron"
autorstwa Christophera Kuecha.

Obiektowo-orientowane i funkcjonalne paradygmaty programowania mogą wydawać się niezgodne, ale oba są równorzędnie wspierane w PowerShell. Praktycznie wszystkie języki programowania, zarówno funkcjonalne, jak i nie, dysponują środkami zaawansowanego powiązania nazw i wartości; Klasy, podobne do struktur i rekordów, to tylko jeden z podejść. Jeśli ograniczymy użycie klas do powiązania nazw i wartości oraz unikniemy „ciężkich” obiektowo-orientowanych koncepcji, takich jak dziedziczenie, polimorfizm czy zmienność (mutability), możemy skorzystać z ich zalet, nie komplikując naszego kodu. Następnie, dodając niezmienne (immutable) metody konwersji typów, możemy wzbogacić nasz funkcjonalny kod o klasy.

Magia rzutów

Rzutowanie to jedna z najpotężniejszych funkcji w PowerShell. Gdy dokonujesz rzutowania wartości, polegasz na funkcjach automatycznej inicjalizacji i walidacji wprowadzonych przez środowisko do twojej aplikacji. Na przykład prosty rzutowanie ciągu w [xml] przeprowadzi go przez kod parsera i wygeneruje pełne drzewo xml. Możemy w naszym kodzie wykorzystać klasy w tym samym celu.

Rzutowanie tablic haszujących

Jeżeli nie masz konstruktora, możesz kontynuować bez niego, używając rzutowania tablicy haszującej na typ twojej klasy. Nie zapomnij skorzystać z atrybutów walidacji, aby w pełni wykorzystać ten wzorzec. Przy okazji możemy użyć typowanych właściwości klasy, aby uruchomić jeszcze głębszą logikę inicjalizacji i walidacji.

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
}

Ponadto, rzutowanie pomaga uzyskać czysty wynik. Porównaj wynik tablicy haszującej klastra przekazany do Format-Table z tym, co otrzymasz, jeśli najpierw rzutujesz te tablice haszujące na klasę. Właściwości klasy są zawsze wymieniane w kolejności, w jakiej zostały zdefiniowane. Nie zapomnij dodać słowa kluczowego hidden przed wszystkimi właściwościami, które nie powinny być widoczne w wynikach.

Funkcjonalny PowerShell z klasami – to nie oksymoron, gwarantuję to

Rzutowanie wartości

Jeśli masz konstruktor z jednym argumentem, rzutowanie wartości na Twój typ klasy przekaże wartość do tego konstruktor, w którym możesz zainicjować instancję swojej klasy.

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"

Rzutowanie na ciąg

Możesz również przesłonić metodę klasy [string] ToString(), aby określić logikę reprezentacji tekstowej obiektu, na przykład korzystając z interpolacji ciągów.

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 "Właśnie utworzyliśmy model dla '$cluster'"

Typ zserializowanych instancji

Typ pozwala na bezpieczną deserializację. Poniższe przykłady zakończą się błędem, jeśli dane nie będą zgodne z naszą specyfikacją w Cluster

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

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

Typy w Twoim kodzie funkcyjnym

Programy funkcyjne najpierw definiują struktury danych, a następnie implementują program jako sekwencję transformacji na niemodyfikowalnych strukturach danych. Mimo kontrowersyjnych wrażeń, klasy faktycznie pomagają w pisaniu kodu funkcyjnego dzięki metodom konwersji typów.

Czy piszę w funkcyjnym Powershellu?

Wielu ludzi z doświadczeniem w C# lub podobnych językach pisze w PowerShell, który przypomina C#. Decydując się na to, rezygnujesz z koncepcji programowania funkcyjnego i prawdopodobnie skorzystasz na深入进去 obiektowym programowaniu w PowerShell lub lepszym zrozumieniu programowania funkcyjnego.

Jeśli mocno polegasz na transformacji danych niemutowalnych przy użyciu potoków (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object itp. — twój styl jest bardziej funkcyjny, a klasy PowerShell w stylu funkcyjnym będą przydatne.

Funkcyjne wykorzystanie klas

Przekazy, mimo że wykorzystują alternatywną składnię, to jedynie mapowanie między dwoma domenami. W potoku można zmapować tablicę wartości za pomocą ForEach-Object.

W poniższym przykładzie konstruktor Node jest wywoływany za każdym razem, gdy następuje rzutowanie na Datum, co pozwala nam uniknąć pisania dużej ilości kodu. W rezultacie nasz potok koncentruje się na deklaratywnym zapytaniu danych i agregacji, podczas gdy nasze klasy zajmują się parsowaniem danych i walidacją.

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

Klasa opakowania do ponownego wykorzystania

Nic nie jest tak dobre, jak się wydaje

Niestety, klasy nie mogą być eksportowane przez moduły w taki sam sposób jak funkcje czy zmienne; jednak są pewne sztuczki. Załóżmy, że Twoje klasy są zdefiniowane w pliku ./my-classes.ps1

  • Możesz doładować plik z klasami: ./my-classes.ps1. To wykona my-classes.ps1 w Twoim bieżącym zakresie i zdefiniuje wszystkie klasy z pliku.

  • Możesz stworzyć moduł Powershell, który eksportuje wszystkie Twoje własne API (cmdlety) i ustawić zmienną ScriptsToProcess = "./my-classes.ps1" w manifeście swojego modułu, osiągając ten sam rezultat: ./my-classes.ps1 wykona się w Twoim środowisku.

Bez względu na wybraną opcję, pamiętaj, że system typów Powershell nie potrafi obsługiwać typów o tej samej nazwie ładowanych z różnych miejsc.
Nawet jeśli załadujesz z różnych miejsc dwie identyczne klasy z tymi samymi właściwościami, możesz napotkać problemy.

Droga naprzód

Najlepszym sposobem na uniknięcie problemów z typami to nigdy nie wystawiać swoich klas użytkownikom. Zamiast oczekiwać, że użytkownik zaimportuje określony typ z klasy, wyeksportuj z modułu funkcję, która zwolni z konieczności bezpośredniego odwoływania się do klasy. W odniesieniu do Cluster możemy wyeksportować funkcję New-Cluster, która obsłuży przyjazne dla użytkownika zestawy parametrów i zwróci 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

Co jeszcze przeczytać

O klasach
Defensywne PowerShell
Programowanie funkcyjne w PowerShell

Źródło: habr.com

Kup solidny hosting dla stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting dla stron z ochroną przed DDoS, serwery VPS VDS | ProHoster