- Published on
แปลง REST API จาก Layered Architecture ไปเป็น Hexagonal Architecture
- Authors
- Name
- Somprasong Damyos
- @somprasongd
แปลง REST API จาก Layered Architecture ไปเป็น Hexagonal Architecture
ทำไมต้องเปลี่ยน?
ปัญหาของ Layered Architecture
- การพึ่งพาระหว่างเลเยอร์แบบแน่น (Tight Coupling)
- แต่ละเลเยอร์ (Controller, Service, Repository) ผูกติดกันโดยตรง ทำให้การเปลี่ยนแปลงโค้ดยาก
- ทดสอบยาก (Difficult to Test)
- Unit Test ยากเพราะต้อง Mock หลายชั้น
- ขยายระบบลำบาก (Hard to Scale)
- การเพิ่มฟีเจอร์ใหม่ต้องผ่านเลเยอร์เดิมทั้งหมด
ข้อดีของ Hexagonal Architecture
- แยก Business Logic ออกจาก User Interface กับ Infrastructure
- ไม่ผูกกับ Framework หรือ Database
- เพิ่มความยืดหยุ่น (Flexibility)
- เปลี่ยนแปลง Framework หรือ Database ได้ง่าย
- ทดสอบง่าย (Testability)
- Mock Interfaces ได้สะดวก
ตัวอย่างการแปลงโค้ด
▶ โค้ดใน Layered Architecture
🗂️ โครงสร้าง Layered Architecture :
├── application
│ └── app.go
├── handler
│ ├── customer.go
│ └── order.go
├── service
│ ├── customer.go
│ └── order.go
├── repository
│ ├── customer.go
│ └── order.go
├── model
│ ├── customer.go
│ └── order.go
└── main.go
#️⃣ handler.go :
package handler
import (
"github.com/gofiber/fiber/v2"
"myapp/service"
)
type CustomerHandler struct {
service *service.CustomerService
}
func NewCustomerHandler(s *service.CustomerService) *CustomerHandler {
return &CustomerHandler{service: s}
}
func (h *CustomerHandler) GetCustomer(c *fiber.Ctx) error {
id := c.Params("id")
customer, err := h.service.GetCustomer(id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(err.Error())
}
return c.JSON(customer)
}
#️⃣ service.go :
package service
import (
"myapp/model"
"myapp/repository"
)
type CustomerService struct {
repo *repository.CustomerRepository
}
func NewCustomerService(r *repository.CustomerRepository) *CustomerService {
return &CustomerService{repo: r}
}
func (s *CustomerService) GetCustomer(id string) (*model.Customer, error) {
return s.repo.FindById(id)
}
#️⃣ repository.go :
package repository
import (
"database/sql"
"myapp/model"
)
type CustomerRepository struct {
db *sql.DB
}
func NewCustomerRepository(db *sql.DB) *CustomerRepository {
return &CustomerRepository{db: db}
}
func (r *CustomerRepository) FindById(id string) (*model.Customer, error) {
var customer model.Customer
err := r.db.QueryRow("SELECT id, name FROM customers WHERE id = $1", id).Scan(&customer.ID, &customer.Name)
if err != nil {
return nil, err
}
return &customer, nil
}
▶ โค้ดใน Hexagonal Architecture
🗂️ โครงสร้าง Hexagonal Architecture :
├── application
│ └── app.go
├── contract
│ └── customer_api
│ ├── dto.go
│ └── service.go
├── module
│ ├── customer
│ │ ├── handler.go
│ │ ├── service.go
│ │ ├── repository.go
│ │ └── model.go
│ └── order
└── main.go
#️⃣ contract/customer_api/service.go :
package customer_api
type CustomerService interface {
GetCustomer(id string) (*CustomerDTO, error)
}
type CustomerDTO struct {
ID string `json:"id"`
Name string `json:"name"`
}
#️⃣ module/customer/model.go :
package customer
type Customer struct {
ID string
Name string
}
#️⃣ module/customer/repository.go :
package customer
import (
"database/sql"
)
type CustomerRepository interface {
FindById(id string) (*Customer, error)
}
type customerRepository struct {
db *sql.DB
}
func NewCustomerRepository(db *sql.DB) CustomerRepository {
return &customerRepository{db: db}
}
func (r *customerRepository) FindById(id string) (*Customer, error) {
var c Customer
err := r.db.QueryRow("SELECT id, name FROM customers WHERE id = $1", id).Scan(&c.ID, &c.Name)
if err != nil {
return nil, err
}
return &c, nil
}
#️⃣ module/customer/service.go :
package customer
import "myapp/contract/customer_api"
type customerService struct {
repo CustomerRepository
}
func NewCustomerService(repo CustomerRepository) customer_api.CustomerService {
return &customerService{repo: repo}
}
func (s *customerService) GetCustomer(id string) (*customer_api.CustomerDTO, error) {
c, err := s.repo.FindById(id)
if err != nil {
return nil, err
}
return &customer_api.CustomerDTO{ID: c.ID, Name: c.Name}, nil
}
#️⃣ module/customer/handler.go :
package customer
import (
"github.com/gofiber/fiber/v2"
"myapp/contract/customer_api"
)
type CustomerHandler struct {
service customer_api.CustomerService
}
func NewCustomerHandler(s customer_api.CustomerService) *CustomerHandler {
return &CustomerHandler{service: s}
}
func (h *CustomerHandler) GetCustomer(c *fiber.Ctx) error {
id := c.Params("id")
customer, err := h.service.GetCustomer(id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(err.Error())
}
return c.JSON(customer)
}
สรุป
Hexagonal Architecture ช่วยให้โค้ดสะอาด (Clean Code) มีการแยกหน้าที่ชัดเจน (Separation of Concerns) และสามารถขยายระบบ (Scalability) ได้ง่ายกว่า Layered Architecture โดยเฉพาะเมื่อแอปพลิเคชันซับซ้อนมากขึ้น.