Saubere Architektur in der Go-Anwendung. Teil 2

    Von einem Übersetzer: Dieser Artikel wurde von Manuel Kiessling im September 2012 als Implementierung von Onkel Bobs Artikel über saubere Architektur unter Berücksichtigung von Go-Besonderheiten verfasst.



    Dies ist der zweite Artikel in einer Reihe über Funktionen zur Implementierung von Clean Architecture in Go. [ Teil 1 ]



    Szenarien


    Beginnen wir mit dem Code der Skriptebene:

    // $GOPATH/src/usecases/usecases.go
    package usecases
    import (
        "domain"
        "fmt"
    )
    type UserRepository interface {
        Store(user User)
        FindById(id int) User
    }
    type User struct {
        Id       int
        IsAdmin  bool
        Customer domain.Customer
    }
    type Item struct {
        Id    int
        Name  string
        Value float64
    }
    type Logger interface {
        Log(message string) error
    }
    type OrderInteractor struct {
        UserRepository  UserRepository
        OrderRepository domain.OrderRepository
        ItemRepository  domain.ItemRepository
        Logger          Logger
    }
    func (interactor *OrderInteractor) Items(userId, orderId int) ([]Item, error) {
        var items []Item
        user := interactor.UserRepository.FindById(userId)
        order := interactor.OrderRepository.FindById(orderId)
        if user.Customer.Id != order.Customer.Id {
            message := "User #%i (customer #%i) "
            message += "is not allowed to see items "
            message += "in order #%i (of customer #%i)"
            err := fmt.Errorf(message,
                user.Id,
                user.Customer.Id,
                order.Id,
                order.Customer.Id)
            interactor.Logger.Log(err.Error())
            items = make([]Item, 0)
            return items, err
        }
        items = make([]Item, len(order.Items))
        for i, item := range order.Items {
            items[i] = Item{item.Id, item.Name, item.Value}
        }
        return items, nil
    }
    func (interactor *OrderInteractor) Add(userId, orderId, itemId int) error {
        var message string
        user := interactor.UserRepository.FindById(userId)
        order := interactor.OrderRepository.FindById(orderId)
        if user.Customer.Id != order.Customer.Id {
            message = "User #%i (customer #%i) "
            message += "is not allowed to add items "
            message += "to order #%i (of customer #%i)"
            err := fmt.Errorf(message,
                user.Id,
                user.Customer.Id,
                order.Id,
                order.Customer.Id)
            interactor.Logger.Log(err.Error())
            return err
        }
        item := interactor.ItemRepository.FindById(itemId)
        if domainErr := order.Add(item); domainErr != nil {
            message = "Could not add item #%i "
            message += "to order #%i (of customer #%i) "
            message += "as user #%i because a business "
            message += "rule was violated: '%s'"
            err := fmt.Errorf(message,
                item.Id,
                order.Id,
                order.Customer.Id,
                user.Id,
                domainErr.Error())
            interactor.Logger.Log(err.Error())
            return err
        }
        interactor.OrderRepository.Store(order)
        interactor.Logger.Log(fmt.Sprintf(
            "User added item '%s' (#%i) to order #%i",
            item.Name, item.Id, order.Id))
        return nil
    }
    type AdminOrderInteractor struct {
        OrderInteractor
    }
    func (interactor *AdminOrderInteractor) Add(userId, orderId, itemId int) error {
        var message string
        user := interactor.UserRepository.FindById(userId)
        order := interactor.OrderRepository.FindById(orderId)
        if !user.IsAdmin {
            message = "User #%i (customer #%i) "
            message += "is not allowed to add items "
            message += "to order #%i (of customer #%i), "
            message += "because he is not an administrator"
            err := fmt.Errorf(message,
                user.Id,
                user.Customer.Id,
                order.Id,
                order.Customer.Id)
            interactor.Logger.Log(err.Error())
            return err
        }
        item := interactor.ItemRepository.FindById(itemId)
        if domainErr := order.Add(item); domainErr != nil {
            message = "Could not add item #%i "
            message += "to order #%i (of customer #%i) "
            message += "as user #%i because a business "
            message += "rule was violated: '%s'"
            err := fmt.Errorf(message,
                item.Id,
                order.Id,
                order.Customer.Id,
                user.Id,
                domainErr.Error())
            interactor.Logger.Log(err.Error())
            return err
        }
        interactor.OrderRepository.Store(order)
        interactor.Logger.Log(fmt.Sprintf(
            "Admin added item '%s' (#%i) to order #%i",
            item.Name, item.Id, order.Id))
        return nil
    }
    


    Der Code für die Scripting-Ebene besteht hauptsächlich aus der User-Entität und zwei Skripten. Eine Entität verfügt über ein Repository wie in der Domänenschicht, da Benutzer einen beständigen Mechanismus zum Speichern und Empfangen von Daten benötigen.

    Die Skripte sind als Methoden der OrderInteractor-Struktur implementiert, was jedoch nicht verwunderlich ist. Dies ist keine Voraussetzung, sie können auch als unabhängige Funktionen implementiert werden, aber wie wir später sehen werden, erleichtert dies die Einführung bestimmter Abhängigkeiten.

    Der obige Code ist ein Paradebeispiel für Denkanstöße zum Thema „Was ist zu setzen?“. Zuallererst sollten alle Interaktionen der äußeren Schichten über die OrderInteractor- und AdminOrderInteractor-Methoden ausgeführt werden, Strukturen, die innerhalb der Scripting-Schicht und tiefer arbeiten. Wiederum - all dies folgt der Regel der Abhängigkeiten. Diese Arbeitsoption ermöglicht es uns, keine externen Abhängigkeiten zu haben, was es uns wiederum ermöglicht, beispielsweise diesen Code mit dem Repository moki zu testen, oder, falls erforderlich, die interne Implementierung von Logger (siehe den Code) ohne Schwierigkeiten durch einen anderen zu ersetzen Diese Änderungen wirken sich nicht auf den Rest der Ebenen aus.

    Onkel Bob sagt über die Szenarien: „Die Besonderheiten der Geschäftsregeln sind in dieser Schicht implementiert. Es kapselt und implementiert alle Verwendungen des Systems. "Diese Szenarien implementieren den Datenfluss zur und von der Entitätsschicht, um Geschäftsregeln zu implementieren."

    Wenn Sie sich beispielsweise die Add-Methode in OrderInteractor ansehen, sehen Sie dies in Aktion. Das Verfahren steuert den Empfang der erforderlichen Objekte und deren Speicherung in einer für die weitere Verwendung geeigneten Form. Diese Methode behandelt Fehler, die für dieses Szenario spezifisch sein können, und berücksichtigt dabei bestimmte Einschränkungen dieses bestimmten Layers. Beispielsweise wird auf Domain-Ebene ein Kauflimit von 250 US-Dollar festgelegt, da dies eine Geschäftsregel ist, die über den Scripting-Regeln liegt. Andererseits sind die Prüfungen zum Hinzufügen von Waren zum Auftrag szenariospezifisch. Darüber hinaus enthält diese Ebene die Benutzerentität, die sich wiederum auf die Verarbeitung von Waren auswirkt, je nachdem, ob der normale Benutzer oder der Administrator dies tut.

    Lassen Sie uns auch die Protokollierung auf dieser Ebene diskutieren. In der Anwendung wirken sich alle Arten der Protokollierung auf mehrere Ebenen aus. Selbst wenn man sich darüber im Klaren ist, dass alle Protokolleinträge letztendlich Zeilen in einer Datei auf der Festplatte sind, ist es wichtig, die konzeptionellen Details von den technischen zu trennen. Die Skriptebene weiß nichts über Textdateien und Festplatten. Konzeptionell sagt diese Ebene einfach: „Auf der Ebene des Szenarios ist etwas Interessantes passiert, und ich möchte Ihnen davon erzählen.“ Dabei bedeutet „erzählen“ nicht „irgendwo schreiben“, sondern nur „erzählen“ - ohne Wissen. Was kommt als nächstes, das wird alles passieren.

    Somit stellen wir einfach eine Schnittstelle zur Verfügung, die den Anforderungen des Skripts entspricht, und stellen eine Implementierung dafür bereit. Unabhängig davon, wie wir die Protokolle (Datei, Datenbank, ...) speichern, werden wir die Protokollierungsverarbeitungsschnittstelle weiterhin damit zufriedenstellen Ebene und diese Änderungen wirken sich nicht auf die inneren Ebenen aus.

    Die Situation ist umso interessanter, als wir zwei verschiedene OrderInteractor erstellt haben. Wenn wir die Aktionen des Administrators in einer Datei und die Aktionen eines normalen Benutzers in einer anderen Datei protokollieren wollten, war dies ebenfalls sehr einfach. In diesem Fall würden wir einfach zwei Logger-Implementierungen erstellen und beide Versionen würden die usecases.Logger-Schnittstelle erfüllen und sie in den entsprechenden OrderInteractor - OrderInteractor und AdminOrderInteractor verwenden.

    Ein weiteres wichtiges Detail im Skriptcode ist die Item-Struktur. Auf Domain-Ebene haben wir bereits eine ähnliche Struktur, oder? Warum nicht einfach in der Methode Items () zurückgeben? Da dies gegen die Regel verstößt, übertragen Sie keine Strukturen auf die äußeren Schichten. Entitäten eines Layers können nicht nur Daten, sondern auch Verhalten enthalten. Daher kann das Verhalten von Skriptentitäten nur auf diese Ebene angewendet werden. Ohne die Entität an die äußeren Schichten zu übergeben, garantieren wir, dass das Verhalten innerhalb der Schicht bleibt. Externe Schichten benötigen nur saubere Daten und unsere Aufgabe ist es, diese in dieser Form bereitzustellen.

    Wie in der Domänenebene zeigt dieser Code, wie die Bereinigungsarchitektur das Verständnis der tatsächlichen Funktionsweise der Anwendung erleichtert: Wenn wir uns die Domänenebene ansehen müssen, um zu verstehen, welche Geschäftsregeln wir haben, müssen wir verstehen, wie der Benutzer mit dem Unternehmen interagiert Schauen Sie sich einfach den Code der Skriptebene an. Wir sehen, dass die Anwendung es dem Benutzer ermöglicht, der Bestellung selbst Produkte hinzuzufügen, und dass der Administrator der Bestellung des Benutzers Waren hinzufügen kann.

    Fortsetzung folgt ... Im dritten Teil werden wir die Ebene Interfaces diskutieren.

    Jetzt auch beliebt: