Cześć, Habr! Przedstawiam wam tłumaczenie artykułu 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.

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 -UniqueOpakowanie 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-KlasterCo jeszcze poczytać
Źródło: habr.com
