用于缓冲管道输入到PowerShell cmdlet的设计模式

时间:2017-01-04 14:00:17

标签: powershell design-patterns

我偶尔会遇到支持Cmdlet的管道输入有意义的情况,但我想要做的操作(例如数据库访问)对批量处理合理数量的对象是有意义的。

实现此目标的典型方法如下所示:

function BufferExample {
<#
.SYNOPSIS
Example of filling and using an intermediate buffer.
#>
[CmdletBinding()]
param(
    [Parameter(ValueFromPipeline)]
    $InputObject
)

BEGIN {
    $Buffer = New-Object System.Collections.ArrayList(10)
    function _PROCESS {
        # Do something with a batch of items here.
        Write-Output "Element 1 of the batch is $($Buffer[1])"
        # This could be a high latency operation such as a DB call where we 
        # retrieve a set of rows by primary key rather than each one individually.

        # Then empty the buffer.
        $Buffer.Clear()
    }
}

PROCESS {
    # Accumulate the buffer or process it if the buffer is full.
    if ($Buffer.Count -ne $Buffer.Capacity) {    
        [void]$Buffer.Add($InputObject)        
    } else {
        _PROCESS 
    }
}

END {
     # The buffer may be partially filled, so let's do something with the remainder.
    _PROCESS
}
}

是否有更少的样板&#34;这样做的方法?

一种方法可能是编写我称之为的函数&#34; _PROCESS&#34;这里接受数组参数但不接受管道输入,然后为暴露给用户的cmdlet构建一个代理函数,用于缓冲输入并传递缓冲区,如 Proxy commands

或者,我可以在我希望编写的cmdlet正文中添加源动态代码以支持此功能,但这似乎容易出错并且可能难以调试/理解。

3 个答案:

答案 0 :(得分:1)

管道的性质给你(你很容易)按照你想要的方式做了一些约束,主要是因为Process块被设计用来接收(和处理)一个对象。

如果你想缓冲所有对象,那就相当简单;您只需收集Process块中的数组或其他集合中的所有对象,然后在End块中完成所有工作;类似于像Sort-Object这样的cmdlet处理它的方式。

在为了底层资源而缓冲的情况下,比如基于Web的API,或者您的数据库访问示例,我认为您采取的方法需要针对具体情况。实现它的可能性不大。

其中一种方法是将操作分成2个(可能更多?)函数。

例如,我编写了一些函数来向Graphite发送指标。我在Format-GraphiteOut-Graphite之间拆分了这些功能。前者根据参数和管道生成格式正确的度量字符串,而后者将字符串发送到Graphite收集器。它允许客户端代码在获取和生成数据方面更加通用,因为它可以管道到Format-Graphite,或者单独调用它而不必担心网络部分效率低下。客户端代码不必处理手动收集自己的数据只是为了避免这种情况。不是没有代码进行演示的最佳示例,但我现在无法发布该代码。

单个操作的“昂贵”部分是初始化和拆除代码的另一种方法是在BeginEnd中执行这些操作,然后使用Process通常

例如,通过在Begin中建立的单个连接进行数十次数据库调用可能不是那么糟糕,甚至可能比建立一个大的SQL字符串并一次性发送它更好。

最终,我认为您可能会更好地查看每个用例并确定满足您需求的最佳方法,平衡性能/效率以及调用代码的简单/直观性。

如果您有特定用例并发布有关该问题的问题,我想阅读它;给我一个链接。

答案 1 :(得分:1)

我从您的示例中制作了一个可重复使用的cmdlet。

function Buffer-Pipeline {
[CmdletBinding()]
param(
    $Size = 10,
    [Parameter(ValueFromPipeline)]
    $InputObject
)
    BEGIN {
        $Buffer = New-Object System.Collections.ArrayList($Size)
    }

    PROCESS {
        [void]$Buffer.Add($_)

        if ($Buffer.Count -eq $Size) {    
            $b = $Buffer;
            $Buffer = New-Object System.Collections.ArrayList($Size)
            Write-Output -NoEnumerate $b
        }
    }

    END {
     # The buffer may be partially filled, so let's do something with the remainder.
        if ($Buffer.Count -ne 0) {  
            Write-Output -NoEnumerate $Buffer
        }
    }
}

用法:

@(1;2;3;4;5) | Buffer-Pipeline -Size 3 | % { "$($_.Count) items: ($($_ -join ','))" }

输出:

3 items: (1,2,3)
2 items: (4,5)

另一个例子:

1,2,3,4,5 | Buffer-Pipeline -Size 10 | Measure-Object

Count    : 2

处理每个批次:

 1,2,3 | Buffer-Pipeline -Size 2 | % { 
    $_ | % { "Starting batch of $($_.Count)" } { $_ * 100 } { "Done with batch of $($_.Count)" }
}

Starting batch of 2
100
200
Done with batch of 2
Starting batch of 1
300
Done with batch of 1

答案 2 :(得分:0)

我认为这是您正在使用的最佳方法之一。

但是当你正在寻找更好的调试时,我相信你可以在函数内部引入一些带有所需日志文件位置的LOGS。

此外,对于点源,你仍然可以编写Dot Source引用并调用另一个脚本中的函数,你可以将整个函数包装在一个函数中。

多功能的最佳方式是创建模块。

希望它对你有所帮助。