Skip to main content

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

Screenshot 2565-11-04 at 21.40.34.png

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

Screenshot 2565-11-04 at 21.43.40.png

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

Screenshot 2565-11-04 at 21.44.46.png

ส่วนสุดท้ายนั้นเราไม่จำเป็นต้องสร้าง 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 ถูกต้องตามที่เราเขียนไว้เพื่อทดสอบ