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

So sánh giữa gorm vs go-pg vs bun

Trong thế giới phát triển Golang, việc tương tác với cơ sở dữ liệu là một phần không thể thiếu của hầu hết các ứng dụng. Để đơn giản hóa quy trình này, các thư viện ORM (Object-Relational Mapping) ra đời, giúp các nhà phát triển thao tác với database thông qua các đối tượng […]

So sánh GORM vs go-pg vs Bun

Cộng đồng GORM Là ORM phổ biến nhất trong cộng đồng Go. Có nhiều tài liệu, ví dụ, StackOverflow câu trả lời, và nhiều package hỗ trợ mở rộng. Nhiều developer đã từng dùng Gorm. go-pg Từng rất phổ biến khi chỉ dùng PostgreSQL, nhưng đang bị Bun thay thế dần. Ít được duy trì […]

clean architecture golang

  1.Clean Architecture là gì? Clean Architecture là một kiến trúc phần mềm được đề xuất bởi Robert C. Martin (Uncle Bob) nhằm mục tiêu tách biệt rõ ràng giữa các tầng trong ứng dụng, giúp mã nguồn dễ bảo trì, mở rộng, và kiểm thử. 2.Tổng quan kiến trúc Entity (Domain Model): Là tầng […]

Sử dụng Request/Response trong ứng dụng RESTful mô hình MVC

DTO là gì? DTO (Data Transfer Object) là một object trung gian dùng để truyền dữ liệu giữa client – server hoặc giữa các service trong ứng dụng web/API theo kiến trúc RESTful API. DTO chỉ chứa các thông tin cần thiết mà client hoặc service khác cần (ví dụ: Login Form chỉ cần thông […]

Docker

Docker là gì? Docker là một nền tảng mã nguồn mở cho phép bạn đóng gói, phân phối và chạy ứng dụng bên trong các “container” – những môi trường ảo nhẹ, cô lập nhưng vẫn chia sẻ nhân hệ điều hành của máy chủ. Khái niệm then chốt ở đây là “containerization”: thay vì […]

Kiểm thử phần mềm

Testing là gì? Quá trình kiểm thử phần mềm (software testing) là một chuỗi hoạt động có hệ thống nhằm đánh giá chất lượng, độ ổn định và tính đúng đắn của một ứng dụng so với yêu cầu ban đầu. Không đơn thuần chỉ là tìm ra lỗi, testing còn giúp đảm bảo rằng […]