PowerShell-Klassen und fortgeschrittenes Modul-Design
Klassen, Enums, Vererbung und Modul-Manifeste in PowerShell: wie du wartbare Module baust, versionierst und ueber PSGallery verteilst.
Warum ueberhaupt Klassen in PowerShell?
Die meisten IT-Admins schreiben PowerShell als Skriptsammlung: ein paar Funktionen, viele globale Variablen, alles in einer .ps1. Das funktioniert, bis das Skript wachsen und mehrere Leute daran mitpflegen. Sobald du Zustand (State) ueber mehrere Funktionsaufrufe hinweg konsistent halten musst - zum Beispiel eine Verbindung zu einem Ticketsystem, ein Inventar-Objekt mit Validierungsregeln oder eine Konfigurationsstruktur mit Abhaengigkeiten - lohnt sich objektorientiertes Design.
Seit PowerShell 5.0 (2016) gibt es dafuer das Schluesselwort class. Es ist syntaktischer Zucker ueber dem .NET-Typsystem, erzeugt aber echte .NET-Typen zur Compile-Zeit des Skripts. Das unterscheidet es fundamental von den alten Ansaetzen mit New-Object PSObject oder [PSCustomObject], die nur lose typisierte Objekte ohne Methodenverhalten liefern.
Dieser Artikel baut ein durchgaengiges Beispiel: ein internes Modul KmuAssetTools, das Hardware-Assets in einem Schweizer KMU verwaltet - inklusive Klassenhierarchie, Enum-basiertem Status, Modul-Manifest und Verteilungsstrategie.
Das class-Keyword im Detail
Eine minimale Klasse sieht so aus:
class AssetBase {
[string]$AssetTag
[string]$Standort
[datetime]$Erfasst = (Get-Date)
[string] ToString() {
return "$($this.AssetTag) @ $($this.Standort)"
}
}
Wichtige Eigenheiten, die in der Praxis stolpern lassen:
- Typisierung ist Pflicht in der Praxis. Technisch erlaubt PowerShell auch untypisierte Properties, aber ohne
[string],[int],[datetime]etc. verlierst du Validierung und IntelliSense komplett. - Klassen werden beim Parsen des Skripts geladen, nicht erst bei Ausfuehrung. Das heisst: Eine Klasse, die eine andere Klasse aus einem anderen Modul referenziert, muss dieses Modul VOR dem
class-Block perusing moduleeinbinden -Import-Modulereicht hier nicht, weil das Parsen sonst zu frueh scheitert. - Kein Scripting-Scope wie bei Funktionen. Innerhalb einer Methode gibt es kein
$script:-Zugriff auf Variablen ausserhalb der Klasse, ausser du arbeitest explizit mit Modul-Variablen. - Klassen sind nicht automatisch exportierbar. Ein
class-Definition wird nie durchExport-ModuleMembertransportiert (dazu spaeter mehr).
using module .\KmuAssetTools.psd1
class Workstation : AssetBase {
# Vererbung siehe unten
}
Enums: typsichere Konstanten
Enums loesen ein Klassikerproblem: Magische Strings wie "Aktiv", "aktiv", "ACTIVE" in verschiedenen Skriptteilen. Mit enum definierst du eine geschlossene Menge gueltiger Werte, die der Parser bereits zur Skriptzeit prueft.
enum AssetStatus {
Bestellt
ImEinsatz
InReparatur
Ausgemustert
}
enum Prioritaet {
Niedrig = 0
Normal = 1
Hoch = 2
Kritisch = 3
}
Enums basieren standardmaessig auf [int32], du kannst aber auch [byte] oder [long] als Basistyp waehlen (enum Prioritaet : byte { ... }), was bei sehr vielen Modulen mit vielen Enum-Werten Speicher spart - in der Praxis fast nie relevant, aber gut zu wissen fuer Interviews.
$status = [AssetStatus]::ImEinsatz
if ($status -eq [AssetStatus]::Ausgemustert) {
Write-Warning "Asset ist ausgemustert, keine neuen Tickets zulaessig."
}
# Enum aus String parsen (z. B. aus CSV-Import) - wirft Exception bei ungueltigem Wert
$importWert = "InReparatur"
$parsed = [AssetStatus]$importWert
Konstruktoren und Methoden
Konstruktoren heissen wie die Klasse und initialisieren neue Instanzen. PowerShell erlaubt mehrere Konstruktoren (Overloading) ueber unterschiedliche Parameterlisten:
class Workstation : AssetBase {
[AssetStatus]$Status
[string]$Benutzer
hidden [int]$_garantieMonate = 36
# Konstruktor 1: nur Pflichtfelder
Workstation([string]$assetTag, [string]$standort) {
$this.AssetTag = $assetTag
$this.Standort = $standort
$this.Status = [AssetStatus]::Bestellt
}
# Konstruktor 2: mit Benutzerzuweisung (Overload)
Workstation([string]$assetTag, [string]$standort, [string]$benutzer) : base() {
$this.AssetTag = $assetTag
$this.Standort = $standort
$this.Benutzer = $benutzer
$this.Status = [AssetStatus]::ImEinsatz
}
[void] Ausmustern() {
if ($this.Status -eq [AssetStatus]::Ausgemustert) {
throw "Asset $($this.AssetTag) ist bereits ausgemustert."
}
$this.Status = [AssetStatus]::Ausgemustert
$this.Benutzer = $null
}
[bool] IstInGarantie([datetime]$kaufdatum) {
return $kaufdatum.AddMonths($this._garantieMonate) -gt (Get-Date)
}
# Statische Methode: gehoert zur Klasse, nicht zur Instanz
static [Workstation] AusInventarZeile([string]$csvZeile) {
$teile = $csvZeile -split ';'
return [Workstation]::new($teile[0], $teile[1])
}
}
Praxis-Hinweise zu Methoden:
hiddenversteckt Properties/Methoden vorGet-Memberund Tab-Completion, aber nicht vor gezieltem Zugriff. Es ist Konvention, kein Sicherheitsmechanismus - jeder kann$obj.psobject.Propertiesdurchsuchen und den Wert trotzdem lesen.static-Methoden brauchst du fuer Factory-Pattern (wieAusInventarZeileoben) oder fuer zustandslose Hilfsfunktionen, die zur Klasse gehoeren, aber keine Instanz benoetigen.- Jede Methode muss einen Rueckgabetyp deklarieren, auch
[void]fuer “kein Rueckgabewert”. Das ist ein haeufiger Anfaengerfehler - Weglassen erzeugt einen Parserfehler, nicht nur eine Warnung. - Es gibt keine Methoden-Ueberladung anhand des Rueckgabetyps, nur anhand der Parameterliste (wie in C#/.NET ueblich).
Vererbung zwischen Klassen
PowerShell unterstuetzt Einfachvererbung (keine Mehrfachvererbung wie in Python, aber wie in C#/Java). Eine abgeleitete Klasse erbt alle nicht-hidden-static Members der Basisklasse und kann Methoden ueberschreiben.
class AssetBase {
[string]$AssetTag
[string]$Standort
[datetime]$Erfasst = (Get-Date)
[string] Beschreibung() {
return "Generisches Asset: $($this.AssetTag)"
}
}
class Server : AssetBase {
[int]$AnzahlCores
[string]$Hypervisor
Server([string]$assetTag, [string]$standort, [int]$cores) {
$this.AssetTag = $assetTag
$this.Standort = $standort
$this.AnzahlCores = $cores
}
# Methoden-Override
[string] Beschreibung() {
return "Server $($this.AssetTag) mit $($this.AnzahlCores) Cores in $($this.Standort)"
}
}
Ein paar Fallstricke, die im Alltag auftauchen:
- Kein
base.Beschreibung()-Aufruf mit Syntax wie in C#. Um die Methode der Basisklasse trotzdem aufzurufen, musst du das Objekt explizit als Basistyp casten:([AssetBase]$this).Beschreibung(). - Konstruktoren werden nicht automatisch vererbt. Jede abgeleitete Klasse braucht ihren eigenen Konstruktor, der per
: base(...)den passenden Basiskonstruktor aufrufen kann - oder implizit den parameterlosen Konstruktor der Basisklasse nutzt, falls vorhanden. - Abstrakte Klassen gibt es nicht nativ. Man simuliert sie, indem der Basisklassen-Konstruktor prueft, ob
$this.GetType().Name -eq 'AssetBase'ist, und in dem Fall eine Exception wirft - ein verbreiteter, wenn auch nicht ganz eleganter Workaround. - Bei Aenderungen an einer Basisklasse muss das komplette Modul neu geladen werden (
Remove-Module+Import-Module), weil Klassen im Session-Speicher fixiert sind. Ein blosses erneutes Dot-Sourcing reicht oft nicht, was in der PowerShell-ISE oder in langlaufenden Sessions zu verwirrenden “alte Version wird noch genutzt”-Situationen fuehrt.
Modul-Struktur und Manifest (.psd1)
Ein produktionsreifes Modul besteht aus mehr als einer .psm1-Datei. Die uebliche Struktur fuer KmuAssetTools:
KmuAssetTools/
├── KmuAssetTools.psd1 # Manifest - Metadaten, Version, Exporte
├── KmuAssetTools.psm1 # Root-Modul - laedt Klassen und Funktionen
├── Classes/
│ ├── AssetBase.ps1
│ ├── Workstation.ps1
│ └── Server.ps1
├── Public/
│ ├── Get-KmuAsset.ps1
│ └── New-KmuAsset.ps1
├── Private/
│ └── Test-AssetTagFormat.ps1
└── en-US/
└── about_KmuAssetTools.help.txt
Das Manifest erzeugst du nicht per Hand, sondern mit New-ModuleManifest:
New-ModuleManifest -Path .\KmuAssetTools.psd1 `
-RootModule 'KmuAssetTools.psm1' `
-ModuleVersion '1.0.0' `
-Author 'IT-Team' `
-CompanyName 'Muster AG' `
-Description 'Interne Asset-Verwaltung fuer Workstations und Server' `
-PowerShellVersion '5.1' `
-FunctionsToExport @('Get-KmuAsset', 'New-KmuAsset') `
-CmdletsToExport @() `
-VariablesToExport @() `
-AliasesToExport @() `
-Tags @('Inventory', 'ITAM', 'Internal') `
-ProjectUri 'https://git.intern.muster-ag.ch/it/kmuassettools' `
-LicenseUri 'https://git.intern.muster-ag.ch/it/kmuassettools/LICENSE'
Die generierte .psd1 ist eine reine Hashtable-Datei - kein ausfuehrbarer Code, sondern Deklaration:
@{
RootModule = 'KmuAssetTools.psm1'
ModuleVersion = '1.0.0'
GUID = 'a1b2c3d4-e5f6-7890-abcd-1234567890ab'
Author = 'IT-Team'
CompanyName = 'Muster AG'
Description = 'Interne Asset-Verwaltung fuer Workstations und Server'
PowerShellVersion = '5.1'
FunctionsToExport = @('Get-KmuAsset', 'New-KmuAsset')
CmdletsToExport = @()
VariablesToExport = @()
AliasesToExport = @()
PrivateData = @{
PSData = @{
Tags = @('Inventory', 'ITAM', 'Internal')
ProjectUri = 'https://git.intern.muster-ag.ch/it/kmuassettools'
ReleaseNotes = 'Erste interne Version.'
}
}
}
Das Root-Modul KmuAssetTools.psm1 bindet Klassen und Funktionen zusammen und ist der einzige Ort, an dem Export-ModuleMember vorkommt:
# KmuAssetTools.psm1
$klassenDateien = Get-ChildItem -Path "$PSScriptRoot\Classes" -Filter '*.ps1'
$publicDateien = Get-ChildItem -Path "$PSScriptRoot\Public" -Filter '*.ps1'
$privateDateien = Get-ChildItem -Path "$PSScriptRoot\Private" -Filter '*.ps1'
foreach ($datei in @($klassenDateien + $privateDateien + $publicDateien)) {
try {
. $datei.FullName
} catch {
Write-Error "Fehler beim Laden von $($datei.FullName): $_"
}
}
Export-ModuleMember -Function $publicDateien.BaseName
Export-ModuleMember gezielt steuern
Export-ModuleMember entscheidet, was ausserhalb des Moduls sichtbar ist - Funktionen, Variablen und Aliase, aber keine Klassen. Klassen exportierst du stattdessen ueber using module im Konsumenten-Skript.
# Nur Funktionen exportieren, Variablen bewusst nicht
Export-ModuleMember -Function 'Get-KmuAsset', 'New-KmuAsset' -Variable $null
# Alias mitgeben
New-Alias -Name 'gka' -Value 'Get-KmuAsset'
Export-ModuleMember -Function 'Get-KmuAsset' -Alias 'gka'
Die Reihenfolge Manifest vs. Export-ModuleMember verwirrt viele: Beides muss uebereinstimmen. Steht eine Funktion nicht in FunctionsToExport des Manifests, wird sie nicht exportiert, selbst wenn Export-ModuleMember sie explizit nennt - das Manifest gewinnt immer. Praktisch heisst das: Bei FunctionsToExport = @('Get-KmuAsset') im Manifest kannst du in der .psm1 ruhig Export-ModuleMember -Function * schreiben, ohne dass ungewollt mehr exportiert wird.
Entscheidungstabelle: Skript, Funktion oder Klasse?
| Kriterium | Einzelnes Skript (.ps1) | Funktionsmodul | Klassenbasiertes Modul |
|---|---|---|---|
| Zustand ueber mehrere Aufrufe | schwierig (globale Variablen) | via Modul-Scope moeglich | nativ ueber Instanzen |
| Wiederverwendbarkeit | gering | hoch | hoch |
| Lernkurve fuers Team | niedrig | mittel | hoch (OOP-Konzepte) |
| Validierung/Kapselung | manuell | manuell | eingebaut (Konstruktor, hidden) |
| Remoting-Kompatibilitaet | gut | gut | eingeschraenkt (Deserialisierung) |
| Testbarkeit mit Pester | einfach | einfach | Klassen brauchen using module in Testdatei |
| Typischer Einsatz | Ad-hoc-Aufgabe, Migration | wiederkehrende Automatisierung | Domain-Modell, komplexe Tools |
Fuer Unit-Tests mit klassenbasierten Modulen lohnt sich ein Blick in Pester: Unit-Tests fuer PowerShell - dort wird auch der using module-Sonderfall in Testdateien behandelt.
Versionierung nach SemVer
PowerShell-Module folgen der Konvention Semantic Versioning (Major.Minor.Patch):
- Major (z. B. 1.x.x -> 2.0.0): Breaking Change, z. B. Funktionssignatur geaendert, Parameter entfernt.
- Minor (1.0.x -> 1.1.0): Neue Funktionalitaet, abwaertskompatibel.
- Patch (1.0.0 -> 1.0.1): Bugfix, keine neuen Features.
# Versionsnummer im Manifest aktualisieren, ohne die Datei manuell zu bearbeiten
Update-ModuleManifest -Path .\KmuAssetTools.psd1 -ModuleVersion '1.1.0' `
-ReleaseNotes 'Neu: Support fuer Server-Assets mit Hypervisor-Feld.'
Verteilung: intern vs. PowerShell Gallery
Fuer ein KMU gibt es drei realistische Verteilwege:
Entscheidung: Wie verteile ich mein Modul?
│
├─ Nur ein Team, kein Internet-Zugriff des Zielsystems noetig?
│ └─ Netzwerkfreigabe + PSModulePath-Eintrag
│ (z. B. \\fileserver\it\PSModules in $env:PSModulePath)
│
├─ Mehrere interne Teams/Standorte, Versionskontrolle gewuenscht?
│ └─ Privates NuGet-Feed (Azure Artifacts, ProGet, GitHub Packages)
│ + Register-PSResourceRepository
│
└─ Modul ist generisch nuetzlich, auch fuer die Community?
└─ PowerShell Gallery (oeffentlich, kostenlos)
Fuer den internen Fall reicht oft schon eine simple Freigabe:
# Auf Client-Seite dauerhaft in PSModulePath aufnehmen (Profil-Skript)
$freigabe = '\\fileserver\it\PSModules'
if ($env:PSModulePath -notlike "*$freigabe*") {
$env:PSModulePath += ";$freigabe"
}
Fuer die oeffentliche PowerShell Gallery brauchst du ein Konto auf powershellgallery.com und einen API-Key. Mit dem modernen Microsoft.PowerShell.PSResourceGet-Modul (Nachfolger von PowerShellGet v2, seit PowerShell 7.4 vorinstalliert):
# Vor der Veroeffentlichung: Modul lokal validieren
Test-ModuleManifest -Path .\KmuAssetTools.psd1
Invoke-ScriptAnalyzer -Path .\KmuAssetTools -Recurse
# Veroeffentlichen
Publish-PSResource -Path .\KmuAssetTools -Repository PSGallery -ApiKey $env:PSGALLERY_API_KEY
Vor jeder Veroeffentlichung gehoert ein Blick auf die interne Automatisierung: Wer ein Modul fuer produktive Ablaeufe baut, sollte es auch testbar machen (Pester: Unit-Tests fuer PowerShell) und pruefen, ob AppLocker/WDAC-Richtlinien im Unternehmen die Ausfuehrung ueberhaupt erlauben (siehe Skript-Sicherheit: AppLocker und WDAC).
Praxis-Checkliste fuer produktive Module
- Manifest mit expliziten
FunctionsToExport- keine Wildcards. - Public/Private-Ordnerstruktur, Klassen in eigenem
Classes-Ordner. - SemVer konsequent einhalten,
ReleaseNotesbei jedem Publish aktualisieren. - Pester-Tests fuer alle exportierten Funktionen und kritische Klassenmethoden.
RequiredModulesim Manifest pflegen, wenn dein Modul von anderen Modulen abhaengt.- GUID im Manifest niemals per Copy-Paste aus einem anderen Modul uebernehmen - jede
.psd1braucht eine eindeutige GUID ([guid]::NewGuid()). - Bei internen Modulen: Versionsnummer und Aenderungsprotokoll auch in IT-Dokumentation und Inventar nachfuehren, damit Kollegen wissen, welche Version wo im Einsatz ist.
Weiterlernen
- about_Classes – Microsoft Learn
- about_Classes_Inheritance – Microsoft Learn
- about_Module_Manifests – Microsoft Learn
- How to Write a PowerShell Module Manifest – Microsoft Learn
- Publish-PSResource – Microsoft Learn
- PowerShell Gallery – FAQs
Videos
Kommentare
Frage, Verbesserungsvorschlag oder eigene Erfahrung zu diesem Artikel? Schreib einen Kommentar. Neue Beiträge erscheinen nach kurzer Moderation.
- Lade Kommentare …