Bonjour Habr ! Je vous présente la traduction de l'article de l’auteur Christopher Kuech.
Les paradigmes de programmation orientée objet et fonctionnelle peuvent sembler incompatibles, mais tous deux sont également pris en charge dans PowerShell. Pratiquement tous les langages de programmation, qu'ils soient fonctionnels ou non, ont des moyens d'association avancés de noms et de valeurs ; les classes, comme les structures et les enregistrements, ne sont qu'une approche. Si nous limitons l'utilisation des classes à l'association de noms et de valeurs et évitons des concepts de programmation orientée objet "lourds" tels que l'héritage, le polymorphisme ou la mutabilité, nous pouvons tirer parti de leurs avantages sans compliquer notre code. Ensuite, en ajoutant des méthodes de conversion de types immuables, nous pouvons enrichir notre code fonctionnel avec des classes.
La magie des casts
Les casts sont l'une des fonctionnalités les plus puissantes de PowerShell. Lorsque vous appliquez un cast à une valeur, vous vous reposez sur la capacité d'initialisation et de validation implicites ajoutée par l'environnement à votre application. Par exemple, un simple cast de chaîne en [xml] passera par le code du parseur et générera un arbre xml complet. Nous pouvons utiliser des classes dans notre code avec le même objectif.
Cast des tables de hachage
Si vous n'avez pas de constructeur, vous pouvez continuer sans lui, en utilisant le cast des tables de hachage en type de votre classe. N'oubliez pas d'utiliser les attributs de validation pour profiter pleinement de ce modèle. Par la même occasion, nous pouvons utiliser des propriétés typées de classe pour déclencher une logique d'initialisation et de validation encore plus approfondie.
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
}De plus, le cast aide à obtenir une sortie propre. Comparez la sortie d'un tableau de tables de hachage Cluster passé à Format-Table avec ce que vous obtiendrez si vous castiez d'abord ces tables de hachage en classe. Les propriétés de classe sont toujours énumérées dans l'ordre dans lequel elles sont définies. N'oubliez pas d'ajouter le mot clé hidden devant toutes les propriétés qui ne doivent pas être visibles dans la sortie.

Cast des valeurs
Si vous avez un constructeur avec un argument, le cast de la valeur vers votre type de classe passera cette valeur à votre constructeur, où vous pourrez initialiser l'instance de votre classe.
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"Cast en chaîne
Vous pouvez également remplacer la méthode de classe [string] ToString() pour définir la logique de représentation en chaîne de l'objet, par exemple en utilisant l'interpolation de chaînes.
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 "Nous venons de créer un modèle pour '$cluster'"Cast des instances sérialisées
Le cast permet une désérialisation sécurisée. Les exemples ci-dessous échoueront si les données ne correspondent pas à notre spécification dans Cluster.
# Валидация сериализованных данных
[Cluster]$cluster = Get-Content "./my-cluster.json" | ConvertFrom-Json
[Cluster[]]$clusters = Import-Csv "./my-clusters.csv"Casts dans votre code fonctionnel
Les programmes fonctionnels définissent d'abord des structures de données, puis implémentent le programme comme une séquence de transformations sur des structures de données immuables. Malgré une impression contradictoire, les classes aident réellement à écrire du code fonctionnel grâce aux méthodes de conversion de types.
Écris-je du Powershell fonctionnel ?
Beaucoup de personnes venant de C# ou ayant un passé similaire écrivent du Powershell qui ressemble à C#. En agissant ainsi, vous renoncez à utiliser les concepts de programmation fonctionnelle et, probablement, vous bénéficierez d'une immersion profonde dans la programmation orientée objet en Powershell ou d'une meilleure compréhension de la programmation fonctionnelle.
Si vous vous reposez fortement sur la transformation de données immuables en utilisant des pipelines (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, etc. — vous avez un style plus fonctionnel, et l'utilisation des classes Powershell dans un style fonctionnel vous aidera.
Utilisation fonctionnelle des classes
Les classes, bien qu'elles utilisent une syntaxe alternative, ne sont qu'un mappage entre deux domaines. Dans le pipeline, il est possible de mapper un tableau de valeurs en utilisant ForEach-Object.
Dans l'exemple ci-dessous, le constructeur Node est exécuté chaque fois qu'il y a un cast vers Datum, ce qui nous permet d'éviter d'écrire une quantité considérable de code. En conséquence, notre pipeline se concentre sur la requête déclarative des données et l'agrégation, tandis que nos classes s'occupent du parsing des données et de la validation.
# Пример комбинирования классов с конвейерами для 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 -UniqueEmballage de classe pour réutilisation
Rien n'est aussi bon qu'il n'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 existe quelques astuces. Supposons que vos classes soient définies dans le fichier ./my-classes.ps1.
Vous pouvez faire un dot-sourcing du fichier contenant les classes : . ./my-classes.ps1. Cela exécutera my-classes.ps1 dans votre portée actuelle et définira toutes les classes à partir du fichier.
Vous pouvez créer un module PowerShell qui exporte toutes vos API personnalisées (cmdlets) 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.
Quel que soit le choix que vous faites, n'oubliez pas que le système de types PowerShell ne peut pas résoudre des types ayant le même nom chargés de différents endroits.
Même si vous avez chargé de différents endroits deux classes identiques avec des propriétés identiques, vous risquez de rencontrer des problèmes.
La voie à suivre
La meilleure façon d'éviter les problèmes de résolution de types est de ne jamais exposer vos classes aux utilisateurs. Au lieu de vous attendre à ce que l'utilisateur importe un type défini dans la classe, exportez de votre module une fonction qui libère l'utilisateur de la nécessité d'accéder directement à la classe. Concernant Cluster, nous pouvons exporter la fonction New-Cluster, qui supportera un ensemble d'options conviviaux pour l'utilisateur et retournera 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À lire aussi
Source : habr.com
