Funkcjonalny Powershell z klasami to nie oksymoron, gwarantuję

Hej Habro! Zwracam uwagę na tłumaczenie artykułu „Funkcjonalny PowerShell z klasami.
Zapewniam, że to nie oksymoron”
przez Christophera Kuecha.

Paradygmaty programowania obiektowego i funkcjonalnego mogą wydawać się ze sobą sprzeczne, ale oba są w równym stopniu obsługiwane w programie Powershell. Prawie wszystkie języki programowania, funkcjonalne lub nie, mają możliwości rozszerzonego wiązania nazw i wartości; Klasy, podobnie jak struktury i rekordy, to tylko jedno podejście. Jeśli ograniczymy użycie klas do wiązania nazw i wartości i unikniemy ciężkich koncepcji programowania obiektowego, takich jak dziedziczenie, polimorfizm lub zmienność, możemy skorzystać z ich zalet bez komplikowania naszego kodu. Co więcej, dodając niezmienne metody konwersji typów, możemy wzbogacić nasz kod funkcjonalny o klasy.

Magia kast

Kasty to jedna z najpotężniejszych funkcji programu Powershell. Rzucając wartość, polegasz na niejawnych możliwościach inicjowania i sprawdzania poprawności, które środowisko dodaje do Twojej aplikacji. Na przykład proste rzutowanie ciągu znaków do pliku [xml] spowoduje uruchomienie go przez kod parsera i wygenerowanie kompletnego drzewa XML. W tym samym celu możemy używać klas w naszym kodzie.

Przesyłaj tablice mieszające

Jeśli nie masz konstruktora, możesz kontynuować bez niego, rzutując tablicę mieszającą na typ swojej klasy. Nie zapomnij użyć atrybutów walidacji, aby w pełni wykorzystać ten wzorzec. Jednocześnie możemy użyć typowanych właściwości klasy, aby uruchomić jeszcze głębszą logikę inicjalizacji i sprawdzania poprawności.

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 odlewanie pomaga uzyskać czysty wydruk. Porównaj wynik tablicy mieszającej Cluster przekazanej do tabeli Format z tym, co otrzymasz, jeśli najpierw rzucisz te tablice mieszające na klasę. Właściwości klasy są zawsze wymienione w kolejności, w jakiej są tam zdefiniowane. Nie zapomnij dodać słowa kluczowego ukrytego przed wszystkimi właściwościami, których nie chcesz, aby były widoczne w wynikach.

Funkcjonalny Powershell z klasami to nie oksymoron, gwarantuję

Obsada znaczeń

Jeśli masz konstruktor z jednym argumentem, rzutowanie wartości na typ klasy spowoduje przekazanie wartości do konstruktora, gdzie 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"

Rzuć do linii

Można także przesłonić metodę klasy [string] ToString(), aby zdefiniować logikę stojącą za reprezentacją obiektu w postaci ciągu znaków, na przykład za pomocą 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 "We just created a model for '$cluster'"

Przesyłaj serializowane instancje

Obsada umożliwia bezpieczną deserializację. Poniższe przykłady nie powiodą się, jeśli dane nie spełniają naszej specyfikacji w klastrze

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

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

Castuje w kodzie funkcjonalnym

Programy funkcjonalne najpierw definiują struktury danych, a następnie implementują program jako sekwencję transformacji na niezmiennych strukturach danych. Pomimo sprzecznego wrażenia, klasy naprawdę pomagają w pisaniu kodu funkcjonalnego dzięki metodom konwersji typów.

Czy PowerShell, który piszę, działa?

Wiele osób mających doświadczenie w języku C# lub podobnym, pisze Powershell, który jest podobny do C#. Robiąc to, odchodzisz od koncepcji programowania funkcjonalnego i prawdopodobnie odniesiesz korzyść, jeśli zagłębisz się w programowanie obiektowe w Powershell lub dowiesz się więcej o programowaniu funkcjonalnym.

Jeśli w dużym stopniu polegasz na przekształcaniu niezmiennych danych za pomocą potoków (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object itp. - masz bardziej funkcjonalny styl i odniesiesz korzyści z używania Powershell zajęcia w stylu funkcjonalnym.

Funkcjonalne wykorzystanie klas

Kasty, choć używają alternatywnej składni, są jedynie odwzorowaniem pomiędzy dwiema domenami. W potoku możesz zmapować tablicę wartości za pomocą ForEach-Object.

W poniższym przykładzie konstruktor Node jest wykonywany za każdym razem, gdy rzucane jest Datum, co daje nam możliwość napisania niewielkiej ilości kodu. W rezultacie nasz potok koncentruje się na deklaratywnym wykonywaniu zapytań i agregacji danych, podczas gdy nasze klasy zajmują się analizowaniem i walidacją danych.

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

Nic nie jest tak dobre, jak się wydaje

Niestety klas nie można eksportować przez moduły w taki sam sposób, jak funkcje lub zmienne; ale jest kilka sztuczek. Załóżmy, że Twoje klasy są zdefiniowane w pliku ./my-classes.ps1

  • Możesz pobrać plik z klasami:. ./moje-klasy.ps1. Spowoduje to wykonanie my-classes.ps1 w bieżącym zakresie i zdefiniowanie wszystkich klas z znajdującego się tam pliku.

  • Możesz utworzyć moduł Powershell, który eksportuje wszystkie niestandardowe interfejsy API (cmdlet) i ustawić zmienną ScriptsToProcess = "./my-classes.ps1" w manifeście modułu, z tym samym skutkiem: ./my-classes.ps1 zostanie wykonane w Twoje środowisko.

Niezależnie od tego, którą opcję wybierzesz, pamiętaj, że system typów Powershell nie może rozpoznawać typów o tej samej nazwie ładowanych z różnych miejsc.
Nawet jeśli załadujesz dwie identyczne klasy o tych samych właściwościach z różnych miejsc, ryzykujesz wystąpieniem problemów.

Droga naprzód

Najlepszym sposobem na uniknięcie problemów z rozpoznawaniem typów jest nigdy nie udostępnianie klas użytkownikom. Zamiast oczekiwać, że użytkownik zaimportuje typ zdefiniowany przez klasę, wyeksportuj funkcję ze swojego modułu, która eliminuje potrzebę bezpośredniego dostępu do klasy. W przypadku klastra możemy wyeksportować funkcję New-Cluster, która będzie obsługiwać przyjazne dla użytkownika zestawy parametrów i zwracać klaster.

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 zajęciach
Defensywny PowerShell
Programowanie funkcjonalne w PowerShell

Źródło: www.habr.com

Dodaj komentarz