带有类的功能性 Powershell 并不是一个矛盾的说法,我保证这一点

嘿哈布尔! 我向您展示这篇文章的翻译 “带有类的功能性 PowerShell。
我保证这不是矛盾”
作者:克里斯托弗·库奇。

面向对象和函数式编程范式看似相互矛盾,但两者在 Powershell 中都受到同等支持。 几乎所有的编程语言,无论是否是函数式的,都具有扩展名称-值绑定的功能; 类,如结构和记录,只是一种方法。 如果我们将类的使用限制为名称和值的绑定,并避免大量的面向对象编程概念,例如继承、多态性或可变性,那么我们就可以利用它们的优势,而不会使我们的代码复杂化。 此外,通过添加不可变类型转换方法,我们可以用类丰富我们的功能代码。

种姓的魔力

种姓是 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
}

此外,铸造有助于获得干净的输出。 将传递给 Format-Table 的 Cluster 哈希表数组的输出与首先将这些哈希表转换为类时得到的结果进行比较。 类的属性始终按照它们在其中定义的顺序列出。 不要忘记在您不希望在结果中可见的所有属性之前添加隐藏关键字。

带有类的功能性 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,它与 C# 类似。 通过这样做,您将不再使用函数式编程概念,并且可能会从深入研究 Powershell 中的面向对象编程或了解有关函数式编程的更多信息中受益。

如果您严重依赖使用管道 (|)、Where-Object、ForEach-Object、Select-Object、Group-Object、Sort-Object 等来转换不可变数据 - 您将拥有更实用的风格,并且将从使用 Powershell 中受益函数式风格的类。

类的功能使用

种姓虽然使用替代语法,但只是两个域之间的映射。 在管道中,您可以使用 ForEach-Object 映射值数组。

在下面的示例中,每次转换 Datum 时都会执行 Node 构造函数,这使我们有机会不必编写大量代码。 因此,我们的管道专注于声明性数据查询和聚合,而我们的类则负责数据解析和验证。

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

  • 您可以使用类来点源文件:。 ./我的班级.ps1。 这将在当前范围内执行 my-classes.ps1 并定义该文件中的所有类。

  • 您可以创建一个导出所有自定义 API (cmdlet) 的 Powershell 模块,并在模块清单中设置 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

还有什么要读的

关于课程
防御性PowerShell
PowerShell 中的函数式编程

来源: habr.com

添加评论