Pull to refresh

Чистая архитектура в Go-приложении. Часть 2

Reading time5 min
Views16K
От переводчика: данная статья написана Manuel Kiessling в сентябре 2012 года, как реализация статьи Дядюшки Боба о чистой архитектуре с учетом Go-специфики.



Это вторая статья цикла об особенности реализации Чистой Архитектуры в Go. [Часть 1]



Сценарии


Сразу начнем с кода слоя Сценария:

// $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
}


Код слоя Сценариев состоит главным образом из сущности User (пользователь) и двух сценариев. Сущность имеет репозиторий точно так же как это было в слое Домена, поскольку Пользователям требуется механизм персистентного сохранения и получения данных.

Сценарии реализованы как методы структуры OrderInteractor, что, впрочем не удивительно. Это не обязательное требование, они могут быть реализованы и как несвязанные функции, но как мы позже увидим — это облегчает введение определенных зависимостей.

Код выше является ярким примером пищи для размышления на тему «что куда поставить». Прежде всего все взаимодействия внешних слоев должны осуществляться через методы OrderInteractor и AdminOrderInteractor, структуры которые оперируют в пределах слоя Сценариев и глубже. Опять же — это все следование Правилу Зависимостей. Такой вариант работы позволяет не иметь внешних зависимостей, что, в свою очередь, позволяет нам, к примеру, протестировать этот код используя моки репозиториев или, при необходимости, можно заменить внутреннюю реализацию Logger (см в код) на другую без каких либо сложностей, поскольку эти изменения не затронут остальные слои.

Дядюшка Боб говорит про Сценарии: «В этом слое реализуется специфика бизнес-правил. Он инкапсулирует и реализует все случаи использования системы. Эти сценарии реализуют поток данных в и из слоя Cущностей для реализации бизнес-правил.»

Если вы посмотрите, скажем, на метод Add в OrderInteractor, вы увидите это в действии. Метод управляет получением необходимых объектов и сохранением их в пригодном для дальнейшего использования виде. В этом методе делается обработка ошибок, которые могут быть специфичны для этого Сценария, с учетом определенных ограничений именно этого слоя. Например, лимит на покупку в 250 долларов накладывается на уровне Домена, поскольку это бизнес-правило и оно приоритетнее правил Сценариев. С другой стороны, проверки, касаемые добавления товаров к заказу — это специфика Сценариев, к тому же именно этот слой содержит сущность User, что влияет в свою очередь на обработку товара в зависимости от того обычный пользователь это делает или администратор.

Давайте так же обсудим логгирование на этом слое. В приложении все виды логгирования затрагивают несколько слоев. Даже с учетом понимания, что все лог-записи будут в конечном итоге строками в файле на диске важно отделить концептуальные детали от технических. Уровень сценариев не знает ничего о текстовых файлах и жестких дисках. Концептуально, этот уровень просто говорит: «На уровне Сценария произошло что-то интересное и я бы хотел это сообщить», где «сообщить» не означает «записать куда-либо», это означает просто «сообщить» — без какого-либо знания, что дальше с этим все произойдет.

Таким образом мы просто обеспечиваем интерфейс, который удовлетворяет потребности Сценария и предоставляет реализацию для этого — таким образом не зависимо от того, как мы в итоге решим сохранять логи (файл, БД, ...) мы по прежнему будем удовлетворять интерфейсу обработки логгирования на данном слое и эти изменения не затронут внутренние слои.

Еще более интересной ситуация видится в свете того, что мы создали два разных OrderInteractor. Если бы мы хотели логгировать действия администратора в один файл, а действия обычного пользователя в другой файл, то это так же было очень просто. В этом случае мы бы просто создали две реализации Logger и обе версии бы удовлетворяли интерфейсу usecases.Logger и использовали бы их в соответствующих OrderInteractor — OrderInteractor и AdminOrderInteractor.

Другая важная деталь в коде Сценария — структура Item. На уровне домена у нас уже есть аналогичная структура, не так ли? Почему бы просто не вернуть ее в методе Items()? Потому что это противоречит правилу — не передавать структуры во внешние слои. Сущности слоя могут содержать в себе не только данные, но и поведение. Таким образом поведение сущностей сценария может быть применено только на этом слое. Не передавая сущности во внешние слои мы гарантируем сохранение поведения в пределах слоя. Внешним слоям нужны только чистые данные и наша задача предоставить их именно в этом виде.

Как и в слое Домена этот код показывает как Чистая архитектура помогает понять как приложение на самом деле работает: если для понимания того какие бизнес-правила у нас есть нам достаточно посмотреть в слой домена, то для того, чтобы понять как пользователь взаимодействует с бизнесом нам достаточно посмотреть в код слоя Сценариев. Мы видим, что приложение позволяет пользователю самостоятельно добавить товары в заказ и что администратор может добавить товары в заказ пользователя.

Продолжение следует… В третьей части обсудим слой Интерфейсов.
Tags:
Hubs:
-6
Comments22

Articles