#2 Lập trình Golang ăn xổi: Clean architecture

Bài viết được sự cho phép của tác giả Lê Xuân Quỳnh

Trong bài #1, tôi đã nói qua loa về mô hình này. Tuy nhiên, sau một khoảng thời gian đọc hiểu các bài viết từ các site nước ngoài, tôi nhận ra là Clean architecture trong Golang nó khác nhiều so với lập trình iOS mà tôi đã triển khai. Lý do viết bài này hơi chậm là do tôi cần thời gian đọc hiểu, ngâm cứu. Nói là ăn xổi nhưng mà cũng phải từ từ mới chén được các bạn à. Okey, vậy hôm nay chúng ta có gì?

  Channel trong Golang là gì? So sánh Callback function và mutex lock với channel
  Clean Architecture: Đứng trên vai những gã khổng lồ

Xem thêm tuyển dụng Golang hấp dẫn trên TopDev

Clean architecture trong Golang.

Trong bài 1 tôi có đưa các bạn 1 đường dẫn Github. Tuy nhiên sau 1 tuần tôi mới biết là những thứ tôi viết đó còn thô sơ lắm, nếu đưa triển khai cho các bạn thì ắt hẳn chúng ta sẽ hơi hổng 1 số lượng kiến thức. Do vậy mà tôi sẽ dùng Repository mới ở đây của 1 developer nước ngoài:

https://github.com/eminetto/clean-architecture-go-v2

Và tôi cũng mượn chính bài viết của các giả tại đây để viết cho bài viết của tôi, nói đúng ra là tôi dịch.

Các bạn cứ tải source về và nghiên cứu vì nó xịn hơn source mà tôi viết. Một số bài viết khác mà bạn có thể đọc thêm ở đây, do các pro dev viết:

https://dev.to/aleksk1ng/my-first-go-rest-api-3bl3

Tuy nhiên, với bài viết trên thì khó cho người mới lắm. Họ cho code nhưng không giải thích thì cũng khó mà nắm được đúng không nào?

Bây giờ chúng ta sẽ đi vào thành phần đầu tiên của Clean architecture:

Entity Layer

Hay nói cách khác là lớp model. Đây là lớp trong cùng của kiến trúc Clean architecture.

Theo bài đăng của tác giả mô hình này – Uncle Bob:

  • Các Entities chứa đựng quy tắc của business ứng dụng. Nó có thể là các objects của phương thức sử dụng, tập hợp các cấu trúc data hay các functions. Nó được sử dụng bởi nhiều ứng dụng trong 1 công ty.

Cấu trúc entity như sau:

entity

Ở package trên, các entities gồm book, book_test, entity.. Ví dụ với user như sau:

package entity

import (
	"time"

	"golang.org/x/crypto/bcrypt"
)

//User data
type User struct {
	ID        ID
	Email     string
	Password  string
	FirstName string
	LastName  string
	CreatedAt time.Time
	UpdatedAt time.Time
	Books     []ID
}

//NewUser create a new user
func NewUser(email, password, firstName, lastName string) (*User, error) {
	u := &User{
		ID:        NewID(),
		Email:     email,
		FirstName: firstName,
		LastName:  lastName,
		CreatedAt: time.Now(),
	}
	pwd, err := generatePassword(password)
	if err != nil {
		return nil, err
	}
	u.Password = pwd
	err = u.Validate()
	if err != nil {
		return nil, ErrInvalidEntity
	}
	return u, nil
}

//AddBook add a book
func (u *User) AddBook(id ID) error {
	_, err := u.GetBook(id)
	if err == nil {
		return ErrBookAlreadyBorrowed
	}
	u.Books = append(u.Books, id)
	return nil
}

//RemoveBook remove a book
func (u *User) RemoveBook(id ID) error {
	for i, j := range u.Books {
		if j == id {
			u.Books = append(u.Books[:i], u.Books[i+1:]...)
			return nil
		}
	}
	return ErrNotFound
}

//GetBook get a book
func (u *User) GetBook(id ID) (ID, error) {
	for _, v := range u.Books {
		if v == id {
			return id, nil
		}
	}
	return id, ErrNotFound
}

//Validate validate data
func (u *User) Validate() error {
	if u.Email == "" || u.FirstName == "" || u.LastName == "" || u.Password == "" {
		return ErrInvalidEntity
	}

	return nil
}

//ValidatePassword validate user password
func (u *User) ValidatePassword(p string) error {
	err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(p))
	if err != nil {
		return err
	}
	return nil
}

func generatePassword(raw string) (string, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(raw), 10)
	if err != nil {
		return "", err
	}
	return string(hash), nil
}

Với entity này, chúng ta có các business như:

  • Tạo 1 user mới với email, password, firstName, lastName.
  • Thêm/xóa/lấy ra 1 cuốn sách Book bởi 1 user theo id của sách.
  • Kiểm tra 1 user có hợp lệ không thông qua các parameters của nó.
  • Tạo 1 mật khẩu hash từ mật khẩu dạng plan text chưa qua mã hóa.

Phần tiếp theo là:

Use Case Layer

Theo tác giả Uncle Bob:

Phần mềm trong lớp này chứa các quy tắc nghiệp vụ cụ thể của ứng dụng. Nó đóng gói và triển khai tất cả các trường hợp sử dụng của hệ thống.

Nó có thể trông như sau:

domain

Các package trong usercase này sẽ triển khai các quy tắc của sản phẩm, ví dụ với quy tắc loan – cho mượn, file service.go sẽ như sau:

package loan

import (
	"github.com/eminetto/clean-architecture-go-v2/entity"
	"github.com/eminetto/clean-architecture-go-v2/usecase/book"
	"github.com/eminetto/clean-architecture-go-v2/usecase/user"
)

//Service loan usecase
type Service struct {
	userService user.UseCase
	bookService book.UseCase
}

//NewService create new use case
func NewService(u user.UseCase, b book.UseCase) *Service {
	return &Service{
		userService: u,
		bookService: b,
	}
}

//Borrow borrow a book to an user
func (s *Service) Borrow(u *entity.User, b *entity.Book) error {
	u, err := s.userService.GetUser(u.ID)
	if err != nil {
		return err
	}
	b, err = s.bookService.GetBook(b.ID)
	if err != nil {
		return err
	}
	if b.Quantity <= 0 {
		return entity.ErrNotEnoughBooks
	}

	err = u.AddBook(b.ID)
	if err != nil {
		return err
	}
	err = s.userService.UpdateUser(u)
	if err != nil {
		return err
	}
	b.Quantity--
	err = s.bookService.UpdateBook(b)
	if err != nil {
		return err
	}
	return nil
}

//Return return a book
func (s *Service) Return(b *entity.Book) error {
	b, err := s.bookService.GetBook(b.ID)
	if err != nil {
		return err
	}

	all, err := s.userService.ListUsers()
	if err != nil {
		return err
	}
	borrowed := false
	var borrowedBy entity.ID
	for _, u := range all {
		_, err := u.GetBook(b.ID)
		if err != nil {
			continue
		}
		borrowed = true
		borrowedBy = u.ID
		break
	}
	if !borrowed {
		return entity.ErrBookNotBorrowed
	}
	u, err := s.userService.GetUser(borrowedBy)
	if err != nil {
		return err
	}
	err = u.RemoveBook(b.ID)
	if err != nil {
		return err
	}
	err = s.userService.UpdateUser(u)
	if err != nil {
		return err
	}
	b.Quantity++
	err = s.bookService.UpdateBook(b)
	if err != nil {
		return err
	}

	return nil
}

Ở file trên các rule sau được triển khai:

  • Mượn sách bởi 1 người dùng.
  • Trả 1 cuốn sách bởi người dùng.

Tiếp theo ta cùng nghiên cứu lớp:

Frameworks and Drivers layer

Theo tác giả Uncle Bob:

Lớp ngoài cùng thường chứa các frameworks và tools như Database, Web Framework… Lớp này chứa tất cả chi tiết của go:

driver

Cho ví dụ, với file infrastructure/repository/user_mysql.go, chúng ta triển khai các interface Repository của MySQL. Nếu chúng ta muốn chuyển đổi sang cơ sở dữ liệu mới, đây là nơi chuyển đổi.

Tiếp theo là lớp:

Interface Adapters layer

Codes ở lớp này được thích ứng và chuyển đổi dữ liệu tới format được sử dụng bởi các entities và các use cases cho việc mở rộng bởi các tác nhân bên ngoài như như databases, web… Ở lớp Application, có 2 cách để truy cập tới các UseCases. Đầu tiên là các API và thứ 2 là các command line của ứng dụng CLI.

Cấu trúc của CLI đơn giản như sau:

cliVí dụ nó được sử dụng bởi domain packages để thực hiện việc tìm kiếm sách:
dataSourceName := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true", config.DB_USER, config.DB_PASSWORD, config.DB_HOST, config.DB_DATABASE)
db, err := sql.Open("mysql", dataSourceName)
if err != nil {
	log.Fatal(err.Error())
}
defer db.Close()
repo := repository.NewBookMySQL(db)
service := book.NewService(repo)
all, err := service.SearchBooks(query)
if err != nil {
	log.Fatal(err)
}
for _, j := range all {
	fmt.Printf("%s %s \n", j.Title, j.Author)
}

Trong ví dụ trên, bạn có thể thấy được cách sử dụng bởi package config.

configCấu trúc API thường phức tạp hơn với 3 packages: handler, presenter và middleware.

Package handler bao gồm các requests và responses, cũng như các quy tắc có sẵn của business ở usecases:

handlerLớp presenters chịu trách nhiệm định dạng dữ liệu sinh ra giống như response bởi các handlers.
presenterTheo cách này, với entity User:
type User struct {
	ID        ID
	Email     string
	Password  string
	FirstName string
	LastName  string
	CreatedAt time.Time
	UpdatedAt time.Time
	Books     []ID
}

Nó sẽ được chuyển đổi thành:

type User struct {
	ID        entity.ID `json:"id"`
	Email     string    `json:"email"`
	FirstName string    `json:"first_name"`
	LastName  string    `json:"last_name"`
}

Điều này cho phép chúng ta kiểm soát 1 entity sẽ được cung cấp thông qua API.

Trong packages cuối cùng của API là các middlewares, được sử dụng bởi các endpoints:

middlwareLớp tiếp theo là:

Support packages

Chúng là các packages cung cấp các hàm dùng chung như mã hóa, logging, xử lý files,… Chúng là các tính năng không thuộc domain của ứng dụng, và tất cả các lớp đều có thể sử dụng chúng. Ngay cả các ứng dụng khác cũng có thể import và sử dụng các packages này.

pkgHãy xem thêm ở README.md để hiểu chi tiết hơn, chẳng hạn hướng dẫn cách xây dựng và sử dụng ví dụ này.

Nếu bạn yêu thích Golang thì bài viết này là 1 ví dụ tuyệt vời để học nó.

Xem thêm nhiều bài viết tại www.facebook.com/codetoanbug

Bài viết gốc được đăng tải tại codetoanbug.com

Có thể bạn quan tâm:

Xem thêm việc làm IT hấp dẫn trên TopDev