Functional Options Pattern
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy — Lektion 79 von 107.
Beim Erstellen von Structs mit vielen optionalen Konfigurationsfeldern können Konstruktorfunktionen unhandlich werden. Das Functional Options Pattern löst dies auf elegante Weise, indem es Funktionen zur Konfiguration von Objekten verwendet.
Betrachten Sie ein Server-Struct mit mehreren optionalen Einstellungen. Anstelle eines Konstruktors mit vielen Parametern definieren wir einen Optionstyp und Funktionen, die Optionen zurückgeben:
type Server struct {
host string
port int
timeout int
}
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout int) Option {
return func(s *Server) {
s.timeout = timeout
}
}Der Konstruktor akzeptiert eine variadische Liste von Optionen und wendet jede einzelne an:
func NewServer(host string, opts ...Option) *Server {
s := &Server{
host: host,
port: 8080, // Standard
timeout: 30, // Standard
}
for _, opt := range opts {
opt(s)
}
return s
}Jetzt wird das Erstellen von Servern lesbar und flexibel:
// Standardwerte verwenden
s1 := NewServer("localhost")
// Spezifische Optionen anpassen
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))Dieses Pattern glänzt, weil Aufrufer nur das angeben, was sie benötigen, Standardwerte an einer Stelle klar definiert sind und das Hinzufügen neuer Optionen bestehenden Code nicht beeinträchtigt. Es wird häufig in Go-Bibliotheken wie grpc und zap verwendet.
Aufgabe
EinfachLassen Sie uns eine konfigurierbare Datenbankverbindung mit dem Functional Options Pattern erstellen! Dieses Muster glänzt, wenn Sie viele optionale Einstellungen mit sinnvollen Standardwerten haben – genau das, was eine Datenbankverbindung benötigt.
Sie werden Ihren Code auf zwei Dateien verteilen:
database.go: Definieren Sie Ihren Datenbankverbindungstyp und die Optionsfunktionen.Erstellen Sie ein
DBConnection-Struct mit diesen nicht exportierten Feldern:host(string)port(int)username(string)password(string)maxConnections(int)timeout(int) - in Sekunden
Definieren Sie einen
Option-Typ als eine Funktion, die ein*DBConnectionmodifiziert.Erstellen Sie diese Optionsfunktionen, die eine
Optionzurückgeben:WithPort(port int)WithCredentials(username, password string)WithMaxConnections(max int)WithTimeout(seconds int)
Erstellen Sie einen Konstruktor
NewDBConnection(host string, opts ...Option), der ein*DBConnectionzurückgibt. Legen Sie diese Standardwerte fest: port5432, username"admin", password"secret", maxConnections10, timeout30. Wenden Sie alle bereitgestellten Optionen nach dem Festlegen der Standardwerte an.Fügen Sie eine
ConnectionString()-Methode hinzu, die die Verbindungsinformationen in diesem Format zurückgibt:[username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)main.go: Erstellen Sie Datenbankverbindungen mit verschiedenen Konfigurationen.Lesen Sie den Host ein und lesen Sie dann die Anzahl der anzuwendenden Optionen. Lesen Sie für jede Option den Optionstyp und dessen Wert(e) ein:
portgefolgt von der Portnummercredentialsgefolgt von Benutzername und Passwort (zwei Zeilen)maxconngefolgt von der maximalen Anzahl an Verbindungentimeoutgefolgt vom Timeout in Sekunden
Erstellen Sie eine
DBConnectionmit den angegebenen Optionen und geben Sie deren Verbindungsstring aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Hostname
- Zeile 2: Anzahl der Optionen
- Folgende Zeilen: Optionstyp und Wert(e)
Zum Beispiel, gegeben:
localhost
0Ihre Ausgabe sollte sein:
admin:secret@localhost:5432 (max:10, timeout:30s)Und gegeben:
db.example.com
2
port
3306
credentials
root
mypasswordIhre Ausgabe sollte sein:
root:mypassword@db.example.com:3306 (max:10, timeout:30s)Und gegeben:
production.server
4
port
5433
credentials
dbuser
securepass123
maxconn
50
timeout
60Ihre Ausgabe sollte sein:
dbuser:securepass123@production.server:5433 (max:50, timeout:60s)Beachten Sie, wie Aufrufer nur die Optionen angeben, die sie benötigen – nicht angegebene Einstellungen verwenden sinnvolle Standardwerte. Das ist die Eleganz des Functional Options Pattern!
Spickzettel
Das Functional Options Pattern bietet eine elegante Möglichkeit, Structs mit vielen optionalen Feldern zu konfigurieren, indem Funktionen anstelle von langen Konstruktorparametern verwendet werden.
Definieren Sie einen Option-Typ als eine Funktion, die einen Pointer auf Ihr Struct modifiziert:
type Server struct {
host string
port int
timeout int
}
type Option func(*Server)Erstellen Sie Option-Funktionen, die Option-Closures zurückgeben:
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout int) Option {
return func(s *Server) {
s.timeout = timeout
}
}Der Konstruktor akzeptiert eine variadische Liste von Optionen, setzt Standardwerte und wendet dann jede Option an:
func NewServer(host string, opts ...Option) *Server {
s := &Server{
host: host,
port: 8080, // Standardwert
timeout: 30, // Standardwert
}
for _, opt := range opts {
opt(s)
}
return s
}Die Verwendung ist sauber und flexibel – Aufrufer geben nur das an, was sie benötigen:
// Standardwerte verwenden
s1 := NewServer("localhost")
// Spezifische Optionen anpassen
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))Vorteile: Standardwerte sind zentralisiert, Aufrufer geben nur das an, was sie benötigen, und das Hinzufügen neuer Optionen führt nicht zu Fehlern in bestehendem Code.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Host lesen
host, _ := reader.ReadString('\n')
host = strings.TrimSpace(host)
// Anzahl der Optionen lesen
numOptionsStr, _ := reader.ReadString('\n')
numOptions, _ := strconv.Atoi(strings.TrimSpace(numOptionsStr))
// Optionen sammeln
var opts []Option
for i := 0; i < numOptions; i++ {
optionType, _ := reader.ReadString('\n')
optionType = strings.TrimSpace(optionType)
switch optionType {
case "port":
portStr, _ := reader.ReadString('\n')
port, _ := strconv.Atoi(strings.TrimSpace(portStr))
// TODO: WithPort-Option an opts anhängen
_ = port // Diese Zeile bei der Implementierung entfernen
case "credentials":
username, _ := reader.ReadString('\n')
username = strings.TrimSpace(username)
password, _ := reader.ReadString('\n')
password = strings.TrimSpace(password)
// TODO: WithCredentials-Option an opts anhängen
_, _ = username, password // Diese Zeile bei der Implementierung entfernen
case "maxconn":
maxStr, _ := reader.ReadString('\n')
maxConn, _ := strconv.Atoi(strings.TrimSpace(maxStr))
// TODO: WithMaxConnections-Option an opts anhängen
_ = maxConn // Diese Zeile bei der Implementierung entfernen
case "timeout":
timeoutStr, _ := reader.ReadString('\n')
timeout, _ := strconv.Atoi(strings.TrimSpace(timeoutStr))
// TODO: WithTimeout-Option an opts anhängen
_ = timeout // Diese Zeile bei der Implementierung entfernen
}
}
// TODO: DBConnection mit NewDBConnection unter Verwendung von host und opts erstellen
// TODO: Den Verbindungsstring mit der Methode ConnectionString() ausgeben
_ = opts // Diese Zeile bei der Implementierung entfernen
fmt.Println("TODO: Print connection string here")
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der Go OOP
Externe DateienGo Workspace & ModulePackages & ImportsExportierte vs. nicht-exportierte NamenEinführung in OOP in GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktor-FunktionenZusammenfassung – Einfacher Taschenrechner4Interfaces
Einführung in InterfacesImplizite ImplementierungInterface als VertragLeeres Interface (any)Type AssertionType SwitchInterface-KompositionStringer & Error InterfacesRückblick - Formen-Rechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf Paket-EbeneGetter- & Setter-MethodenInformation Hiding in GoZusammenfassung - Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterType ConstraintsGenerische StructsWorkaround für generische MethodenZusammenfassung – Generische Collection2Vertiefung: Typen & Structs
Basis- & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct TagsAnonyme StructsVerschachtelte StructsZero Values & StandardwerteZusammenfassung - Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen des Struct EmbeddingMethod PromotionEinbetten mehrerer StructsEmbedding vs. AggregationShadowing eingebetteter MethodenZusammenfassung – Mitarbeiter-Hierarchie8Fehlerbehandlung & OOP
Das error-InterfaceEigene FehlertypenError Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverZusammenfassung – Datei-Parser3Zeiger & Speicher
Zeiger-Grundlagen in GoZeiger auf StructsWertübergabe vs. ReferenzübergabeDie new() FunktionGarbage Collection in GoWiederholung – Linked List Builder6Polymorphismus in Go
Polymorphismus über InterfacesDuck Typing in GoInterface-ErfüllungsregelnPolymorphe CollectionsDependency InjectionZusammenfassung – Payment Processor9Nebenläufigkeit & OOP
Goroutines GrundlagenChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignZusammenfassung - Worker Pool12Fortgeschrittene OOP-Konzepte
Functional Options PatternBuilder-Pattern in GoMethodenverkettungTyp-Aliase vs. DefinitionenReflection-GrundlagenÜberblick zur Code-Generierung