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.

Paradoksy programowania obiektowego i funkcyjnego mogą wydawać się ze sobą sprzeczne, ale oba są w równym stopniu wspierane w PowerShell. Prawie wszystkie języki programowania, zarówno funkcyjne, jak i nie, mają środki do zaawansowanego powiązania nazw i wartości; Klasy, podobnie jak struktury i rekordy, to tylko jeden z możliwych podejść. Jeśli ograniczymy użycie klas do powiązań nazw i wartości, unikając "ciężkich" paradygmatów programowania obiektowego, takich jak dziedziczenie, polimorfizm czy mutowalność, będziemy mogli korzystać z ich zalet, nie komplikując naszego kodu. Ponadto, dodając niemutowalne metody konwersji typów, możemy wzbogacić nasz funkcjonalny kod o klasy.

Magia rzutów

Rzuty to jedna z najpotężniejszych funkcji w PowerShell. Kiedy dokonujesz rzutu wartości, polegasz na dodawanej przez środowisko możliwości niejawnej inicjalizacji i walidacji twojej aplikacji. Na przykład, prosty rzut ciągu na [xml] przeprowadzi go przez parser i wygeneruje pełne drzewo XML. Możemy w naszym kodzie używać klas w tym samym celu.

Rzut tablic asocjacyjnych

Jeśli nie masz konstruktora, możesz kontynuować bez niego, używając rzutu tablic asocjacyjnych na typ swojego klasy. Nie zapomnij skorzystać z atrybutów walidacji, aby w pełni wykorzystać ten wzorzec. Przy okazji możemy użyć typizowanych 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 rzut pomaga uzyskać czysty wynik. Porównaj wynik tablic asocjacyjnych Cluster przekazany do Format-Table z tym, co otrzymasz, jeśli najpierw rzucisz te tablice asocjacyjne 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

Rzut wartości

Jeśli masz konstruktor z jednym argumentem, rzutowanie wartości do swojego typu klasy przekaże tę wartość do twojego konstruktora, 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 znaków

Możesz również nadpisać metodę klasy [string] ToString(), aby zdefiniować logikę reprezentacji tekstowej obiektu, na przykład używając 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 stworzyliśmy model dla '$cluster'"

Rzutowanie zserializowanych instancji

Rzutowanie pozwala na bezpieczną deserializację. Poniższe przykłady zakończą się błędem, jeśli dane nie będą odpowiadać naszej specyfikacji w Cluster.

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

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

Rzutowania w twoim kodzie funkcyjnym

Programy funkcyjne najpierw definiują struktury danych, a następnie implementują program jako sekwencję transformacji na niemutowalnych strukturach danych. Pomimo pozornie sprzecznego wrażenia, klasy faktycznie pomagają pisać kod funkcyjny dzięki metodom konwersji typów.

Czy piszę funkcjonalnego Powershella?

Wielu ludzi z doświadczeniem w C# lub podobnym tłem pisze Powershell, który przypomina C#. Robiąc tak, rezygnujesz z użycia koncepcji programowania funkcyjnego i prawdopodobnie zyskasz na głębszym zanurzeniu w programowanie obiektowe w Powershell lub lepszego poznania programowania funkcyjnego.

Jeśli głęboko polegasz na transformacji danych niemutowalnych, używając potoków (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object itd. — masz bardziej funkcjonalny styl i pomoże ci używanie klas Powershell w stylu funkcyjnym.

Funkcjonalne wykorzystanie klas

Typy, mimo że używają alternatywnej składni, są tylko mapowaniem między dwoma domenami. W pipeline można zmapować tablicę wartości, używając ForEach-Object.

W poniższym przykładzie konstruktor Node jest wywoływany za każdym razem, gdy dochodzi do rzutowania na Datum, co daje nam możliwość uniknięcia pisania zbyt dużej ilości kodu. W rezultacie nasz pipeline koncentruje się na deklaratywnym zapytaniu o dane 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

Opakowanie klasy do ponownego użycia

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; ale istnieją pewne triki. Załóżmy, że twoje klasy są zdefiniowane w pliku .\/my-classes.ps1

  • Możesz zrobić dot-source'owanie pliku z klasami: . .\/my-classes.ps1. To wykona my-classes.ps1 w twoim bieżącym zakresie widoczności i zdefiniuje wszystkie klasy z pliku.

  • Możesz stworzyć moduł PowerShell, który eksportuje wszystkie twoje niestandardowe API (Polecenia) i ustawić zmienną ScriptsToProcess = ".\/my-classes.ps1" w manifeście swojego modułu, uzyskując ten sam rezultat: .\/my-classes.ps1 zostanie wykonane w twoim środowisku.

Niezależnie od wybranego rozwiązania, pamiętaj, że system typów PowerShell nie może rozwiązać typów o tych samych nazwach, załadowanych z różnych lokalizacji.
Nawet jeśli załadowałeś dwa identyczne klasy z tych samych miejsc z takimi samymi właściwościami, ryzykujesz napotkanie problemów.

Droga naprzód

Najlepszym sposobem na uniknięcie problemów z rozwiązywaniem typów jest nigdy nie udostępniać swoich klas użytkownikom. Zamiast oczekiwać, że użytkownik zaimportuje konkretny typ z klasy, wyeksportuj z twojego modułu funkcję, która uwolni od konieczności bezpośredniego odnoszenia się do klasy. W odniesieniu do klastra możemy wyeksportować funkcję New-Cluster, która będzie obsługiwać przyjazne dla użytkownika zestawy parametrów i zwróci Klaster.

klasa Klaster {
    [ValidatePattern("^[A-z]+$")]
    [string] $Usługa
    [ValidateSet("TEST", "STAGE", "CANARY", "PROD")]
    [string] $PierścieńLotniczy
    [ValidateSet("EastUS", "WestUS", "NorthEurope")]
    [string] $Region
    [ValidateRange(0, 255)]
    [int] $Indeks
}

function Nowy-Klaster {
    [OutputType([Klaster])]
    Param(
        [Parameter(Mandatory, ParameterSetName = "Id", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Id,
        [Parameter(Mandatory, ParameterSetName = "Components")]
        [string] $Usługa,
        [Parameter(Mandatory, ParameterSetName = "Components")]
        [string] $PierścieńLotniczy,
        [Parameter(Mandatory, ParameterSetName = "Components")]
        [string] $Region,
        [Parameter(Mandatory, ParameterSetName = "Components")]
        [int] $Indeks
    )

    if ($Id) {
        $Usługa, $PierścieńLotniczy, $Region, $Indeks = $Id -split "-"
    }

    [Klaster]@{
        Usługa       = $Usługa
        PierścieńLotniczy = $PierścieńLotniczy
        Region        = $Region
        Indeks         = $Indeks
    }
}

Export-ModuleMember Nowy-Klaster

Co jeszcze poczytać

O klasach
Obrona PowerShell
Programowanie funkcyjne w PowerShell

Źródło: habr.com

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