Clean architecture in the Go application. Part 2
From a translator: this article was written by Manuel Kiessling in September 2012, as an implementation of Uncle Bob 's article on clean architecture, taking into account Go-specifics.
This is the second article in a series about features of implementing Clean Architecture in Go. [ Part 1 ]
Let's start with the script layer code:
The code for the Scripting layer consists mainly of the User entity and two scripts. An entity has a repository just like it was in the Domain layer, because Users need a persistent mechanism for saving and receiving data.
The scripts are implemented as methods of the OrderInteractor structure, which, however, is not surprising. This is not a mandatory requirement, they can 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 on the topic of “what to put.” First of all, all interactions of the outer layers should be carried out through the OrderInteractor and AdminOrderInteractor methods, structures that operate within the Scripting layer and deeper. Again - this is all following the Rule of Dependencies. This work option allows us not to have external dependencies, which, in turn, allows us, for example, to test this code using the repository moki or, if necessary, we can replace the internal implementation of Logger (see the code) with another without any difficulties, since these changes will not affect the rest of the layers.
Uncle Bob says about the Scenarios: “The specifics of business rules are implemented in this layer. It encapsulates and implements all uses of the system. "These scenarios implement data flow to and from the Entity layer to implement business rules."
If you look at, say, the Add method in OrderInteractor, you will see this in action. The method manages to obtain the necessary objects and save them in a form suitable for further use. This method handles errors that may be specific to this Scenario, taking into account certain limitations of this particular layer. For example, a purchase limit of $ 250 is imposed at the Domain level, as this is a business rule and it is higher than the Scripting rules. On the other hand, the checks regarding adding goods to the order are specifics of the Scenarios, moreover, it is this layer that contains the User entity, which in turn affects the processing of goods, depending on whether the ordinary user or the administrator does it.
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 file on disk, it is important to separate the conceptual details from the technical ones. The scripting layer does not know anything about text files and hard drives. Conceptually, this level simply says: “Something interesting happened at the Scenario level and I would like to tell you about it”, where “tell” does not mean “write somewhere”, it means just “tell” - without any knowledge, what's next with this, everything will happen.
Thus, we simply provide an interface that satisfies the needs of the Script and provides an implementation for this - thus, no matter how we decide to save the logs (file, database, ...), we will continue to 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 the actions of the administrator into one file, and the actions of an ordinary user into another file, then it was also very simple. In this case, we would simply create two Logger implementations and both versions would satisfy the usecases.Logger interface and use them in the corresponding 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 - do not transfer structures to the outer layers. Entities of a layer can contain not only data, but also behavior. Therefore, the behavior of script entities can only be applied on this layer. Without passing the entity to the outer layers, we guarantee that the behavior remains within the layer. External layers need only clean data and our task is to provide them in this form.
As in the Domain layer, this code shows how Pure architecture helps to understand how the application actually works: if to understand what business rules we have, we just need to look into the domain layer, then in order to understand how the user interacts with the business, just look into the scripting layer code. We see that the application allows the user to independently add products to the order and that the administrator can add goods to the user’s order.
To be continued ... In the third part we will discuss the Interfaces layer.
This is the second article in a series about features of implementing Clean Architecture in Go. [ Part 1 ]
Scenarios
Let's 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 code for the Scripting layer consists mainly of the User entity and two scripts. An entity has a repository just like it was in the Domain layer, because Users need a persistent mechanism for saving and receiving data.
The scripts are implemented as methods of the OrderInteractor structure, which, however, is not surprising. This is not a mandatory requirement, they can 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 on the topic of “what to put.” First of all, all interactions of the outer layers should be carried out through the OrderInteractor and AdminOrderInteractor methods, structures that operate within the Scripting layer and deeper. Again - this is all following the Rule of Dependencies. This work option allows us not to have external dependencies, which, in turn, allows us, for example, to test this code using the repository moki or, if necessary, we can replace the internal implementation of Logger (see the code) with another without any difficulties, since these changes will not affect the rest of the layers.
Uncle Bob says about the Scenarios: “The specifics of business rules are implemented in this layer. It encapsulates and implements all uses of the system. "These scenarios implement data flow to and from the Entity layer to implement business rules."
If you look at, say, the Add method in OrderInteractor, you will see this in action. The method manages to obtain the necessary objects and save them in a form suitable for further use. This method handles errors that may be specific to this Scenario, taking into account certain limitations of this particular layer. For example, a purchase limit of $ 250 is imposed at the Domain level, as this is a business rule and it is higher than the Scripting rules. On the other hand, the checks regarding adding goods to the order are specifics of the Scenarios, moreover, it is this layer that contains the User entity, which in turn affects the processing of goods, depending on whether the ordinary user or the administrator does it.
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 file on disk, it is important to separate the conceptual details from the technical ones. The scripting layer does not know anything about text files and hard drives. Conceptually, this level simply says: “Something interesting happened at the Scenario level and I would like to tell you about it”, where “tell” does not mean “write somewhere”, it means just “tell” - without any knowledge, what's next with this, everything will happen.
Thus, we simply provide an interface that satisfies the needs of the Script and provides an implementation for this - thus, no matter how we decide to save the logs (file, database, ...), we will continue to 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 the actions of the administrator into one file, and the actions of an ordinary user into another file, then it was also very simple. In this case, we would simply create two Logger implementations and both versions would satisfy the usecases.Logger interface and use them in the corresponding 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 - do not transfer structures to the outer layers. Entities of a layer can contain not only data, but also behavior. Therefore, the behavior of script entities can only be applied on this layer. Without passing the entity to the outer layers, we guarantee that the behavior remains within the layer. External layers need only clean data and our task is to provide them in this form.
As in the Domain layer, this code shows how Pure architecture helps to understand how the application actually works: if to understand what business rules we have, we just need to look into the domain layer, then in order to understand how the user interacts with the business, just look into the scripting layer code. We see that the application allows the user to independently add products to the order and that the administrator can add goods to the user’s order.
To be continued ... In the third part we will discuss the Interfaces layer.