Clean architecture in the Go application. Part 3

    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 third article in a series about the implementation features of Clean Architecture in Go. [ Part 1 ] [ Part 2 ]

    At the moment, everything that should have been said about business and Scenarios is said. Let's look at the interface layer now. While the code of the inner layers is logically located together, the code of the interfaces consists of several parts that exist separately, so we will split the code into several files. Let's start with the web service:

    // $GOPATH/src/interfaces/webservice.go
    package interfaces
    import (
        "fmt"
        "io"
        "net/http"
        "strconv"
        "usecases"
    )
    type OrderInteractor interface {
        Items(userId, orderId int) ([]usecases.Item, error)
        Add(userId, orderId, itemId int) error
    }
    type WebserviceHandler struct {
        OrderInteractor OrderInteractor
    }
    func (handler WebserviceHandler) ShowOrder(res http.ResponseWriter, req *http.Request) {
        userId, _ := strconv.Atoi(req.FormValue("userId"))
        orderId, _ := strconv.Atoi(req.FormValue("orderId"))
        items, _ := handler.OrderInteractor.Items(userId, orderId)
        for _, item := range items {
            io.WriteString(res, fmt.Sprintf("item id: %d\n", item.Id))
            io.WriteString(res, fmt.Sprintf("item name: %v\n", item.Name))
            io.WriteString(res, fmt.Sprintf("item value: %f\n", item.Value))
        }
    }
    


    We are not going to implement all web services here, because they all look more or less the same. In a real application, for example, we would also implement adding goods to the order, administrator access to the order, etc.

    Most significant in this code is that this code actually does nothing particularly. Interfaces, if done correctly, are quite simple, simply because their main task is simply to deliver data between layers. This is just visible in the code above. It simply essentially hides the HTTP call from the Scripts layer and passes the data received from the request into it.

    It should be noted once again that code injections are used here to handle dependencies. Order processing in production would be through real usecases.OrderInteractor, but in case of testing this object is easily wetted, which allows you to test the web service in isolation, which primarily means that unit tests will test exactly the behavior of the web service handlers.

    Also, I emphasize once again that in this code there are not many things that should be in the production code, for example, authorization, checking input parameters, sessions, cookies, etc. All of this is intentionally skipped to simplify the code.

    Nevertheless, it is worth saying a few words about sessions and cookies. First of all, it should be noted that sessions and cookies are entities of different conceptual levels. Cookies are a low-level mechanism that essentially works with HTTP headers. While sessions are in some ways an abstraction, which allows working within the framework of different requests in the context of a single user, which is implemented, for example, through cookies.

    Users, on the other hand, are an abstraction of an even higher level: “a person who interacts with the application” and this is done including through sessions. And finally, there is a client, an entity that works in terms of business, through a user who ... well, you understand the idea.

    I recommend doing this separation by level of abstraction explicitly and immediately, thereby avoiding future problems. An example of such a situation is the need to transfer the session mechanism from using cookies to client SSL certificates. With the correct abstraction, you will need to add only the library for working with certificates on the infrastructure layer and the interface code for working with them in the Interfaces layer. And these changes will not affect users nor customers.

    Also on the interface layer is code that creates HTML responses based on the data that comes from the Scripts layer. In a real application, most likely this will be done using some kind of template engine located in the Infrastructure layer.

    Let's move on to the last block - storage. We already have a working code for the Domain layer, we have implemented the Scripting layer responsible for data delivery, and we have implemented an interface that allows users to access our application via the web. Now we need to implement saving data to disk.

    This is done by implementing abstract repositories, whose interfaces we saw on the Domain and Scripting layer. This is done on the Interfaces layer because it is the interface between the database (low-level storage implementation) and high-level business entities.

    Some repository implementations can be isolated depending on the layer of Interfaces and below, for example, when implementing caching of memory objects or when implementing mocks for unit testing. However, most repository implementations must interact with external persistent storage (DB) mechanisms, most likely through some libraries, and here we must make sure once again that we do not violate the Dependency Rule, since the libraries should be located in the Infrastructure layer.

    This does not mean that the Repository is isolated from the database! The repository perfectly represents what it passes to the database, but does it in a certain high-level representation. Get the data from this table, put the data in that table. Low-level operations or “physical” ones, such as establishing a connection to a database, accepting a slave for reading or a master for writing, processing timeouts, and the like, are infrastructure issues.

    In other words, our Warehouse needs to use some kind of high-level interface that would hide all these low-level things.

    Let's create an interface like this:
    type DbHandler interface {
        Execute(statement string)
        Query(statement string) Row 
    }
    type Row interface {
        Scan(dest ...interface{})
        Next() bool
    }
    


    This is of course a very limited interface, but it allows you to perform all the necessary operations: read, insert, update and delete records in the database.

    In the Infrastructure layer, we implement some kind of binding code that allows you to work with the database through the library for sqlite3 and implements the operation of this interface. but first, let's finish the implementation of the Repository:

    // $GOPATH/src/interfaces/repositories.go
    package interfaces
    import (
        "domain"
        "fmt"
        "usecases"
    )
    type DbHandler interface {
        Execute(statement string)
        Query(statement string) Row
    }
    type Row interface {
        Scan(dest ...interface{})
        Next() bool
    }
    type DbRepo struct {
        dbHandlers map[string]DbHandler
        dbHandler  DbHandler
    }
    type DbUserRepo DbRepo
    type DbCustomerRepo DbRepo
    type DbOrderRepo DbRepo
    type DbItemRepo DbRepo
    func NewDbUserRepo(dbHandlers map[string]DbHandler) *DbUserRepo {
        dbUserRepo := new(DbUserRepo)
        dbUserRepo.dbHandlers = dbHandlers
        dbUserRepo.dbHandler = dbHandlers["DbUserRepo"]
        return dbUserRepo
    }
    func (repo *DbUserRepo) Store(user usecases.User) {
        isAdmin := "no"
        if user.IsAdmin {
            isAdmin = "yes"
        }
        repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO users (id, customer_id, is_admin)
                                            VALUES ('%d', '%d', '%v')`,
                                            user.Id, user.Customer.Id, isAdmin))
        customerRepo := NewDbCustomerRepo(repo.dbHandlers)
        customerRepo.Store(user.Customer)
    }
    func (repo *DbUserRepo) FindById(id int) usecases.User {
        row := repo.dbHandler.Query(fmt.Sprintf(`SELECT is_admin, customer_id
                                                 FROM users WHERE id = '%d' LIMIT 1`,
                                                 id))
        var isAdmin string
        var customerId int
        row.Next()
        row.Scan(&isAdmin, &customerId)
        customerRepo := NewDbCustomerRepo(repo.dbHandlers)
        u := usecases.User{Id: id, Customer: customerRepo.FindById(customerId)}
        u.IsAdmin = false
        if isAdmin == "yes" {
            u.IsAdmin = true
        }
        return u
    }
    func NewDbCustomerRepo(dbHandlers map[string]DbHandler) *DbCustomerRepo {
        dbCustomerRepo := new(DbCustomerRepo)
        dbCustomerRepo.dbHandlers = dbHandlers
        dbCustomerRepo.dbHandler = dbHandlers["DbCustomerRepo"]
        return dbCustomerRepo
    }
    func (repo *DbCustomerRepo) Store(customer domain.Customer) {
        repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO customers (id, name)
                                            VALUES ('%d', '%v')`,
                                            customer.Id, customer.Name))
    }
    func (repo *DbCustomerRepo) FindById(id int) domain.Customer {
        row := repo.dbHandler.Query(fmt.Sprintf(`SELECT name FROM customers
                                                 WHERE id = '%d' LIMIT 1`,
                                                 id))
        var name string
        row.Next()
        row.Scan(&name)
        return domain.Customer{Id: id, Name: name}
    }
    func NewDbOrderRepo(dbHandlers map[string]DbHandler) *DbOrderRepo {
        dbOrderRepo := new(DbOrderRepo)
        dbOrderRepo.dbHandlers = dbHandlers
        dbOrderRepo.dbHandler = dbHandlers["DbOrderRepo"]
        return dbOrderRepo
    }
    func (repo *DbOrderRepo) Store(order domain.Order) {
        repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO orders (id, customer_id)
                                            VALUES ('%d', '%v')`,
                                            order.Id, order.Customer.Id))
        for _, item := range order.Items {
            repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO items2orders (item_id, order_id)
                                                VALUES ('%d', '%d')`,
                                                item.Id, order.Id))
        }
    }
    func (repo *DbOrderRepo) FindById(id int) domain.Order {
        row := repo.dbHandler.Query(fmt.Sprintf(`SELECT customer_id FROM orders
                                                 WHERE id = '%d' LIMIT 1`,
                                                 id))
        var customerId int
        row.Next()
        row.Scan(&customerId)
        customerRepo := NewDbCustomerRepo(repo.dbHandlers)
        order := domain.Order{Id: id, Customer: customerRepo.FindById(customerId)}
        var itemId int
        itemRepo := NewDbItemRepo(repo.dbHandlers)
        row = repo.dbHandler.Query(fmt.Sprintf(`SELECT item_id FROM items2orders
                                                WHERE order_id = '%d'`,
                                                order.Id))
        for row.Next() {
            row.Scan(&itemId)
            order.Add(itemRepo.FindById(itemId))
        }
        return order
    }
    func NewDbItemRepo(dbHandlers map[string]DbHandler) *DbItemRepo {
        dbItemRepo := new(DbItemRepo)
        dbItemRepo.dbHandlers = dbHandlers
        dbItemRepo.dbHandler = dbHandlers["DbItemRepo"]
        return dbItemRepo
    }
    func (repo *DbItemRepo) Store(item domain.Item) {
        available := "no"
        if item.Available {
            available = "yes"
        }
        repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO items (id, name, value, available)
                                            VALUES ('%d', '%v', '%f', '%v')`,
                                            item.Id, item.Name, item.Value, available))
    }
    func (repo *DbItemRepo) FindById(id int) domain.Item {
        row := repo.dbHandler.Query(fmt.Sprintf(`SELECT name, value, available
                                                 FROM items WHERE id = '%d' LIMIT 1`,
                                                 id))
        var name string
        var value float64
        var available string
        row.Next()
        row.Scan(&name, &value, &available)
        item := domain.Item{Id: id, Name: name, Value: value}
        item.Available = false
        if available == "yes" {
            item.Available = true
        }
        return item
    }
    


    I already hear from you: this is a terrible code! :) A lot of duplication, no error handling, and a few other bad smelling things. But the point of this article is neither to explain the style of the code, nor to implement design patterns - it's all about the ARCHITECTURE of the application, so the code is written so that it is easier to explain and easier to read this article using its example. This code is very simplified - its main and only task: to be simple and understandable.

    Pay attention to the dbHandlers map [string] DbHandler in each repository - here each repository can use a different repository without using Dependency Injection - if any of the repositories use some other implementation of dbHandlers, then the rest of the repositories should not think about who uses what . This is a poor DI implementation.

    Let's look at one of the most interesting methods - DbUserRepo.FindById (). This is a good example to show that in our architecture, Interfaces are all about transforming data from one layer to another. FindById reads records from the database and creates objects from them for the Scenario and Domain level. I deliberately made the presentation of the User.IsAdmin attribute more difficult than necessary, storing it in the database as a field of the varchar type with values ​​“yes” and “no”. At the Scenario level, this is of course represented as a Boolean value. In this gap of representations between layers, we will illustrate overcoming the boundaries between data layers with different representations.

    The User entity has the Customer attribute - this is essentially a reference to the Domain. The User repository simply uses the Customer repository to get the necessary data.

    It's easy to imagine how a similar architecture can help us when our application grows. Following the Rule of Dependencies, we will be able to process the implementation of data storage without having to process entities and layers. For example, we could decide that the object data in the database can be stored in several tables, but the separation of the data for saving and the assembly for transferring objects to the application will be hidden in the repository and will not affect the other layers.

    Also popular now: