Entwickler, der außergewöhnliche CRM- und Laravel-Lösungen liefert

Als erfahrener Entwickler spezialisiere ich mich auf Laravel- und Vue.js-Entwicklung, die Implementierung von Vtiger CRM sowie auf vielfältige WordPress-Projekte. Meine Arbeit zeichnet sich durch kreative, dynamische und benutzerzentrierte Weblösungen aus, die individuell an die Bedürfnisse meiner Kunden angepasst werden.

In einem vorherigen Artikel haben wir das Builder-Pattern diskutiert, das die Konfiguration komplexer Objekte vereinfacht. Doch ein Nachteil blieb: Die Validierung von Parametern war erst im Build()-Schritt möglich, was Methodenketten unterbrach. Das Functional Options Pattern löst dieses Problem elegant und ist zudem ein idiomatischer Ansatz in Go.

Golang: Flexible API-Konfiguration mit dem Functional Options Pattern

In einem vorherigen Artikel haben wir das Builder-Pattern diskutiert, das die Konfiguration komplexer Objekte vereinfacht. Doch ein Nachteil blieb: Die Validierung von Parametern war erst im Build()-Schritt möglich, was Methodenketten unterbrach. Das Functional Options Pattern löst dieses Problem elegant und ist zudem ein idiomatischer Ansatz in Go.

Das Problem: Validierung in Methodenketten

Stellen Sie sich ein CRM-System vor, bei dem Kontakte mit optionalen Feldern wie Telefonnummer, E-Mail oder Firmendaten erstellt werden. Jedes Feld hat spezifische Validierungsregeln:

  • Telefonnummern müssen ≥10 Zeichen lang sein.
  • E-Mails müssen ein valides Format haben.
  • Firmendaten benötigen Name und Position.

Ein Builder-Ansatz würde Validierungsfehler erst bei Build() werfen – zu spät für eine flüssige Methodenkette.


Lösung: Functional Options Pattern

Das Pattern nutzt Closures und variadische Funktionen, um Optionen flexibel und mit sofortiger Validierung zu handhaben.

Schritt 1: Strukturen definieren

type Contact struct {
    ID       string
    Name     string
    Phone    string
    Email    string
    Company  *CompanyInfo
    Address  string
    Notes    string
}

type CompanyInfo struct {
    Name     string
    Position string
}

type ContactOptions struct {
    Phone    string
    Email    string
    Company  *CompanyInfo
    Address  string
    Notes    string
}

type ContactOption func(*ContactOptions) error

Schritt 2: Optionen als Funktionen

Jede Option ist eine Funktion, die ContactOptions aktualisiert und sofort validiert:

func WithPhone(phone string) ContactOption {
    return func(opts *ContactOptions) error {
        if len(phone) < 10 {
            return errors.New("Telefonnummer muss ≥10 Zeichen haben")
        }
        opts.Phone = phone
        return nil
    }
}

func WithEmail(email string) ContactOption {
    return func(opts *ContactOptions) error {
        _, err := mail.ParseAddress(email)
        if err != nil {
            return errors.New("Ungültige E-Mail-Adresse")
        }
        opts.Email = email
        return nil
    }
}

func WithCompany(name, position string) ContactOption {
    return func(opts *ContactOptions) error {
        if strings.TrimSpace(name) == "" || strings.TrimSpace(position) == "" {
            return errors.New("Firmenname und Position sind erforderlich")
        }
        opts.Company = &CompanyInfo{Name: name, Position: position}
        return nil
    }
}

Schritt 3: Objekterstellung mit Optionen

func CreateContact(id, name string, opts ...ContactOption) (*Contact, error) {
    options := ContactOptions{}

    for _, opt := range opts {
        if err := opt(&options); err != nil {
            return nil, err // Frühe Validierung!
        }
    }

    return &Contact{
        ID:       id,
        Name:     name,
        Phone:    options.Phone,
        Email:    options.Email,
        Company:  options.Company,
        Address:  options.Address,
        Notes:    options.Notes,
    }, nil
}

Anwendung des Patterns

func main() {
    contact, err := CreateContact(
        "1", 
        "John Doe",
        WithPhone("1234567890"),
        WithEmail("john.doe@gmail.com"),
        WithCompany("ABC Corp", "Manager"),
    )

    if err != nil {
        fmt.Println("Fehler:", err)
        return
    }
    fmt.Printf("Kontakt angelegt: %+v\n", contact)
}

Ausgabe:

Kontakt angelegt: &{ID:1 Name:John Doe Phone:1234567890 Email:john.doe@gmail.com Company:0xc0000ae040 Address: Notes:}

Vorteile gegenüber dem Builder-Pattern

  1. Sofortige Validierung: Fehler werden direkt bei der Option erkannt.
  2. Flexibilität: Optionen können dynamisch kombiniert werden.
  3. Keine Boilerplate-Strukturen: Kein separater Builder-Typ erforderlich.
  4. Idiomatisches Go: Weit verbreitet in Bibliotheken wie gRPC oder AWS SDK.

Nachteile

  • Boilerplate-Code: Jede Option benötigt eine eigene Funktion.
    Lösung: Codegenerierung mit go generate.
  • Komplexität bei vielen Optionen: Ab ~10 Optionen wird die Lesbarkeit beeinträchtigt.

Wann verwenden?

Das Pattern eignet sich ideal für:

  • Konfigurationen mit vielen optionalen Parametern.
  • APIs, die Validierung erfordern.
  • Bibliotheken, die erweiterbar sein sollen.

Fazit

Das Functional Options Pattern ist ein leistungsstarker Ansatz, um flexible und validierbare APIs in Go zu gestalten. Im Vergleich zum Builder-Pattern ermöglicht es frühe Fehlererkennung und bleibt dabei idiomatisch.

Weiterführende Optimierungen:

  • Default-Werte: Integrieren Sie Standardwerte in CreateContact.
  • Optionale Pflichtfelder: Nutzen Sie RequiredPhone()-Optionen für Context-spezifische Regeln.

Mit diesem Pattern bleiben Ihre APIs wartbar, erweiterbar und developer-friendly.