Powershell fonctionnel avec classes n'est pas un oxymore, je le garantis

Salut Habr ! Je présente à votre attention la traduction de l'article "PowerShell fonctionnel avec classes.
Je te promets que ce n'est pas un oxymore"
par Christophe Kuech.

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

La magie des castes

Les castes sont l'une des fonctionnalités les plus puissantes de Powershell. Lorsque vous convertissez une valeur, vous comptez sur les capacités implicites d'initialisation et de validation que l'environnement ajoute à votre application. Par exemple, le simple fait de convertir 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.

Caster des tables de hachage

Si vous n'avez pas de constructeur, vous pouvez continuer sans en convertissant une table de hachage en votre type de classe. N'oubliez pas d'utiliser les attributs de validation pour profiter pleinement de ce modèle. Dans le même temps, nous pouvons utiliser les propriétés typées de la classe pour exécuter 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 casting permet d’obtenir un résultat propre. Comparez la sortie du tableau de tables de hachage Cluster transmis à Format-Table avec ce que vous obtenez si vous transtypez d'abord ces tables de hachage dans une classe. Les propriétés d'une classe sont toujours répertoriées dans l'ordre dans lequel elles y sont définies. N'oubliez pas d'ajouter le mot-clé masqué avant toutes les propriétés que vous ne souhaitez pas voir dans les résultats.

Powershell fonctionnel avec classes n'est pas un oxymore, je le garantis

Casting de significations

Si vous avez un constructeur avec un argument, la conversion d'une valeur en votre type de classe transmettra la valeur à votre constructeur, où vous pourrez initialiser une 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"

Caster sur la ligne

Vous pouvez également remplacer la méthode de classe [string] ToString() pour définir la logique derrière la représentation sous forme de chaîne de l'objet, par exemple en utilisant l'interpolation de chaîne.

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'"

Caster des instances sérialisé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 Cluster

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

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

Castes dans votre code fonctionnel

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

Le Powershell que j'écris est-il fonctionnel ?

De nombreuses personnes issues de C# ou d'horizons similaires écrivent Powershell, qui est similaire à C#. En faisant cela, vous vous éloignez de l'utilisation des concepts de programmation fonctionnelle et bénéficieriez probablement d'une plongée approfondie dans la programmation orientée objet dans Powershell ou d'en apprendre davantage sur 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 disposez d'un style plus fonctionnel et vous bénéficierez de l'utilisation de Powershell. cours dans un style fonctionnel.

Utilisation fonctionnelle des classes

Les castes, bien qu'elles utilisent une syntaxe alternative, ne sont qu'une cartographie 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 donnée est convertie, ce qui nous donne la possibilité de ne pas écrire une grande quantité de code. En conséquence, notre pipeline se concentre sur l'interrogation et l'agrégation déclaratives des données, tandis que nos classes s'occupent 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 "([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

Classe d'emballage pour la réutilisation

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

Malheureusement, les classes ne peuvent pas être exportées par des 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 dotsourcer un fichier avec des classes :. ./mes-classes.ps1. Cela exécutera my-classes.ps1 dans votre portée actuelle et définira toutes les classes du fichier.

  • Vous pouvez créer un module Powershell qui exporte toutes vos API personnalisées (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 de Powershell ne peut pas résoudre les types du même nom chargés à partir d'emplacements différents.
Même si vous chargez deux classes identiques avec les mêmes propriétés depuis des endroits différents, vous risquez de rencontrer des problèmes.

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 de l'utilisateur qu'il importe un type défini par la classe, exportez une fonction de votre module qui élimine le besoin d'accéder directement à la classe. Pour Cluster, nous pouvons exporter une fonction New-Cluster qui prendra en charge des ensembles de paramètres conviviaux et renverra un 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

Quoi d'autre à lire

À propos des cours
PowerShell défensif
Programmation fonctionnelle dans PowerShell

Source: habr.com

Ajouter un commentaire