listGoAPI Essential
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.goEntities
เริ่มจากส่วนแรกคือ 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.goIdea ของ 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
```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)
}
```order_repository.go
```go
package usecases
import "mike/entities"
type OrderRepository interface {
Save(order entities.Order) error
}
```อธิบาย code
order_repository.goทำการเก็บ Repository ของ Order ไว้ จึงมี code เพียงแค่ interface ของOrderRepositoryเท่านั้นและเป็นการบอกว่า Repository ต้องมี methodSave()มาด้วย จึงจะตรงตามOrderRepositoryorder_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 Servicehttp_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
```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
}
```http_order_handler.go
```go title="http_order_handler.go"
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)
