Powershell fonctionnel avec des classes - pas un oxymore, je le garantis

Bonjour, Habr! Je vous présente la traduction de l'article "PowerShell fonctionnel avec classes.
Je promets que ce n'est pas un oxymore " de Christopher Kuech.


Les paradigmes de programmation orientée objet et fonctionnelle peuvent sembler en contradiction, mais les deux sont également pris en charge dans Powershell. Presque tous les langages de programmation, fonctionnels et non, ont des moyens de liaison étendue des noms et des valeurs; Les classes, comme les structures et les enregistrements, ne sont qu'une approche. Si nous limitons l'utilisation des classes à la liaison de noms et de valeurs et évitons des concepts de programmation orientés objet «lourds» tels que l'héritage, le polymorphisme ou la mutabilité, nous pouvons en tirer parti sans compliquer notre code. De plus, en ajoutant des méthodes immuables de conversion de type, nous pouvons enrichir notre code fonctionnel avec des classes.


Magie de caste


Castes est l'une des fonctionnalités les plus puissantes de Powershell. Lorsque vous convertissez une valeur, vous comptez sur la capacité de l'environnement à ajouter une initialisation et une validation implicites à votre application. Par exemple, une simple conversion d'une chaîne en [xml] l'exécutera via le code de l'analyseur et générera une arborescence xml complète. Nous pouvons utiliser des classes dans notre code dans le même but.


Hashtables personnalisés


Si vous n'avez pas de constructeur, vous pouvez continuer sans lui en utilisant une table de hachage en fonction du type de votre classe. N'oubliez pas d'utiliser des attributs de validation pour tirer pleinement parti de ce modèle. Dans le même temps, nous pouvons utiliser les propriétés typées de la classe pour déclencher une logique d'initialisation et de validation encore plus approfondie.


class Cluster { [ValidatePattern("^[Az]+$")] [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 } 

De plus, le casting aide à obtenir une conclusion claire. Comparez la sortie du tableau de table de hachage de cluster passée à Format-Table avec ce qui se passe si vous convertissez d'abord ces tables de hachage dans une classe. Les propriétés de classe sont toujours répertoriées dans l'ordre dans lequel elles y sont définies. N'oubliez pas d'ajouter le mot-clé caché devant toutes ces propriétés qui ne devraient pas être visibles dans la sortie.


image

Valeurs personnalisées


Si vous avez un constructeur avec un argument, le transtypage des valeurs dans votre type de classe transmettra la valeur à ce constructeur, dans lequel vous pouvez initialiser l'instance de votre classe


 class Cluster { [ValidatePattern("^[Az]+$")] [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" 

Ligne personnalisée


Vous pouvez également remplacer la méthode de classe [chaîne] ToString () pour déterminer la logique de la représentation sous forme de chaîne de l'objet, par exemple, à l'aide d'une interpolation de chaîne.


 class Cluster { [ValidatePattern("^[Az]+$")] [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'" 

Instances sérialisées personnalisées


Cast permet une désérialisation en toute sécurité. Les exemples ci-dessous échoueront si les données ne répondent pas à nos spécifications dans le cluster


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


Castes dans votre code de fonction


Les programmes fonctionnels définissent d'abord les structures de données, puis mettent en œuvre le programme comme une séquence de transformations sur des structures de données immuables. Malgré l'impression conflictuelle, les classes aident vraiment à écrire du code fonctionnel grâce aux méthodes de conversion de type.


Powershell est-il fonctionnel?


Beaucoup de gens qui viennent de C # ou avec un passé similaire écrivent Powershell, qui est similaire à C #. En faisant cela, vous êtes réticent à utiliser les concepts de programmation fonctionnelle et vous aurez probablement l'avantage d'être fortement immergé dans la programmation orientée objet dans Powershell ou une meilleure compréhension de la programmation fonctionnelle.


Si vous comptez beaucoup sur la transformation de données immuables à l'aide de pipelines (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, etc., vous avez un style plus fonctionnel et utilisez Classes Powershell dans un style fonctionnel.


Utilisation fonctionnelle des classes


Les castes, bien qu'utilisant une syntaxe alternative, ne font que mapper entre deux domaines. Dans le pipeline, vous pouvez mapper un tableau de valeurs à l'aide de ForEach-Object.


Dans l'exemple ci-dessous, le constructeur Node est exécuté chaque fois qu'une conversion en Datum se produit, ce qui nous permet de ne pas écrire une bonne quantité de code. En conséquence, notre pipeline se concentre sur l'interrogation et l'agrégation de données déclaratives, tandis que nos classes se chargent de l'analyse et de la validation des données.


 #       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 "([az]+)(INT|PPE|PROD)([az]+)" $_, $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 


Réutiliser l'emballage de classe


Rien n'est aussi bon qu'il y paraît


Malheureusement, les classes ne peuvent pas être exportées par les modules de la même manière que les fonctions ou les variables; mais il y a quelques astuces. Disons que vos classes sont définies dans le fichier ./my-classes.ps1


  • Vous pouvez doser un fichier avec des classes:. ./my-classes.ps1. Cela exécutera my-classes.ps1 dans votre portée actuelle et y définira toutes les classes du fichier.


  • Vous pouvez créer un module Powershell qui exporte toutes vos API utilisateur (applets de commande) et définir la variable ScriptsToProcess = "./my-classes.ps1" dans le manifeste de votre module, avec le même résultat: ./my-classes.ps1 s'exécutera dans votre environnement .



Quelle que soit l'option que vous choisissez, n'oubliez pas que le système de types Powershell ne peut pas résoudre les types du même nom chargés à partir de différents endroits.
Même si vous téléchargez deux classes identiques avec les mêmes propriétés à partir d'endroits différents, vous courez le risque de rencontrer des problèmes.


La voie à suivre


La meilleure façon d'éviter les problèmes de résolution de type est de ne jamais exposer vos classes aux utilisateurs. Au lieu d'attendre que l'utilisateur importe un type défini dans la classe, exportez de votre module une fonction qui élimine le besoin d'accéder directement à la classe. Pour Cluster, nous pouvons exporter la fonction New-Cluster, qui prendra en charge des ensembles de paramètres conviviaux et renverra Cluster.


 class Cluster { [ValidatePattern("^[Az]+$")] [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 

Quoi d'autre à lire


À propos des cours
Defensive PowerShell
Programmation fonctionnelle dans PowerShell

Source: https://habr.com/ru/post/fr484300/


All Articles