От переводчика: данная статья написана 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/
Добавить комментарий