code ด้วย Clean Architecture
มาเริ่ม code กัน
เราจะนำ Diagram จากหน้าที่แล้วมา code ไปในแต่ละส่วนกัน โดยจะทำ การวาง Structure ประมาณนี้ออกมา
เมื่อลองมาเรียบเรียงเป็น code
.
├── adapters --> ส่วนของ adapter
│ ├── gorm_order_repository.go
│ └── http_order_handler.go
├── entities --> ส่วนของ entities
│ └── order.go
├── main.go --> เป็นตัวหลักที่เชื่อมทั้งหมดเข้าด้วยกัน
└── usecases --> ส่วนของ use case
├── order_repository.go
└── order_use_case.go
Entities
เริ่มจากส่วนแรกคือ Entities คือส่วนของ Order Entity
ที่เป็นการประกาศโครงสร้างของข้อมูล Order เอาไว้ โดยไฟล์ที่เกี่ยวข้องจะมีเพียงไฟล์เดียวคือ order.go
.
├── adapters --> ส่วนของ adapter
│ ├── gorm_order_repository.go
│ └── http_order_handler.go
├── entities --> ส่ว นของ entities
│ └── order.go
├── main.go --> เป็นตัวหลักที่เชื่อมทั้งหมดเข้าด้วยกัน
└── usecases --> ส่วนของ use case
├── order_repository.go
└── order_use_case.go
Idea ของ Entities คือการประกาศโครงสร้างของ Data ไว้ ดังนั้น code ของ Entities จึงเป็นเพียงการประกาศ struct ของข้อมูลเพื่อเป็นการบอกว่าข้อมูลมีหน้าตาออกมาประมาณไหน
และนี่คือ code ของ order.go
โดยเป็นการประกาศโครงสร้างของ Order data เอาไว้ว่าจะเก็บ 2 ค่าคือ ID และ Total
package entities
type Order struct {
ID uint
Total float64
}
Usecase
ส่วนต่อมา Use case คือส่วนของการใช้งาน ซึ่งในที่นี่คือ function createOrder
สำหรับการสร้าง Order ซึ่งจะเก็บเอาไว้ใน Order Service
- โดย
Order Service
นั้นก็จะอ้างอิงโครงสร้างของ Order ตามOrder Entity
ที่อยู่ใน layer ด้านในสุดอีกที - เพิ่มเติมอีกตัวคือ ในการจัดการกับ Use case ต้องมีการจัดการผ่าน
Repository
(เป็นตัวแทนของการคุยกับ Database) ด้วย ดังนั้น ในการทำ Use case ต้องมีการเพิ่ม interface ของ Repository ด้วย เพื่อเป็นการบอกไปยังAdapter
ที่จะ implement - ว่าจะต้องส่งคำสั่งไหนมาบ้างเพื่อให้ใช้งานตาม Use case ได้
ดังนั้น file ที่เกี่ยวข้องจะมี 2 files คือ
order_repository.go
เป็นตัวแทนของ interface ของ Repositoryorder_use_case.go
เป็นไฟล์สำหรับการเก็บ Business Logic ของ use case ไว้
.
├── adapters --> ส่วนของ adapter
│ ├── gorm_order_repository.go
│ └── http_order_handler.go
├── entities --> ส่วนของ entities
│ └── order.go
├── main.go --> เป็นตัวหลักที่เชื่อมทั้งหมดเข้าด้วยกัน
└── usecases --> ส่วนของ use case
├── order_repository.go
└── order_use_case.go
และนี่คือส่วน code ของทั้ง 2 files
- order_use_case.go
- order_repository.go
package usecases
import (
"mike/entities"
)
type OrderUseCase interface {
CreateOrder(order entities.Order) error
}
type OrderService struct {
repo OrderRepository
}
func NewOrderService(repo OrderRepository) OrderUseCase {
return &OrderService{repo: repo}
}
func (s *OrderService) CreateOrder(order entities.Order) error {
return s.repo.Save(order)
}
package usecases
import "mike/entities"
type OrderRepository interface {
Save(order entities.Order) error
}
อธิบาย code
order_repository.go
ทำการเก็บ Repository ของ Order ไว้ จึงมี code เพียงแค่ interface ของOrderRepository
เท่านั้นและเป็นการบอกว ่า Repository ต้องมี methodSave()
มาด้วย จึงจะตรงตามOrderRepository
order_use_case.go
ทำการเก็บคำสั่งสำหรับจัดการสร้าง order เอาไว้ซึ่งก็คือCreateOrder()
โดยจะทำการ implement ผ่าน method ของ usecase ไว้ในตัวชื่อOrderUseCase
และจะมีOrderService
เป็นตัวแทนของการรับOrderRepository
จากภายนอก (ที่ส่งเข้ามาจาก Adapter) มาอีกที
Adapter
ต่อมา Adapter คือส่วนของการแปลงข้อมูลเพื่อทำการส่งไปยัง Use case เพื่อให้ Use case สามารถจัดการต่อได้ถูกได้จะมีทั้งหมด 2 ส่วน (โดยจะแยกออกเป็น 2 files) คือ
gorm_order_repository.go
(GORM Order Repository) คือส่วนที่จะเก็บ function สำหรับจัดการ database เอาไว้ โดยจะเก็บคำสั่งที่เกี่ยวข้องกับการ query database ไว้ โดยจะต้อง implement ตาม spec ของ interface ที่กำหนดไว้ในOrder Service
http_order_handler.go
(HTTP Order Handler) คือส่วนที่จะเก็บ function สำหรับจัดการ data ที่ผ่าน เข้ามาทาง HTTP Request โดยทำการแปลงข้อมูลให้ถูกต้องตาม Pattern ของ Entities เพื่อส่งให้Order Service
สามารถจัดการต่อได้
ไฟล์ที่แตะก็จะมี 2 files นี้
.
├── adapters --> ส่วนของ adapter
│ ├── gorm_order_repository.go
│ └── http_order_handler.go
├── entities --> ส่วนของ entities
│ └── order.go
├── main.go --> เป็นตัวหลักที่เชื่อมทั้งหมดเข้าด้วยกัน
└── usecases --> ส่วนของ use case
├── order_repository.go
└── order_use_case.go
และนี่คือหน้าตา code ของ Adapter ทั้ง 2 ตัว
- gorm_order_repository.go
- http_order_handler.go
package adapters
import (
"gorm.io/gorm"
"mike/usecases"
"mike/entities"
)
type GormOrderRepository struct {
db *gorm.DB
}
func NewGormOrderRepository(db *gorm.DB) usecases.OrderRepository {
return &GormOrderRepository{db: db}
}
func (r *GormOrderRepository) Save(order entities.Order) error {
return r.db.Create(&order).Error
}
package adapters
import (
"github.com/gofiber/fiber/v2"
"mike/entities"
"mike/usecases"
)
type HttpOrderHandler struct {
orderUseCase usecases.OrderUseCase
}
func NewHttpOrderHandler(useCase usecases.OrderUseCase) *HttpOrderHandler {
return &HttpOrderHandler{orderUseCase: useCase}
}
func (h *HttpOrderHandler) CreateOrder(c *fiber.Ctx) error {
var order entities.Order
if err := c.BodyParser(&order); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request"})
}
if err := h.orderUseCase.CreateOrder(order); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(order)
}
อธิบาย code เพิ่มเติม
gorm_order_repository.go
ทำการ implementSave()
โดยทำการใส่ query ที่เกี่ยวข้องกับการจัดการ Order เข้าไป (ซึ่งก็คือคำสั่ง Gorm)http_order_handler.go
ทำการ implementHttpOrderHandler
โดยเป็นการรับ ตัวแทนของ Use case มาเพื่อใช้คำสั่งภายในOrder Service
(คำสั่งสำหรับการสร้าง Order) และทำการสร้าง methodCreateOrder()
เพื่อใช้สำหรับการเรียกใช้งานจากฝั่งของ HTTP Request ออกมา โดยจะเรียกไปยังOrder Service
และทำการแปลงข้อมูลจาก HTTP Request มาเป็นข้อมูลของOrder Entity
เพื่อให้สามารถใช้งานที่Order Service
ได้
เรียกใช้งาน
เมื่อทำการสร้าง Entities, Use Case และ Adapter เรียบร้อย ขั้นตอนสุดท้ายเราจะทำการมัดรวมทุกอย่างเข้าด้วยกัน (เหมือนตอนทำ Hexagonal Architecture) โดยเราจะรวมทั้งหมดผ่านไฟล์ main.go
เข้าด้วยกัน
.
├── adapters --> ส่วนของ adapter
│ ├── gorm_order_repository.go
│ └── http_order_handler.go
├── entities --> ส่วนของ entities
│ └── order.go
├── main.go --> เป็นตัวหลักที่เชื่อมทั้งหมดเข้าด้วยกัน
└── usecases --> ส่วนของ use case
├── order_repository.go
└── order_use_case.go
และนี่คือหน้าตาของ code main.go
package main
import (
"github.com/gofiber/fiber/v2"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"log"
"mike/adapters"
"mike/entities"
"mike/usecases"
)
func main() {
app := fiber.New()
db, err := gorm.Open(sqlite.Open("orders.db"), &gorm.Config{})
if err != nil {
log.Fatalf("failed to connect database: %v", err)
}
if err := db.AutoMigrate(&entities.Order{}); err != nil {
log.Fatalf("failed to migrate database: %v", err)
}
orderRepo := adapters.NewGormOrderRepository(db)
orderService := usecases.NewOrderService(orderRepo)
orderHandler := adapters.NewHttpOrderHandler(orderService)
app.Post("/order", orderHandler.CreateOrder)
log.Fatal(app.Listen(":8000"))
}
อธิบายตาม code คือ
- ขั้นแรกกำหนด config ของ GORM (ส่วน database เชื่อมไปยัง sqlite) และ Fiber (ทำ HTTP Server)
- ต่อมาทำการส่ง db (ตัวแทนของ GORM) เข้าไปยัง
adapters.NewGormOrderRepository
เพื่อทำการแปลงเป็นOrder Repository
สำหรับใช้งานในOrder Service
- ส่ง
Order Repository
เข้าไปยังusecases.NewOrderService(orderRepo)
เพื่อทำการแปลงเป็นOrder Service
ที่เป็นตัวแทนสำหรับคุยในขั้นของ Use case - สุดท้ายส่ง
Order Service
เข้าไปในadapters.NewHttpOrderHandler(orderService)
เพื่อทำการแปลงเป็นOrder HTTP Handler
เป็นตัวแทนของการคุย Request นี้ออกมา โดย Handler นั้นก็จะทำการเรียกใช้จาก Service ที่มีการส่งเข้าไปสำหรับการทำ logic ภายใน Handler นั้น - และที่เหลือก็ทำการผูก
orderHandler.CreateOrder
เข้ากับ endpointPOST /order
เพื่อให้สามารถเรียกใช้งานCreateOrder()
จากใน Adapter ได้
เมื่อลอง run project ด้วย go run main.go
ดู ก็จะสามารถ run ผลลัพธ์ fiber ออกมาได้
ข้อสังเกตคือ
- เหมือนกับ Hexagonal Architecture ทุกองค์ประกอบจะไม่รู้จักกันเลย จะส่งเพียงแค่ตัวแทนเข้าไปจัดการ ขอแค่ตัวแทน "implement ตาม interface" ก็จะสามารถสื่อสารระหว่าง Module ด้วยกันได้
- มุมมองของทั้ง 3 ตัว Entities, Use case และ Adapter แยกจากกันชัดเจน
- Entities ดูแค่โครงสร้างข้อมูล
- Use case ดูแค่ logic การจัดการ Data
- Adapter หยิบส่วนของ Use case ไปจัดการต่อ (Gorm หยิบ
Order Repository
, Fiber หยิบOrder Service
)