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ì?
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:
Ở 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:
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:
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:
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.
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:
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:
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.
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:
- #1 Lập trình Golang ăn xổi: Giới thiệu dự án
- Clean Code là gì? Tại sao phải CLEAN CODE trong lập trình?
- Golang toàn tập – Goroutines và Channels
Xem thêm việc làm IT hấp dẫn trên TopDev