📜 ⬆️ ⬇️

Pure architecture in a go app. Part 2

From the translator: this article was written by Manuel Kiessling in September 2012, as the implementation of Uncle Bob ’s article on pure architecture with consideration for Go-specificity.



This is the second article in a series on the implementation of Pure Architecture in Go. [ Part 1 ]
')


Scenarios


Immediately start with the Script layer code:

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


The Script layer code consists mainly of a User entity and two scripts. An entity has a repository in the same way as it was in the Domain layer, because Users require a persistent storage and retrieval mechanism.

Scripts are implemented as methods of the OrderInteractor structure, which, however, is not surprising. This is not a mandatory requirement; they can also be implemented as unrelated functions, but as we will see later, this facilitates the introduction of certain dependencies.

The code above is a prime example of food for thought about what to put. First of all, all interactions of the external layers should be carried out through the OrderInteractor and AdminOrderInteractor methods, the structures that operate within the Scripting layer and deeper. Again, this is all following the Rule of Dependencies. This way of working allows us not to have external dependencies, which, in turn, allows us, for example, to test this code using mocks of repositories or, if necessary, we can replace the internal implementation of the Logger (see code) with another without any difficulties, because these changes do not affect the remaining layers.

Uncle Bob says about Scenarios: “In this layer, the specificity of business rules is implemented. It encapsulates and implements all uses of the system. These scripts implement the flow of data to and from the Entity layer to implement business rules. ”

If you look at, say, the Add method in the OrderInteractor, you will see this in action. The method controls the receipt of necessary objects and saving them in a form suitable for further use. This method is used to handle errors that may be specific to this Scenario, subject to certain limitations of this particular layer. For example, a limit on the purchase of $ 250 is imposed at the Domain level, since this is a business rule and it takes precedence over the Script rules. On the other hand, the checks related to the addition of goods to the order are the specifics of the Scenarios, and it is this layer that contains the User entity, which in turn affects the processing of goods, depending on whether a regular user or an administrator does this.

Let's also discuss logging on this layer. In the application, all types of logging affect several layers. Even with the understanding that all log entries will ultimately be lines in a disk file, it is important to separate conceptual details from technical ones. The script level does not know anything about text files and hard drives. Conceptually, this level simply says: “At the Scenario level, something interesting happened and I would like to report it”, where “report” does not mean “write somewhere”, it simply means “inform” - without any knowledge, what's next with this all happens.

Thus, we simply provide an interface that satisfies the needs of the Scenario and provides an implementation for this — in this way, regardless of how we decide to save the logs (file, database, ...), we will still satisfy the logging processing interface on this layer and these changes will not affect the inner layers.

The situation is even more interesting in the light of the fact that we created two different OrderInteractor. If we wanted to log administrator actions into one file, and ordinary user actions into another file, then it was also very simple. In this case, we would simply create two implementations of the Logger and both versions would satisfy the usecases.Logger interface and use them in the appropriate OrderInteractor - OrderInteractor and AdminOrderInteractor.

Another important detail in the Script code is the Item structure. At the domain level, we already have a similar structure, right? Why not just return it in the Items () method? Because it contradicts the rule - not to transfer structures to the outer layers. Entities of a layer can contain not only data, but also behavior. Thus, the behavior of script entities can only be applied on this layer. Without transferring the essence to the outer layers, we guarantee the preservation of the behavior within the layer. The outer layers need only pure data and our task is to provide them in this form.

As in the Domain layer, this code shows how the Pure Architecture helps to understand how the application actually works: if we only have to look at the domain layer in order to understand what business rules, then in order to understand how the user interacts with the business, we just look at the Script layer code. We see that the application allows the user to add products to the order on their own and that the administrator can add products to the user's order.

To be continued ... In the third part we will discuss the Interfaces layer.

Source: https://habr.com/ru/post/270351/


All Articles