Функцыянальны Powershell з класамі - не аксюмарон, я гарантую гэта

Прывітанне, Хабр! Уяўляю вашай увазе пераклад артыкула "Functional PowerShell with Classes.
I promise it's не an oxymoron"
аўтара Christopher Kuech.

Аб'ектна-арыентаваная і функцыянальная парадыгмы праграмавання могуць здавацца не ў ладах сябар з сябрам, але абедзве ў роўнай меры падтрымліваюцца ў Powershell. Практычна ўсе праграмныя мовы, функцыянальныя і не, маюць сродкі пашыранага звязвання імён і значэнняў; Класы, падобна struct-ам і record-ам, гэта ўсяго толькі адзін падыход. Калі мы абмяжуем выкарыстанне Класаў звязваннем імён і значэнняў і станем пазбягаць такіх "цяжкіх" аб'ектна-арыентаваных праграмных канцэпцый, як атрыманне ў спадчыну, палімарфізм, або змяняльнасць (mutability), мы зможам выкарыстоўваць іх перавагі, не ўскладняючы наш код. Далей, дадаючы нязменныя (immutable) метады пераўтварэнні тыпаў, мы можам узбагаціць Класамі наш функцыянальны код.

Магія кастаў

Касты адна з самых магутных фіч у Powershell. Калі вы падвяргаеце значэнне касту, вы належыце на дадаванае асяроддзем у ваша прыкладанне магчымасць няяўных ініцыялізацыі і валідацыі. Напрыклад, просты каст радка ў [xml] прагоніць яе праз код парсера і згенеруе поўнае дрэва xml. Мы можам у сваім кодзе выкарыстоўваць Класы з той жа мэтай.

Каст хэштабліц

Калі ў вас няма канструктара, вы можаце працягваць і далей без яго, выкарыстоўваючы кастаў хэштабліцы ў тып вашага класа. Не забудзьцеся скарыстацца атрыбутамі валідацыі, каб у поўнай меры скарыстацца гэтым патэрнам. Заадно мы можам выкарыстоўваць тыпізаваныя ўласцівасці класа, каб запусціць яшчэ глыбейшую логіку ініцыялізацыі і валідацыі.

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
}

Акрамя таго, каст дапамагае атрымаць чыстую выснову. Параўнайце выснову масіва хэштабліц Cluster перададзены ў Format-Table c тым, што атрымаецца, калі перш каставаць гэтыя хэштабліцы ў клас. Уласцівасці класа заўсёды пералічваюцца ў тым парадку, у якім яны тамака вызначаныя. Не забудзьцеся дадаць ключавое слова hidden перад усімі тымі ўласцівасцямі, якія не павінны быць бачныя ў выдачы.

Функцыянальны Powershell з класамі - не аксюмарон, я гарантую гэта

Каст значэнняў

Калі ў вас ёсць канструктар з адным аргументам, каст значэння да вашага тыпу класа перадасць значэнне гэтаму вашаму канструктару, у якім вы можаце ініцыялізаваць інстанс вашага класа

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"

Каст да радка

Таксама вы можаце перавызначыць метад класа [string] ToString(), каб вызначыць логіку радковага прадстаўлення аб'екта, напрыклад выкарыстоўваць інтэрпаляцыю радкоў.

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

Каст серыялізаваных інстансаў

Каст дазваляе бяспечную дэсерыялізацыю. Прыклады ніжэй завершацца памылкай, калі дадзеныя не адпавядаюць нашай спецыфікацыі ў Cluster.

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

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

Касты ў вашым функцыянальным кодзе

Функцыянальныя праграмы спачатку вызначаюць структуры дадзеных, затым імплементуюць праграму як паслядоўнасць трансфармацый над нязменнымі структурамі дадзеных. Нягледзячы на ​​супярэчлівае ўражанне, класы рэальна дапамагаюць пісаць функцыянальны код, дзякуючы метадам канвертавання тыпаў.

Ці функцыянальны Powershell я пішу?

Шмат людзей якія прыйшлі з C# або з падобным мінулым пішуць Powershell, які падобны на З#. Паступаючы так, вы адмаўляецеся ад выкарыстання канцэпцый функцыянальнага праграмавання і, верагодна, атрымаеце карысць узмоцнена пагрузіўшыся ў аб'ектна-арыентаванае праграмаванне ў Powershell ці лепш вывучыўшы функцыянальнае праграмаванне.

Калі вы глыбока належыце на трансфармацыю імутабельных дадзеных, выкарыстоўваючы канвееры (|), Where-Object, ForEach-Object, Select-Object, Group-Object, Sort-Object і т. д. - у вас больш функцыянальны стыль, і вам дапаможа выкарыстанне класаў Powershell у функцыянальным стылі.

Функцыянальнае выкарыстанне класаў

Касты, хоць і выкарыстоўваюць альтэрнатыўны сінтаксіс, усяго толькі мапінг паміж двума даменамі. У канвееры можна змапіць масіў значэнняў, выкарыстоўваючы ForEach-Object.

У прыкладзе ніжэй канструктар Node выконваецца кожны раз калі адбываецца касты да Datum, і гэта дае нам магчымасць не пісаць ладную колькасць кода. У выніку наш канвеер факусуюць на дэкларатыўным запыце дадзеных і агрэгацыі, у той час як нашы класы бяруць на сябе парсінг дадзеных і валідацыю.

# Пример комбинирования классов с конвейерами для 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

Упакоўка класа для перавыкарыстання

Нішто не гэтак ужо добра, як здаецца

На жаль, класы не могуць быць экспартаваны модулямі тым жа чынам, як функцыі ці зменныя; але хітрасці сякія-такія ёсць. Дапушчальны, вашы класы вызначаны ў файле ./my-classes.ps1

  • Вы можаце зрабіць датсорсінг файла з класамі:. ./my-classes.ps1. Гэта выканае my-classes.ps1 у вашай бягучай вобласці бачнасці і вызначыць тамака ўсе класы з файла.

  • Вы можаце стварыць модуль Powershell, які экспартуе ўсе вашы карыстацкія API (камандлеты) і ўсталяваць зменную ScriptsToProcess = "./my-classes.ps1" у маніфесце вашага модуля, з тым жа вынікам: ./my-classes.ps1 выканаецца ў вашым асяроддзі .

Які б варыянт вы ні абралі, не забывайце, што сістэма тыпаў Powershell не можа разруліць аднайменныя тыпы, загружаныя з розных месцаў.
Нават калі вы загрузілі з розных месцаў два ідэнтычныя класы з аднолькавымі ўласцівасцямі, вы рызыкуеце нарвацца на праблемы.

шлях наперад

Лепшы спосаб пазбегнуць праблем з дазволам тыпаў - ніколі не выстаўляць для карыстачоў вашыя класы. Замест таго, каб чакаць, што карыстач імпартуе вызначаны ў класе тып, экспартуйце з вашага модуля функцыю, якая вызваляе ад неабходнасці напроста звяртацца да класа. У дачыненні да Cluster, мы можам экспартаваць функцыю New-Cluster, якая падтрымае дружалюбныя карыстачу наборы параметраў і верне 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

Што яшчэ пачытаць

About Classes
Defensive PowerShell
Functional Programming in PowerShell

Крыніца: habr.com

Дадаць каментар