Hexagonal Architecture là gì?
Hexagonal Architecture (tên gọi khác là ports and adapters architecture), là một mẫu kiến trúc được dùng trong thiết kế phần mềm. Nó hướng tới việc xây dựng ứng dụng xoay quanh business/application logic mà không ảnh hưởng hoặc phụ thuộc bởi bất kì thành phần bên ngoài, mà chỉ giao tiếp với chúng qua ports/adapters.
Vì tính không phụ thuộc nên chúng ta dễ dàng chuyển đổi giữa các data sources (libs/frameworks) mà không ảnh hưởng đến business/application logic. Inputs/Outputs của data sources đều được đặt ở các cạnh của hình lục giác (hexagonal).
Ưu điểm của Hexagonal Architecture
- Tổ chức code xoay quay business rules, không phải framework hay library
- Dễ kiểm soát bởi nguyên tắc phụ thuộc chỉ cho phép các layer phụ thuộc các layer bên trong nó
- Hỗt trợ tốt triển khai testing, maintain
- Code base dễ dàng mở rộng
- Ứng dụng miễn phụ thuộc với sự phát triển của công nghệ (library/framework)
- Hạn chế việc tốn thời gian trong việc lựa chọn công nghệ cho dự án
- Codebase có thể dùng chung cho frontend, backend hay mobile app
Cấu trúc của Hexagonal Architecture
- Core (Application): Nằm ở trung tâm, chứa domain và use cases (business rules).
- Ports (Interfaces): Là các cổng giao tiếp được định nghĩa bởi core, biểu diễn các hành vi mà core yêu cầu hoặc chấp nhận.
- Adapters: Là các lớp cụ thể triển khai các Ports, dùng để kết nối với thế giới bên ngoài (REST API, DB, CLI, UI…).
Xây dựng ứng dụng RESTful API đơn giản theo cấu trúc Hexagonal Architecture
Tổ chức thư mục
Domain layer: chứa các business entity và business rules. Độc lập với các layer khác, không phụ thuộc framework hay database.
File user.go:
- Struct User: đại diện cho entity người dùng
- Interface UserRepository: định nghĩa các phương thức tương tác với database
- Interface UserUseCase: định nghĩa các business logic
package domain //business entity and business rules type User struct { ID uint `json:"id" gorm:"primaryKey"` Username string `json:"username" gorm:"unique"` Password string `json:"password"` Email string `json:"email" gorm:"unique"` Name string `json:"name"` } type UserRepository interface { Create(user *User) error FindByUsername(username string) (*User, error) FindByEmail(email string) (*User, error) Update(user *User) error } type UserUseCase interface { Register(user *User) error Login(username, password string) (*User, error) UpdateProfile(user *User) error }
Repository Layer: implement các interface từ domain layer. Đây là adapter bên ngoài để làm việc với database. File user_repository.go implement từ interface UserRepository:
type userRepository struct { db *gorm.DB } func NewUserRepository(db *gorm.DB) domain.UserRepository { return &userRepository{db: db} } func (r *userRepository) Create(user *domain.User) error { return r.db.Create(user).Error } func (r *userRepository) FindByUsername(username string) (*domain.User, error) { var user domain.User err := r.db.Where("username = ?", username).First(&user).Error if err != nil { return nil, err } return &user, nil } func (r *userRepository) FindByEmail(email string) (*domain.User, error) { var user domain.User err := r.db.Where("email = ?", email).First(&user).Error if err != nil { return nil, err } return &user, nil } func (r *userRepository) Update(user *domain.User) error { return r.db.Save(user).Error }
type userUseCase struct { userRepo domain.UserRepository } func NewUserUseCase(userRepo domain.UserRepository) domain.UserUseCase { return &userUseCase{userRepo: userRepo} } func (uc *userUseCase) Register(user *domain.User) error { if _, err := uc.userRepo.FindByUsername(user.Username); err == nil { return errors.New("username already exists") } if _, err := uc.userRepo.FindByEmail(user.Email); err == nil { return errors.New("email already exists") } return uc.userRepo.Create(user) } func (uc *userUseCase) Login(username, password string) (*domain.User, error) { user, err := uc.userRepo.FindByUsername(username) if err != nil { return nil, errors.New("invalid username or password") } if user.Password != password { return nil, errors.New("invalid username or password") } return user, nil } func (uc *userUseCase) UpdateProfile(user *domain.User) error { existingUser, err := uc.userRepo.FindByUsername(user.Username) if err != nil { return errors.New("user not found") } existingUser.Name = user.Name existingUser.Email = user.Email return uc.userRepo.Update(existingUser) }
type UserHandler struct { userUseCase domain.UserUseCase } func NewUserHandler(userUseCase domain.UserUseCase) *UserHandler { return &UserHandler{userUseCase: userUseCase} } func (h *UserHandler) Register(c *gin.Context) { var user domain.User if err := c.ShouldBindJSON(&user); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.userUseCase.Register(&user); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"message": "user registered successfully"}) } func (h *UserHandler) Login(c *gin.Context) { var loginRequest struct { Username string `json:"username"` Password string `json:"password"` } if err := c.ShouldBindJSON(&loginRequest); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user, err := h.userUseCase.Login(loginRequest.Username, loginRequest.Password) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"user": user}) } func (h *UserHandler) UpdateProfile(c *gin.Context) { var user domain.User if err := c.ShouldBindJSON(&user); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.userUseCase.UpdateProfile(&user); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "profile updated successfully"}) }
func main() { if err := godotenv.Load("../../.env"); err != nil { log.Fatal("Error loading .env file") } dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_NAME"), ) db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { log.Fatal("Failed to connect to database:", err) } //migrate the database db.AutoMigrate(&domain.User{}) userRepo := repository.NewUserRepository(db) userUseCase := usecase.NewUserUseCase(userRepo) userHandler := http.NewUserHandler(userUseCase) // Setup router r := gin.Default() r.POST("/register", userHandler.Register) r.POST("/login", userHandler.Login) r.PUT("/profile", userHandler.UpdateProfile) port := os.Getenv("PORT") if port == "" { port = "8080" } r.Run(":" + port) }
So sánh với MVC và Clean Architecture
Tiêu chí | MVC | Clean Architecture | Hexagonal Architecture |
---|---|---|---|
Tách biệt domain | Không rõ ràng | Rõ ràng | Rõ ràng |
Phụ thuộc chiều | View → Controller → Model | Vòng tròn, phụ thuộc hướng vào trong | Từ Adapters phụ thuộc ngược vào Core |
Linh hoạt thay đổi adapter | Hạn chế (UI, DB gắn liền logic) | Dễ (nhờ Dependency Inversion) | Dễ (do tách Port và Adapter) |
Trọng tâm | Hiển thị và điều khiển dữ liệu | Tập trung domain và usecase | Tập trung vào việc kết nối core ↔ outside |
Interface đóng vai trò gì | Không rõ ràng | Là contract giữa layers | Port là interface, adapter là implementation |
Ứng dụng phổ biến | Web app nhỏ, đơn giản | Hệ thống lớn, phức tạp | Microservices, ứng dụng cần dễ tích hợp |
Github: DphatDora/golang_resful_hexa
Tham khảo:
- Building RESTful API with Hexagonal Architecture in Go – DEV Community
- Hexagonal Architecture là gì và ứng dụng của nó