Das Options-Pattern in der Go-Programmierung ist eine Umsetzung des Open-Close Principals und dient dazu die Auswirkungen von Code-Änderungen bzw. Erweiterungen zu minimieren und damit Systeme leicht erweiterbar zu machen.
“Modules should be both open (for extension) and closed (for modification).” - Bertrand Meyer
In diesem Artikel werden wir uns eingehend mit dem Options-Pattern in Golang befassen und wie es zur Verbesserung der Codelesbarkeit und -flexibilität beitragen kann.
Das Options-Pattern ist eine Designpraxis, die es ermöglicht, Funktionen mit variabler Anzahl von Parametern zu erstellen, ohne die Lesbarkeit des Codes zu beeinträchtigen. In Golang wird diese Technik oft verwendet, um “Konstruktoren” bzw. Factory-Methoden von Structs oder Funktionen mit optionalen Parametern zu implementieren.
Durch die Verwendung des Option-Patterns wird der Code klarer und leichter verständlich. Benötigt eine Funktion z. B. mehrere Parameter, die als String-Werte übergeben werden müssen, wird eine Parameterliste schnell unübersichtlich. Mit dem Options-Pattern werden die Parameter typisiert und Entwickler können auf einen Blick sehen, welche Optionen bzw. Parameter für eine Funktion verfügbar sind, ohne den gesamten Quellcode durchsuchen zu müssen.
String-Beisiel:
func BuildMyObjekt(param1, param2, param3, param3 string) MyObj {
//doSth
}
//Aufruf irgendwo
BuildObject("server1", ":8090", "server3", "4231")
Ohne das Options-Pattern sieht man recht schön, dass man im Quellcode die Semantik der einzelnen Parameter nur schwer erkennen kann.
Options-Pattern-Beispiel:
func BuildMyObjekt(opts ...Option) MyObj {
//doSth
}
//Aufruf irgendwo
BuildObject(WithConnectServer("server1"),
WithConnectServerBinding(":8090),
WithXYServer("server3"),
WithMaxConnections("4231))
Mit dem Options-Pattern ist der Code deutlich lesbarer.
Das Optionsmuster ermöglicht es Entwicklern, nur die benötigten Parameter anzugeben und den Rest mit Standardwerten zu füllen. Dadurch wird die Anwendung flexibler und anpassbar, ohne die Integrität des Codes zu beeinträchtigen.
Im obigen Beispiel müssen in der ersten Variante immer alle Werte angeben werden. Beim Einsatz des Options-Pattern hingegen, hat man die Möglichkeit auch nur die Werte zu übergeben, die für den aktuellen Einsatz benötigt werden.
//Aufruf irgendwo
BuildObject(WithXYServer("server3"),
WithMaxConnections("4231))
Spätere Erweiterungen bzw. das Hinzufügen von weiteren Optionen ist ebenfalls ohne Probleme möglich. Die API bleibt stabil.
Das Options-Pattern in Golang wird mit Hilfe von Function-Types und unter Anwendung von Closures umgesetzt.
Schritt 1: Über einen Function-Type wird eine Funktion definiert , die das
zu konfigurierende Objekt als Parameter übergeben bekommt. Im folgendem
Beispiel soll der Typ *http.Server
konfiguriert werden.
type Option func(server *http. Server)
Schritt 2: Die einzelnen Konfigurationen werden anschließend über Factory-Methoden mit Closures umgesetzt. Die Funktionen liefern hierbei konkrete Implementierungen des definierten Function-Types und konfigurieren das übergebene “Ziel-Objekt”.
func WithBinding ( binding string ) Option {
return func( server *http. Server ) {
server .Addr = binding
}
}
Schitt 3: In einer entsprechenden Factory-Function bzw. eines Konstruktor werden die übergebenen Optionen in einr Schleife auf das zu konfigurierende Objekt angewendet. Standardwerte können hier ebenfalls definiert und überschrieben werden.
func NewServer(opts ...ServerOption) *ServerConfig {
// Standardwerte für die Konfiguration festlegen
config := &ServerConfig{
Host: "localhost",
Port: 8080,
Timeout: 30 * time.Second,
}
// Optionen anwenden
for _, opt := range opts {
opt(config)
}
return config
}
Schritt 4: Ein Client dieser API kann somit die einzelnen Konfigurationen über Parameterübergabe vornehmen und weitere Optionen können später ohne API-Anpassungen aufgenommen werden.
server := NewServer (WithBinding (":8080 "))
Hier ist das komplette Beispiel:
type Option func(server *http.Server)
func WithBinding(binding string) Option {
return func(server *http.Server) {
server.Addr = binding
}
}
func WithReadTimeout(readTimeout time.Duration) Option {
return func(server *http.Server) {
server.ReadTimeout = readTimeout
}
}
func NewServer(opts ...Option) *http.Server {
server := &http.Server{}
for _, opt := range opts {
opt(server)
}
return server
}
Dieses Beispiel ist übrigens Bestandteil unseres Golang Softwaredesign Workshops
17.11.2023
Der Author auf LinkedIn: Kristian Köhler und Mastodon: @kkoehler@mastodontech.de
Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.
zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)
Source Fellows GmbH
Lerchenstraße 31
72762 Reutlingen
Telefon: (0049) 07121 6969 802
E-Mail: info@source-fellows.com