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
}
Usercase Layer: implement business logic được định nghĩa trong domain layer. file user_usercase.go chứa login đăng ký, đăng nhập, cập nhật thông tin.
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)
}
Delivery Layer: chứa các handler cho API endpoints. Đây là adapter bên ngoài để tương tác với client:
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"})
}
Application entry point: File main.go là điểm khởi đầu của ứng dụng. Khởi tạo và kết nối các layer với nhau. Cấu hình database, router, và các dependency khác.
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:

About the Author

Đức Phát

View all author's posts

Leave a Comment

Your email address will not be published. Required fields are marked *

Bài viết khác

Go-pg

go-pg là một thư viện ORM (Object-Relational Mapping) và trình điều khiển PostgreSQL cho ngôn ngữ lập trình Go. Nó cung cấp một cách tiện lợi để tương tác với cơ sở dữ liệu PostgreSQL bằng cách ánh xạ các cấu trúc (structs) trong Go thành các bảng trong cơ sở dữ liệu và ngược […]

Ngôn ngữ lập trình Golang

Golang là gì? Go (hay còn gọi là Golang) là một ngôn ngữ lập trình được thiết kế dựa trên tư duy lập trình hệ thống. Go được phát triển bởi Robert Griesemer, Rob Pike và Ken Thompson tại Google vào năm 2007. Điểm mạnh của Go là bộ thu gom rác và hỗ trợ […]

Tìm hiểu RESTful API

RESTful là gì? REST (Representational State Transfer) là gì? REST (Representational State Transfer) không phải là một ngôn ngữ lập trình hay một framework, mà là một kiểu kiến trúc phần mềm (architectural style) để thiết kế các hệ thống mạng phân tán, đặc biệt là các dịch vụ web (web services). Nó được giới […]

Hệ quản trị CSDL PostgreSQL

SQL là gì? SQL (Structured Query Language) là viết tắt của Ngôn ngữ truy vấn có cấu trúc, là ngôn ngữ được tiêu chuẩn hóa để tương tác với các hệ thống quản lý cơ sở dữ liệu quan hệ (RDBMS). Cơ sở dữ liệu quan hệ là tập hợp dữ liệu được tổ chức thành các […]

Flutter Form

FLUTTER FORM LÀ GÌ? Form trong Flutter là một widget dùng để thu thập và kiểm tra dữ liệu người dùng nhập vào. Khi ứng dụng yêu cầu nhập nhiều trường (fields) và cần xác minh tính hợp lệ của các giá trị này, việc sử dụng Form kết hợp với TextFormField là giải pháp […]

Backend xử lý với dữ liệu người dùng tải lên như thế nào?

Khi người dùng tải lên dữ liệu (file, hình ảnh, video, v.v.), backend thường xử lý dữ liệu này qua các bước sau: Nhận dữ liệu  Qua API: Backend nhận dữ liệu từ HTTP request (thường là POST hoặc PUT). Ví dụ: sử dụng multipart/form-data cho file. Xác thực dữ liệu Kiểm tra định dạng: […]