Public/New-HDTFeatureService.ps1

function New-HDTFeatureService {
    <#
        .SYNOPSIS
            The real IFeatureService: a thin adapter over Get-WindowsFeature and
            Install-WindowsFeature.
 
        .DESCRIPTION
            DESIGN 10.2's Install-WindowsFeature wrapper. Rule 5 forbids engine
            logic from calling it directly, so the InstallRoles step receives this
            object and can be handed New-HDTFakeFeatureService in a test.
 
            IT IS BRANCH-FREE, WHICH IS WHY IT IS NOT UNIT TESTED (rule 1's
            adapter exception). Every decision - is this feature name real, is it
            already installed, which of these still have to be installed, does the
            result mean restart - is made by the step against the flat listing
            this returns. The adapter's whole job is to call the cmdlet and
            project its output.
 
            THE TWO CMDLETS LIVE IN THE ServerManager MODULE, which exists only on
            a Windows Server SKU. Nothing here imports it: it auto-loads on a
            server, and on a client the call fails with a CommandNotFoundException
            naming Get-WindowsFeature - which is a better message than an import
            error naming a module an administrator has never heard of. The
            contract test skips this row on a client for the same reason.
 
            InstallState IS PASSED THROUGH AS THE STRING Get-WindowsFeature
            reports: Installed, Available or Removed. Removed - the payload is
            gone from the image - is the state that makes source: necessary, and
            an adapter that collapsed it into Available would make the .NET 3.5
            case undiagnosable.
 
        .PARAMETER Journal
            The shared cross-service operation journal. When supplied, every call
            is appended to it as well as to $Operations.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject carrying GetFeature and
            InstallFeature, plus Operations, GetOperationName and ServiceName.
 
        .EXAMPLE
            $feature = New-HDTFeatureService
            $feature.GetFeature() | Where-Object { $_.InstallState -eq 'Available' }
 
        .EXAMPLE
            $feature.InstallFeature([string[]] @('Web-Server'), $true, 'X:\Sources\SxS')
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Builds the service object; installing is done by the caller through it, and the step that calls it declares SupportsShouldProcess.')]
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter()]
        [AllowNull()]
        [System.Collections.ArrayList] $Journal = $null
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $service = [pscustomobject] @{
        Operations  = [System.Collections.ArrayList]::new()
        Journal     = $Journal
        ServiceName = 'FeatureService'
    }

    $service | Add-Member -MemberType ScriptMethod -Name Record -Value {
        param([string] $Operation, [object[]] $Argument)

        [void] $this.Operations.Add([pscustomobject] @{
                Sequence  = $this.Operations.Count + 1
                Operation = $Operation
                Arguments = $Argument
            })

        if ($null -ne $this.Journal) {
            [void] $this.Journal.Add([pscustomobject] @{
                    Sequence  = $this.Journal.Count + 1
                    Service   = $this.ServiceName
                    Operation = $Operation
                    Arguments = $Argument
                })
        }
    }

    $service | Add-Member -MemberType ScriptMethod -Name GetOperationName -Value {
        return [string[]] @($this.Operations | ForEach-Object { $_.Operation })
    }

    $service | Add-Member -MemberType ScriptMethod -Name GetFeature -Value {
        $this.Record('GetFeature', @())

        $row = @(Get-WindowsFeature | ForEach-Object {
                [pscustomobject] @{
                    Name         = [string] $_.Name
                    DisplayName  = [string] $_.DisplayName
                    InstallState = [string] $_.InstallState
                }
            })

        # The unary comma is mandatory: a ScriptMethod collapses a single-element
        # array to a scalar without it.
        return , ([object[]] $row)
    }

    $service | Add-Member -MemberType ScriptMethod -Name InstallFeature -Value {
        param([string[]] $Name, [bool] $IncludeManagementTools, [string] $Source)

        $this.Record('InstallFeature', @($Name, $IncludeManagementTools, $Source))

        $argument = @{ Name = [string[]] $Name }
        if ($IncludeManagementTools) { $argument['IncludeManagementTools'] = $true }
        if (-not [string]::IsNullOrWhiteSpace($Source)) { $argument['Source'] = $Source }

        $result = Install-WindowsFeature @argument

        return [pscustomobject] @{
            Success       = [bool] $result.Success
            RestartNeeded = ([string] $result.RestartNeeded -ne 'No')
            ExitCode      = [int] $result.ExitCode
            Message       = [string] $result.ExitCode
            FeatureResult = [string[]] @($result.FeatureResult | ForEach-Object { [string] $_.Name })
        }
    }

    return $service
}