Menu
Coddy logo textTech

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.

challenge icon

Aufgabe

Einfach

Lassen 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 *DBConnection modifiziert.

    Erstellen Sie diese Optionsfunktionen, die eine Option zurü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 *DBConnection zurückgibt. Legen Sie diese Standardwerte fest: port 5432, username "admin", password "secret", maxConnections 10, timeout 30. 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:

    • port gefolgt von der Portnummer
    • credentials gefolgt von Benutzername und Passwort (zwei Zeilen)
    • maxconn gefolgt von der maximalen Anzahl an Verbindungen
    • timeout gefolgt vom Timeout in Sekunden

    Erstellen Sie eine DBConnection mit 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
0

Ihre Ausgabe sollte sein:

admin:secret@localhost:5432 (max:10, timeout:30s)

Und gegeben:

db.example.com
2
port
3306
credentials
root
mypassword

Ihre 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
60

Ihre 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")
}
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung