Powershell funcional com classes não é um oxímoro, garanto

Olá, Habr! Apresento a sua atenção uma tradução do artigo "PowerShell Funcional com Classes.
Eu prometo que não é um oxímoro"
por Christopher Kuech.

Os paradigmas de programação funcional e orientado a objetos podem parecer conflitantes, mas ambos são igualmente suportados no Powershell. Quase todas as linguagens de programação, funcionais ou não, possuem recursos para ligação estendida de nome-valor; Classes, como estruturas e registros, são apenas uma abordagem. Se limitarmos nosso uso de Classes à ligação de nomes e valores e evitarmos conceitos pesados ​​de programação orientada a objetos, como herança, polimorfismo ou mutabilidade, poderemos tirar vantagem deles sem complicar nosso código. Além disso, ao adicionar métodos de conversão de tipo imutáveis, podemos enriquecer nosso código funcional com Classes.

A magia das castas

Castas são um dos recursos mais poderosos do Powershell. Ao converter um valor, você depende dos recursos implícitos de inicialização e validação que o ambiente adiciona ao seu aplicativo. Por exemplo, simplesmente converter uma string em [xml] irá executá-la através do código do analisador e gerar uma árvore xml completa. Podemos usar Classes em nosso código para a mesma finalidade.

Transmitir hashtables

Se você não tiver um construtor, poderá continuar sem ele convertendo uma tabela hash para o seu tipo de classe. Não se esqueça de usar os atributos de validação para aproveitar ao máximo esse padrão. Ao mesmo tempo, podemos usar as propriedades digitadas da classe para executar uma inicialização e uma lógica de validação ainda mais profundas.

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
}

Além disso, a transmissão ajuda a obter uma saída limpa. Compare a saída da matriz hashtable Cluster passada para Format-Table com o que você obtém se primeiro converter essas tabelas hash em uma classe. As propriedades de uma classe são sempre listadas na ordem em que são definidas. Não se esqueça de adicionar a palavra-chave oculta antes de todas as propriedades que você não deseja que fiquem visíveis nos resultados.

Powershell funcional com classes não é um oxímoro, garanto

Elenco de significados

Se você tiver um construtor com um argumento, converter um valor para o seu tipo de classe passará o valor para o seu construtor, onde você poderá inicializar uma instância da sua 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"

Transmitir para linha

Você também pode substituir o método de classe [string] ToString() para definir a lógica por trás da representação de string do objeto, como usar interpolação de string.

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

Transmitir instâncias serializadas

Cast permite desserialização segura. Os exemplos abaixo falharão se os dados não atenderem à nossa especificação no Cluster

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

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

Castas em seu código funcional

Os programas funcionais primeiro definem estruturas de dados e depois implementam o programa como uma sequência de transformações em estruturas de dados imutáveis. Apesar da impressão contraditória, as classes realmente ajudam você a escrever código funcional graças aos métodos de conversão de tipo.

O Powershell que estou escrevendo está funcional?

Muitas pessoas que vêm de C# ou com formação semelhante estão escrevendo Powershell, que é semelhante ao C#. Ao fazer isso, você está deixando de usar conceitos de programação funcional e provavelmente se beneficiaria se se aprofundasse na programação orientada a objetos no Powershell ou aprendesse mais sobre programação funcional.

Se você depende muito da transformação de dados imutáveis ​​​​usando pipelines (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object, etc. - você tem um estilo mais funcional e se beneficiará com o uso do Powershell aulas em um estilo funcional.

Uso funcional de classes

As castas, embora utilizem uma sintaxe alternativa, são apenas um mapeamento entre dois domínios. No pipeline, você pode mapear uma matriz de valores usando ForEach-Object.

No exemplo abaixo, o construtor Node é executado toda vez que um Datum é lançado, e isso nos dá a oportunidade de não escrever uma quantidade razoável de código. Como resultado, nosso pipeline se concentra na consulta e agregação de dados declarativos, enquanto nossas classes cuidam da análise e validação dos dados.

# Пример комбинирования классов с конвейерами для 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 de embalagem para reutilização

Nada é tão bom quanto parece

Infelizmente, as classes não podem ser exportadas por módulos da mesma forma que funções ou variáveis; mas existem alguns truques. Digamos que suas classes estejam definidas no arquivo ./my-classes.ps1

  • Você pode dotsource um arquivo com classes:. ./minhas-classes.ps1. Isso executará my-classes.ps1 em seu escopo atual e definirá todas as classes do arquivo lá.

  • Você pode criar um módulo Powershell que exporta todas as suas APIs personalizadas (cmdlets) e definir a variável ScriptsToProcess = "./my-classes.ps1" no manifesto do seu módulo, com o mesmo resultado: ./my-classes.ps1 será executado em seu ambiente.

Qualquer que seja a opção escolhida, lembre-se de que o sistema de tipos do Powershell não pode resolver tipos com o mesmo nome carregados de locais diferentes.
Mesmo se você carregar duas classes idênticas com as mesmas propriedades de locais diferentes, você corre o risco de ter problemas.

Caminho a seguir

A melhor maneira de evitar problemas de resolução de tipos é nunca expor suas classes aos usuários. Em vez de esperar que o usuário importe um tipo definido pela classe, exporte uma função do seu módulo que elimine a necessidade de acessar a classe diretamente. Para Cluster, podemos exportar uma função New-Cluster que oferecerá suporte a conjuntos de parâmetros fáceis de usar e retornará um 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

O que mais ler

Sobre aulas
PowerShell Defensivo
Programação Funcional no PowerShell

Fonte: habr.com

Adicionar um comentário