Skip to main content

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

order.go
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 คือ

  1. order_repository.go เป็นตัวแทนของ interface ของ Repository
  2. order_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

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)
}

อธิบาย code

  • order_repository.go ทำการเก็บ Repository ของ Order ไว้ จึงมี code เพียงแค่ interface ของ OrderRepository เท่านั้นและเป็นการบอกว่า Repository ต้องมี method Save() มาด้วย จึงจะตรงตาม OrderRepository
  • order_use_case.go ทำการเก็บคำสั่งสำหรับจัดการสร้าง order เอาไว้ซึ่งก็คือ CreateOrder() โดยจะทำการ implement ผ่าน method ของ usecase ไว้ในตัวชื่อ OrderUseCase และจะมี OrderService เป็นตัวแทนของการรับ OrderRepository จากภายนอก (ที่ส่งเข้ามาจาก Adapter) มาอีกที

Adapter

ต่อมา Adapter คือส่วนของการแปลงข้อมูลเพื่อทำการส่งไปยัง Use case เพื่อให้ Use case สามารถจัดการต่อได้ถูกได้จะมีทั้งหมด 2 ส่วน (โดยจะแยกออกเป็น 2 files) คือ

  1. gorm_order_repository.go (GORM Order Repository) คือส่วนที่จะเก็บ function สำหรับจัดการ database เอาไว้ โดยจะเก็บคำสั่งที่เกี่ยวข้องกับการ query database ไว้ โดยจะต้อง implement ตาม spec ของ interface ที่กำหนดไว้ใน Order Service
  2. 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 ตัว

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
}

อธิบาย code เพิ่มเติม

  • gorm_order_repository.go ทำการ implement Save() โดยทำการใส่ query ที่เกี่ยวข้องกับการจัดการ Order เข้าไป (ซึ่งก็คือคำสั่ง Gorm)
  • http_order_handler.go ทำการ implement HttpOrderHandler โดยเป็นการรับ ตัวแทนของ Use case มาเพื่อใช้คำสั่งภายใน Order Service (คำสั่งสำหรับการสร้าง Order) และทำการสร้าง method CreateOrder() เพื่อใช้สำหรับการเรียกใช้งานจากฝั่งของ 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 เข้ากับ endpoint POST /order เพื่อให้สามารถเรียกใช้งาน CreateOrder() จากใน Adapter ได้

เมื่อลอง run project ด้วย go run main.go ดู ก็จะสามารถ run ผลลัพธ์ fiber ออกมาได้

hex-result

ข้อสังเกตคือ

  1. เหมือนกับ Hexagonal Architecture ทุกองค์ประกอบจะไม่รู้จักกันเลย จะส่งเพียงแค่ตัวแทนเข้าไปจัดการ ขอแค่ตัวแทน "implement ตาม interface" ก็จะสามารถสื่อสารระหว่าง Module ด้วยกันได้
  2. มุมมองของทั้ง 3 ตัว Entities, Use case และ Adapter แยกจากกันชัดเจน
  • Entities ดูแค่โครงสร้างข้อมูล
  • Use case ดูแค่ logic การจัดการ Data
  • Adapter หยิบส่วนของ Use case ไปจัดการต่อ (Gorm หยิบ Order Repository, Fiber หยิบ Order Service)