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

от автора

От переводчика: данная статья написана 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()? Потому что это противоречит правилу — не передавать структуры во внешние слои. Сущности слоя могут содержать в себе не только данные, но и поведение. Таким образом поведение сущностей сценария может быть применено только на этом слое. Не передавая сущности во внешние слои мы гарантируем сохранение поведения в пределах слоя. Внешним слоям нужны только чистые данные и наша задача предоставить их именно в этом виде.

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/270351/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *