Implementing Hexagonal Architecture
ส่วนนี้จะไม่มีการเขียน logic เพื่อให้ section นี้ไม่ยาวจนเกินไป
ก่อนอื่นการจะเปลี่ยนก่อนที่จะเปลี่ยน Architecture แบบชุ่ย(ที่ทำใน HttpHTTP handle บทความก่อน) ไปเป็นในบทความก่อนไปเป็น Hexagonal Architecture เราต้องสร้างหลายๆอย่างทำให้โค้ดเราต้องสร้างหลาย ๆ อย่างทำให้โค้ด base เปลี่ยนค่อนข้างเยอะแต่ก็จะขอให้ค่อยๆทำไปในความเร็วของตัวเองครับเปลี่ยนค่อนข้างเยอะ แต่ก็จะขอให้ค่อย ๆ ทำไปในความเร็วของตัวเองครับ
การจะทำการที่จะทำ Hexagonal Architecture อย่างที่บอกไปใน introduction เราจำเป็นต้องมี Port ที่เป็น interface ก่อน ซึ่ง interface ในที่นี้คือการกำหนดชื่อของ function รวมถึง parameter และ return statement ด้วย งั้นก่อนอื่นเรามาสร้าง folder เพื่อให้เข้าใจง่ายว่าส่วนไหนคือส่วนไหนของเพื่อให้เข้าใจโครงสร้างของ Hexagonal Architecture กันดีกว่า ซึ่งในที่นี้เราจำเป็นต้องสร้าง 3 folder นั้นคือนั่นคือ Repository, Service, และ Handler
เราจะเริ่มสร้างจากชั้นที่ลึกที่สุดที่เก็บข้อมูลไปถึงชั้นที่เอาข้อมูลที่ประมวณผลแล้วไปแสดงให้ user สามารถเรียกได้ หรือก็คือเราจะสร้าง repository -> service -> handler นั้นเองนั่นเอง
Repository
1. สร้างไฟล์ชื่อว่า user.go ขึ้นมาใน folder repository และโค้ดด้านในเราจะเก็บ struct (model ของข้อมูล) และ interface ที่เป็น "PortPort" "นั้นเองนั่นเอง
package repository
type User struct {
Id int64 `json:"id"`
Email string `json:"email"`
Password string `json:"password"`
Secret string `json:"secret"`
}
type UserRepository interface {
CreateUser(email string, password string, secret string) (*User, error)
CheckUser(email string) (*User, error)
GetUsers() ([]*User, error)
}
ซึ่ง model พวกนี้สามารถเปลี่ยนแปลงได้ตามที่เราออกแบบไว้ได้เลย แต่ส่วนมากจะนำข้อมูลที่มีอยู่ใน database มาเป็น struct นั้นเองนั่นเอง ส่วน interface ก็สามารถเปลี่ยนไปตามที่เราออกแบบได้เช่นกัน ซึ่ง method พวกนี้จะเป็นการ query, insert, update, หรือ delete ข้อมูลต่างๆที่เป็นขั้นสุดท้ายที่ติดต่อกับข้อมูลต่าง ๆ ที่เป็นขั้นสุดท้ายที่ติดต่อกับ database แต่ใน session นี้ขอให้มีเท่านี้ก่อนครับ
2. สร้างไฟล์ user_db.go ขึ้นมาใน folder เดียวกัน ซึ่งไฟล์นี้จะมาทำหน้าที่เป็น "adapter"Adapter" ฝั่งที่ติดต่อกับ database ของเรานั่นเอง
package repository
import (
"database/sql"
)
type userRepositoryDB struct {
db *sql.DB
}
func NewRepositoryDB(db *sql.DB) userRepositoryDB { // รับ instance database ที่เราจะใช้มาแล้วออกมาเป็น "Adapter" ของ repository
return userRepositoryDB{db: db}
}
func (u userRepositoryDB) CreateUser(email string, password string, secret string) (*User, error) {
// implement me
fmt.Print("User Inserted")
return nil, nil
}
func (u userRepositoryDB) CheckUser(email string) (*User, error) {
// implement me
fmt.Print("User Fetched")
return nil, nil
}
func (u userRepositoryDB) GetUsers() ([]*User, error) {
// implement me
fmt.Print("Users Fetched")
return nil, nil
}
ใน file นี้จะมี struct ที่เก็บ instance ของ Databasedatabase ไว้ด้วยเพื่อที่จะสามารถทำ operation ต่างๆได้ต่าง ๆ ได้ (ในครั้งนี้เราจะใช้ MySQL กัน) ซึ่ง struct นี้จะเป็น private ที่จะไม่สามารถสร้างได้ในไฟล์อื่นเฉยๆแต่ต้องเรียกผ่านที่จะไม่สามารถสร้างได้ในไฟล์อื่นเฉย ๆ แต่ต้องเรียกผ่าน function ที่สร้างไว้เท่านั้น และเนื่องจากเราจะทำให้ instance นี้เป็น Adapter เราจึงจำเป็นที่ต้องมี function ต่างๆเหมือนกับต่าง ๆ เหมือนกับ Port ของเราทั้งหมดด้วย
ผมทำการวาง print ไว้ด้วยเพื่อจะได้ไว้ด้วย เพื่อ test ได้โดยไม่ได้สร้าง logic มาก และเนื่องจากตอนนี้เรายังไม่ได้วาง port ไว้ เราเลยต้องไปสร้าง service ก่อนนั้นเองก่อนนั่นเอง
Service
1. สร้างไฟล์ชื่อ user.go ขึ้นมาใน folder service ส่วนโค้ดด้านในจะคล้ายกับส่วน Repository เลยแต่จะเปลี่ยนจากเลย แต่จะเปลี่ยนจาก Port ของส่วน Repository ไปเป็น Port ของ Handler แทน
package service
type User struct {
Id int64 `json:"id" bson:"_id"`
Email string `json:"email"`
}
type UserService interface {
SignUp(email string, password string) (*string, *string, error)
SignIn(email string, password string) (*UserService, error)
ListUsers() ([]*User, error)
}
2. สร้างไฟล์ชื่อ user_service.go ขึ้นมาใน folder เดียวกันซึ่งไฟล์นี้จะเป็นหน้าที่เก็บเดียวกัน ซึ่งไฟล์นี้จะเป็นหน้าที่เก็บ logic ต่างๆก่อนที่จะไปเรียกทำต่าง ๆ ก่อนที่จะไปเรียก operation database และก็เป็นที่วางและเป็นที่วาง Port ของ repositoryRepository ด้วยนั่นเองทำให้ด้วย ทำให้ userService สามารถเรียกใช้ function ที่มีอยู่ใน interface ได้ทั้งหมดนั่นเอง
package service
type userService struct {
repository repository.UserRepository // interface หรือ "Port" ของ repository
}
func NewUserService(userRepository repository.UserRepository) userService { // รับ "Adapter" ของ Repository
return userService{repository: userRepository}
}
func (s userService) SignUp(email string, password string) (*string, *string, error) {
// implement me
s.repository.CreateUser(email, password, "")
return nil, nil, nil
}
func (s userService) SignIn(email string, password string) (*UserService, error) {
// implement me
s.repository.GetUser(email)
return nil, nil
}
func (s userService) ListUsers() ([]*User, error) {
users, err := s.repository.GetUsers()
if err != nil {
return nil, err
}
var userResponse []*User
for _, user := range users {
userResponse = append(userResponse, &User{Id: user.Id, Email: user.Email})
}
return userResponse, nil
}
ในโค้ดนี้ผมได้มีตัวอย่างที่เรียกใช้ function GetUsers ด้วยซึ่งสังเกตุว่าเราเรียกได้โดยการเรียกด้วยซึ่งสังเกตว่าเราเรียกได้โดยการเรียก s.repository.{your_function} ได้เลย เพราะว่าตัว repositoryRepository ที่อยู่ใน struct userService มี interface ของ Repository เก็บไว้อยู่ และแน่นอน struct นี้จะเป็น private เช่นกันเพื่อจะไม่ให้สามารถสร้างได้ในไฟล์อื่นได้เฉยๆแต่ต้องเรียกผ่านเช่นกัน โดยจะไม่สามารถสร้างในไฟล์อื่นได้เฉย ๆ แต่ต้องเรียกผ่าน function ที่สร้างไว้เท่านั้นเหมือนเดิม
Handler
ส่วนสุดท้ายนั้นเราไม่จำเป็นต้องสร้าง Port เพิ่มแล้ว แต่สร้างแค่ Adapter ที่จะมาใส่ในที่จะนำมาใส่ใน "Port"Port ของ Service
package handler
type userHandler struct {
service service.UserService // interface หรือ "Port" จาก Service
}
func NewUserHandler(userSerivice service.UserService) userHandler { // รับ "Adapter" ของ Service
return userHandler{service: userSerivice}
}
func (h userHandler) SignUp(w http.ResponseWriter, r *http.Request) {
// implement me
h.service.SignUp("","")
}
func (h userHandler) SignIn(w http.ResponseWriter, r *http.Request) {
// implement me
h.service.SignIn("", "")
}
func (h userHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
// implement me
h.service.ListUsers()
}
ทีนี้ structure ของเราก็เสร็จเป็นที่เรียบร้อยแล้วครับ ภาพเมื่อนำไปใช้จริงก็เป็นประมาณนี้เมื่อนำไปใช้จริงเราจะได้โค้ดตามข้างบน
ในไฟล์ซึ่งในไฟล์ main.go จะเป็นดังนี้
package main
func main() {
s := &http.Server{
Addr: ":8080",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
db, err := sql.Open("mysql", "{your_sql_connection_string}")
if err != nil {
panic(err)
}
userRepository := repository.NewRepositoryDB(db)
userService := service.NewUserService(userRepository)
userHandler := handler.NewUserHandler(userService)
http.HandleFunc("/signup", userHandler.SignUp)
http.HandleFunc("/signin", userHandler.SignIn)
http.HandleFunc("/listuser", userHandler.ListUsers)
if err := s.ListenAndServe(); err != nil {
panic(err)
}
defer db.Close()
}
อย่างที่เห็นว่าเราก็จะสามารถเรียกจากโค้ดข้างต้นเราจะสามารถเรียก function เพื่อที่จะสร้าง "Adapter"Adapter (reposiptory) แล้วใส่ไปใน "Port"Port (function ของ service) เพื่อที่จะได้เพื่อให้ได้ "Adapter"Adapter (service) อีกตัวและไปใส่ในอีกตัว และนำไปใส่ใน Port (function ของ handler) อีกรอบเพื่อสามารถเรียกใช้ได้โดยง่ายดายแต่ขั้นตอนก่อนหน้านั้นจะเยอะอย่างที่เห็นครับอีกรอบ เพื่อสามารถเรียกใช้ได้ง่ายขึ้น แต่ขั้นตอนก่อนหน้านั้นจะเยอะอย่างที่เห็นครับ
ก่อนจะ run โค้ดผ่านเราต้องสร้าง instance ของ database ก่อน
เมื่อเราลอง callเมื่อเราลองเรียก function ก็จะได้ว่ามีผลลัพธ์ในก็จะได้ผลลัพธ์ใน console ถูกต้องตามที่เราเขียนไว้เพื่อทดสอบ


