Published on

Design Patterns ที่นิยมใช้ในภาษา Go

Authors

Design Patterns ที่นิยมใช้ในภาษา Go

การออกแบบซอฟต์แวร์ที่ดีต้องคำนึงถึงความสามารถในการขยาย (scalability) และการบำรุงรักษา (maintainability) ซึ่ง Design Patterns เป็นหนึ่งในแนวทางที่ช่วยให้โค้ดของเรามีโครงสร้างที่ชัดเจน แยกส่วนความรับผิดชอบ (Separation of Concerns) ได้ดีขึ้น และลดการทำซ้ำ (DRY Principle) ในบทความนี้ เราจะมาทำความรู้จักกับ 7 Design Patterns ที่นิยมใช้ในภาษา Go


1. Factory Pattern

Factory Pattern เป็นการสร้างวัตถุ (Object) โดยซ่อนรายละเอียดของการสร้างไว้ในฟังก์ชันหรือเมธอด ใช้ในกรณีที่การสร้างวัตถุต้องมีเงื่อนไขที่ซับซ้อน หรือมีหลายประเภท

✅ ข้อดี

  • แยกความรับผิดชอบ (Separation of Concerns): แยกส่วนของการสร้างอ็อบเจกต์ออกจากส่วนที่ใช้งาน ทำให้การเปลี่ยนแปลงวิธีการสร้างอ็อบเจกต์สามารถทำได้โดยไม่กระทบโค้ดที่ใช้งาน
  • ลดการจับคู่ (Coupling): Client ไม่ต้องทราบรายละเอียดของโครงสร้างหรือชนิดของอ็อบเจกต์ที่ถูกสร้าง ลดการผูกมัดกับคลาสที่เฉพาะเจาะจง
  • รองรับการขยายตัว (Extensibility): เพิ่มประเภทใหม่ ๆ ของอ็อบเจกต์ได้ง่าย โดยการเพิ่มการรองรับในฟังก์ชัน Factory โดยไม่กระทบโค้ดเดิม
  • ทำให้โค้ดอ่านง่าย: การแยกการสร้างอ็อบเจกต์ออกจากการใช้งาน ช่วยให้โค้ดมีความชัดเจนและง่ายต่อการดูแลรักษา

❌ ข้อเสีย

  • ความซับซ้อนที่เพิ่มขึ้น: ในบางกรณีที่โปรเจกต์มีความเรียบง่าย การใช้ Factory Pattern อาจทำให้เกิดความซับซ้อนและจำนวนไฟล์ที่เพิ่มขึ้นโดยไม่จำเป็น
  • การซ่อนรายละเอียดอาจเป็นอุปสรรค: หากมีการดีบักหรือแก้ไขปัญหาการสร้างอ็อบเจกต์ การซ่อนรายละเอียดไว้ใน Factory อาจทำให้การติดตามปัญหาทำได้ยากขึ้นเล็กน้อย

🎯 Use Case ที่เหมาะสม

  • ระบบที่มีการสร้างอ็อบเจกต์หลายประเภทที่มี interface เดียวกัน: เช่น ระบบเกมที่มีตัวละครหรืออาวุธที่แตกต่างกัน
  • การประมวลผลคำสั่งที่หลากหลาย: เช่น ระบบที่รับคำสั่งต่าง ๆ แล้วต้องสร้างอ็อบเจกต์ที่แตกต่างกันตามประเภทคำสั่ง
  • การทำงานกับข้อมูลที่มีรูปแบบต่าง ๆ: เช่น การอ่านไฟล์หลายรูปแบบ (JSON, XML, CSV) และต้องแปลงเป็นอ็อบเจกต์ภายในโปรแกรม
  • เมื่อการสร้างอ็อบเจกต์มีความซับซ้อน ต้องจัดการ dependency

🔢 วิธีการสร้าง

  1. สร้าง interface สำหรับวัตถุที่ต้องการผลิต
  2. สร้าง struct ที่ implement interface เหล่านั้น
  3. สร้างฟังก์ชัน Factory เพื่อคืนค่าตามประเภทที่ร้องขอ

ตัวอย่าง:

สมมติว่าเราต้องสร้าง Notification หลายประเภท เช่น Email และ SMS

package main

import "fmt"

// Notification interface
type Notification interface {
	Send(message string)
}

// EmailNotification implements Notification
type EmailNotification struct{}

func (e *EmailNotification) Send(message string) {
	fmt.Println("Sending Email:", message)
}

// SMSNotification implements Notification
type SMSNotification struct{}

func (s *SMSNotification) Send(message string) {
	fmt.Println("Sending SMS:", message)
}

// Factory function
func NewNotification(notificationType string) Notification {
	switch notificationType {
	case "email":
		return &EmailNotification{}
	case "sms":
		return &SMSNotification{}
	default:
		panic("Invalid notification type")
	}
}

func main() {
	// ใช้งาน Factory
	email := NewNotification("email")
	email.Send("Hello via Email!")

	sms := NewNotification("sms")
	sms.Send("Hello via SMS!")
}

ผลลัพธ์:

Sending Email: Hello via Email!
Sending SMS: Hello via SMS!

2. Functional Options

Functional Options ไม่ใช่ Design Pattern แต่เป็นแนวทาง (idiom) การเขียนโค้ดใน Go ที่ใช้ฟังก์ชันเป็นอาร์กิวเมนต์เพื่อกำหนดค่าคอนฟิกของ struct แทนการใช้ constructor ที่มีพารามิเตอร์จำนวนมาก

ปกติแล้ว ใน Go เรามักจะใช้ constructor function (NewXXX()) ในการสร้าง struct และกำหนดค่าต่าง ๆ แต่เมื่อ struct มีพารามิเตอร์จำนวนมาก constructor จะมีข้อเสีย เช่น

  • ต้องส่งพารามิเตอร์ทั้งหมด แม้ว่าบางค่าจะไม่จำเป็น
  • การเพิ่มพารามิเตอร์ใหม่อาจทำให้ต้องเปลี่ยนโค้ดที่มีอยู่

Functional Options แก้ปัญหานี้โดยใช้ฟังก์ชันเป็นตัวกำหนดค่าพารามิเตอร์แทน

✅ ข้อดี

  • เพิ่มความยืดหยุ่น – สามารถกำหนดค่าที่ต้องการโดยไม่ต้องใช้ constructor ที่รับพารามิเตอร์จำนวนมาก
  • อ่านง่าย – Code ที่ใช้งานดูสะอาดขึ้น เพราะสามารถเลือก option ที่ต้องการได้
  • รองรับค่า Default – สามารถกำหนดค่าเริ่มต้นได้ง่าย

❌ ข้อเสีย

  • Debug ยากขึ้น – ค่าไม่ถูกกำหนดตายตัว ต้องใช้ฟังก์ชัน callback
  • Performance Overhead เล็กน้อย – เนื่องจากต้องใช้ฟังก์ชันและ loop ในการ apply options

🎯 Use Case ที่เหมาะสม

  • เมื่อ struct มีค่าคอนฟิกหลายตัว อย่างพวก HTTP Client, Database Connection, Server Configuration เช่น สร้าง REST API Client ที่รองรับค่า Base URL, Timeout, Retry Policy, Headers โดยไม่ต้องเปลี่ยน constructor
  • เมื่อใช้ร่วมกับ Factory Pattern เพื่อสร้าง object แบบ flexible
  • เมื่อโค้ดต้องการรองรับค่า default และสามารถขยายได้ในอนาคต

🔢 วิธีการสร้าง

  1. ใช้ ฟังก์ชัน type เป็น Option
  2. ใช้ variadic function (...options) รับ options หลายตัว
  3. กำหนดค่า default
  4. ใช้ loop เพื่อ apply options
type Option func(*Config)

func NewConfig(options ...Option) *Config {
    cfg := &Config{}  // ค่า default
    for _, opt := range options {
        opt(cfg) // Apply options
    }
    return cfg
}

ตัวอย่าง วิธีปกติ (Constructor แบบดั้งเดิม):

package main

import (
	"fmt"
	"net/http"
	"time"
)

// APIClient เก็บค่าการตั้งค่า HTTP Client
type APIClient struct {
	BaseURL string
	Client  *http.Client
}

// NewAPIClient สร้าง APIClient
func NewAPIClient(baseURL string, timeout time.Duration) *APIClient {
	return &APIClient{
		BaseURL: baseURL,
		Client:  &http.Client{Timeout: timeout},
	}
}

func main() {
	client := NewAPIClient("https://api.example.com", 10*time.Second)
	fmt.Println("Base URL:", client.BaseURL)
	fmt.Println("Timeout:", client.Client.Timeout)
}

ตัวอย่าง ใช้ Functional Options แทน Constructor:

package main

import (
	"fmt"
	"net/http"
	"time"
)

// APIClient เก็บค่าการตั้งค่า HTTP Client
type APIClient struct {
	BaseURL string
	Client  *http.Client
}

// APIClientOption คือฟังก์ชันที่ใช้ปรับแต่ง APIClient
type APIClientOption func(*APIClient)

// WithTimeout กำหนดค่า timeout
func WithTimeout(timeout time.Duration) APIClientOption {
	return func(c *APIClient) {
		c.Client.Timeout = timeout
	}
}

// WithBaseURL กำหนดค่า Base URL
func WithBaseURL(url string) APIClientOption {
	return func(c *APIClient) {
		c.BaseURL = url
	}
}

// NewAPIClient ใช้ Functional Options
func NewAPIClient(opts ...APIClientOption) *APIClient {
	client := &APIClient{
		BaseURL: "https://default.example.com", // ค่า default
		Client:  &http.Client{Timeout: 5 * time.Second},
	}

	// Apply options
	for _, opt := range opts {
		opt(client)
	}

	return client
}

func main() {
	// ใช้ค่า default
	client1 := NewAPIClient()
	fmt.Println("Client 1:", client1.BaseURL, client1.Client.Timeout)

	// กำหนดค่า Base URL และ Timeout เอง
	client2 := NewAPIClient(WithBaseURL("https://api.example.com"), WithTimeout(10*time.Second))
	fmt.Println("Client 2:", client2.BaseURL, client2.Client.Timeout)
}

ตารางเปรียบเทียบ:

FeatureFunctional OptionsConstructor แบบปกติ
ความยืดหยุ่น✅ สูง (เพิ่มพารามิเตอร์ได้ง่าย)❌ ต้องเปลี่ยน constructor ทุกครั้ง
อ่านง่าย✅ ใช้เฉพาะค่าที่ต้องการ❌ ต้องส่งค่าทั้งหมด
ค่า Default✅ ใช้ค่า default ได้ง่าย❌ ต้องกำหนดค่าเองเสมอ
ขยาย (Extensibility)✅ เพิ่ม options ใหม่ได้ง่าย❌ ต้องแก้ไข constructor
Debug❌ ยากขึ้น เพราะค่าถูกกำหนดแบบ dynamic✅ ง่ายกว่า

Functional Options เป็นแนวทางการออกแบบที่ช่วยให้โค้ดมีความยืดหยุ่นมากขึ้น ลดการใช้ constructor ที่ต้องรับพารามิเตอร์จำนวนมาก และช่วยให้โค้ดอ่านง่ายขึ้น


3. Repository Pattern

Repository Pattern เป็นการแยกการเข้าถึงข้อมูล (Data Access Layer) ออกจาก Business Logic เพื่อให้โค้ดสะอาดและเปลี่ยนแปลงฐานข้อมูลได้ง่าย

✅ ข้อดี

  • แยกความรับผิดชอบ: ทำให้ business logic ไม่ต้องยุ่งเกี่ยวกับรายละเอียดของการติดต่อกับฐานข้อมูลหรือแหล่งข้อมูลอื่น ๆ
  • ลด coupling: Client ทำงานกับ interface ที่กำหนดไว้ ทำให้สามารถเปลี่ยน implementation ของ repository ได้โดยไม่กระทบกับโค้ดที่ใช้งาน
  • รองรับ unit testing: การแยกส่วนเข้าถึงข้อมูลออกจาก business logic ทำให้สามารถ mocking repository เพื่อทดสอบได้ง่ายขึ้น
  • ความยืดหยุ่นในการเปลี่ยนแปลง: เมื่อมีการเปลี่ยนแปลงในแหล่งข้อมูลหรือวิธีการเข้าถึงข้อมูล สามารถแก้ไขเฉพาะ repository ได้โดยไม่ต้องแก้ไขส่วนอื่นของระบบ

❌ ข้อเสีย

  • เพิ่มชั้นการแสดงผล: ในโปรเจกต์ที่มีความซับซ้อนน้อย การเพิ่ม repository อาจทำให้โค้ดดูมีชั้นที่มากเกินความจำเป็น
  • ความซับซ้อนในการออกแบบ: การออกแบบ interface และการแยกส่วนการเข้าถึงข้อมูลให้เหมาะสมอาจต้องใช้เวลาและความเข้าใจในแนวทางของระบบ

🎯 Use Case ที่เหมาะสม

  • แอปพลิเคชันที่มีการเข้าถึงข้อมูลจากหลายแหล่ง: เช่น การอ่านข้อมูลจากฐานข้อมูล หลาย ๆ ตาราง หรือแม้กระทั่งการดึงข้อมูลจาก API ภายนอก
  • ระบบที่ต้องการทดสอบ business logic โดยไม่ผูกกับฐานข้อมูล: การใช้ repository ช่วยให้สามารถสร้าง mock object เพื่อนำไปใช้ในการทดสอบได้
  • ระบบที่มีการเปลี่ยนแปลงแหล่งข้อมูลในอนาคต: เช่น การเปลี่ยนจากฐานข้อมูล SQL ไปยัง NoSQL หรือการปรับเปลี่ยนวิธีการเข้าถึงข้อมูลโดยไม่กระทบกับ business logic

🔢 วิธีการสร้าง

  1. สร้าง Model ที่ใช้แทนข้อมูล
  2. สร้าง interface สำหรับการเข้าถึงข้อมูล
  3. สร้าง implementation ของ interface นั้น

ตัวอย่าง:

ใช้ In-Memory repository ในระบบจัดการผู้ใช้งาน

package main

import (
	"fmt"
)

// User model สำหรับข้อมูลผู้ใช้งาน
type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

// UserRepository interface สำหรับการเข้าถึงข้อมูลผู้ใช้งาน
type UserRepository interface {
	GetUserByID(id int) (*User, error)
	CreateUser(user User) error
}

// InMemoryUserRepository เป็น implementation ที่เก็บข้อมูลใน map
type InMemoryUserRepository struct {
	data map[int]User
}

func NewInMemoryUserRepository() UserRepository {
	return &InMemoryUserRepository{
		data: make(map[int]User),
	}
}

func (r *InMemoryUserRepository) GetUserByID(id int) (*User, error) {
	user, ok := r.data[id]
	if !ok {
		return nil, fmt.Errorf("User not found")
	}
	return &user, nil
}

func (r *InMemoryUserRepository) CreateUser(user User) error {
	if _, exists := r.data[user.ID]; exists {
		return fmt.Errorf("User already exists")
	}
	r.data[user.ID] = user
	return nil
}

func main() {
	repo := NewInMemoryUserRepository()
	// สร้างผู้ใช้งานตัวอย่าง
	repo.CreateUser(User{ID: 1, Name: "Alice"})

	user, err := repo.GetUserByID(1)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Found User:", user.Name)
}

ผลลัพธ์:

Found User: Alice

4. Singleton Pattern

Singleton Pattern เป็นรูปแบบการออกแบบ (Creational Pattern) ที่กำหนดให้มี instance ของคลาส (หรือ struct ใน Go) เพียงตัวเดียวเท่านั้นในระบบ พร้อมทั้งให้ทุกส่วนของโปรแกรมสามารถเข้าถึง instance นั้นได้อย่างมีระบบ โดยทั่วไปแล้วเราจะใช้กลไกอย่าง sync.Once เพื่อให้แน่ใจว่าการสร้าง instance นั้นเกิดขึ้นเพียงครั้งเดียวในสภาวะแวดล้อมที่ทำงานแบบ concurrent

✅ ข้อดี

  • ควบคุม instance ได้อย่างชัดเจน: ช่วยให้แน่ใจว่าจะมี instance เพียงตัวเดียวในระบบ ช่วยป้องกันปัญหาการสร้าง instance ซ้ำในโปรแกรมที่มีการทำงานแบบ concurrent
  • การเข้าถึงที่ง่ายและทั่วถึง: ทุกส่วนของโปรแกรมสามารถเรียกใช้ instance เดียวกันได้จากจุดศูนย์กลาง ทำให้การจัดการ state ร่วมเป็นไปอย่างมีประสิทธิภาพ
  • ประหยัดทรัพยากร: เนื่องจากมีการสร้าง instance เพียงครั้งเดียว จึงช่วยลดภาระของการสร้างอ็อบเจกต์ซ้ำ ๆ ที่อาจมีค่าใช้จ่ายสูงในการสร้างหรือเชื่อมต่อทรัพยากรต่าง ๆ

❌ ข้อเสีย

  • การทดสอบ (Unit Testing) อาจซับซ้อน: การมี instance เพียงตัวเดียวอาจทำให้การทดสอบในระดับแยกส่วนยากขึ้น เนื่องจาก state ที่ถูกแชร์กันในหลายส่วนของโปรแกรม
  • เกิดความเชื่อมโยง (Tight Coupling): เมื่อหลายส่วนของระบบพึ่งพา Singleton โดยตรง การเปลี่ยนแปลงใน Singleton อาจส่งผลกระทบต่อส่วนอื่น ๆ ได้
  • ข้อควรระวังในสภาวะ concurrent: แม้ว่า Go จะมีเครื่องมืออย่าง sync.Once แต่การใช้งาน Singleton ในระบบที่มีการทำงานพร้อมกันสูง จำเป็นต้องออกแบบอย่างระมัดระวังเพื่อหลีกเลี่ยงปัญหาการแข่งขัน (race condition)

🎯 Use Case ที่เหมาะสม

  • การจัดการ configuration: เช่น โหลดไฟล์ configuration เพียงครั้งเดียวแล้วแชร์ข้อมูลนั้นไปยังทุกส่วนของโปรแกรม
  • การเชื่อมต่อฐานข้อมูล: เมื่อเชื่อมต่อกับฐานข้อมูลควรสร้าง connection pool เพียงครั้งเดียวและให้ทุกส่วนของโปรแกรมใช้ instance เดียวกัน
  • การจัดการ logger: ใช้ logger instance เพียงตัวเดียวเพื่อเก็บ log ของโปรแกรม ทำให้การจัดการ log มีความเป็นระเบียบและสามารถควบคุมระดับ log ได้ง่ายขึ้น

🔢 วิธีการสร้าง

  1. สร้างตัวแปรที่เก็บ instance
  2. ใช้ sync.Once เพื่อสร้างเพียงครั้งเดียว

ตัวอย่าง:

การเก็บ configuration ของแอปพลิเคชัน

package main

import (
	"fmt"
	"net/http"
	"sync"
)

// Config เก็บ configuration ของแอปพลิเคชัน
type Config struct {
	AppName string `json:"app_name"`
	Port    int    `json:"port"`
}

var configInstance *Config
var once sync.Once

func GetConfig() *Config {
	once.Do(func() {
		configInstance = &Config{
			AppName: "My REST API",
			Port:    8080,
		}
	})
	return configInstance
}

func main() {
	config := GetConfig()
	config2 := GetConfig()

	fmt.Println(config == config2) // true

	http.ListenAndServe(fmt.Sprintf(":%d", config.Port), nil)
}

ผลลัพธ์:

true

5. Builder Pattern

Builder Pattern ช่วยให้การสร้างอ็อบเจ็กต์ที่ซับซ้อนทำได้ง่ายขึ้น ด้วยการแยกขั้นตอนการสร้างออกจากกัน ร่วมกับ Fluent Interface ที่เป็นแนวทางการออกแบบ API ที่ให้แต่ละ method ที่เรียกใช้งานสามารถคืนค่า instance ของ object เดิม (หรือ builder ในที่นี้) กลับไป ทำให้สามารถเรียก chain method ซึ่งจะช่วยให้โค้ดมีความกระชับและเข้าใจได้ง่าย

// ตัวอย่างการเรียกใช้งานแบบ chain method
builder.SetCPU("Intel i9").SetRAM(32).Build()

✅ ข้อดี

  • ความชัดเจน: การแบ่งขั้นตอนการสร้างอ็อบเจกต์ออกเป็นส่วน ๆ ช่วยให้โค้ดอ่านและเข้าใจง่าย
  • ยืดหยุ่น: รองรับการสร้างอ็อบเจกต์ที่มีพารามิเตอร์หลายตัวและอ็อบเจกต์ที่ซับซ้อน
  • รองรับ Method Chaining: ด้วย Fluent Interface ทำให้สามารถเรียกใช้งานแบบ chain method ซึ่งช่วยลดบรรทัดโค้ดและเพิ่มความเป็นธรรมชาติในการอ่าน
  • การบำรุงรักษาง่าย: แยก logic ของการสร้างออกจาก business logic ทำให้แก้ไขหรือขยายได้ง่าย

❌ ข้อเสีย

  • เพิ่มความซับซ้อน: ในโปรเจกต์ที่มีความซับซ้อนน้อย การนำ Builder Pattern มาใช้อาจทำให้โค้ดดูมีชั้นมากเกินความจำเป็น
  • ค่าใช้จ่ายในการออกแบบ: อาจต้องใช้เวลาพอสมควรในการออกแบบ Builder และจัดการกับ method chaining ให้เหมาะสม
  • Overhead: สำหรับอ็อบเจกต์ที่สร้างได้ง่าย การใช้ Builder Pattern อาจเพิ่ม overhead ที่ไม่จำเป็น

🎯 Use Case ที่เหมาะสม

  • อ็อบเจกต์ที่มีพารามิเตอร์มาก: เช่น การสร้าง configuration, การตั้งค่าระบบ หรือการสร้างอ็อบเจกต์ที่มีหลายส่วนประกอบที่สามารถกำหนดค่าได้
  • การอ่านโค้ดที่ต้องการความชัดเจน: เมื่อการสร้างอ็อบเจกต์ต้องการความเข้าใจในแต่ละขั้นตอน Fluent Interface จะช่วยให้โค้ดดูอ่านง่าย
  • ระบบที่มีการเปลี่ยนแปลงค่า parameter บ่อย: สามารถปรับเปลี่ยนหรือเพิ่ม method ใน Builder ได้โดยไม่กระทบส่วนที่ใช้งาน

🔢 วิธีการสร้าง

  1. สร้าง struct ของ builder
  2. เพิ่มเมธอดสำหรับกำหนดค่าทีละขั้นตอน
  3. ใช้เมธอด Build() เพื่อคืนค่า

ตัวอย่าง:

package main

import "fmt"

// Response โครงสร้างสำหรับ JSON response
type Response struct {
	Status  int         `json:"status"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

// ResponseBuilder ใช้สำหรับสร้าง response ด้วยวิธี chain method
type ResponseBuilder struct {
	response Response
}

func NewResponseBuilder() *ResponseBuilder {
	return &ResponseBuilder{}
}

func (b *ResponseBuilder) SetStatus(status int) *ResponseBuilder {
	b.response.Status = status
	return b
}

func (b *ResponseBuilder) SetMessage(message string) *ResponseBuilder {
	b.response.Message = message
	return b
}

func (b *ResponseBuilder) SetData(data interface{}) *ResponseBuilder {
	b.response.Data = data
	return b
}

func (b *ResponseBuilder) Build() Response {
	return b.response
}

func main() {
	// สร้าง Response โดยใช้ Builder และ chain method ด้วย Fluent Interface
	response := NewResponseBuilder().
		SetStatus(200).
		SetMessage("Success").
		SetData(map[string]string{"key": "value"}).
		Build()

	fmt.Printf("Response: %+v\n", response)
}

ผลลัพธ์:

Response: {Status:200 Message:Success Data:map[key:value]}

6. Adapter Pattern

Adapter Pattern เป็นการทำให้สอง interface ที่แตกต่างกันสามารถทำงานร่วมกันได้ ซึ่งช่วยให้เราสามารถใช้โค้ดเก่าร่วมกับโค้ดใหม่ได้ โดยไม่ต้องเปลี่ยนแปลงโครงสร้างเดิม

✅ ข้อดี

  • ช่วยให้โค้ดทำงานร่วมกันได้ – สามารถใช้โค้ดเก่ากับ API ใหม่ได้โดยไม่ต้องเปลี่ยนแปลงโค้ดเดิม
  • ลดการแก้ไขโค้ดที่มีอยู่ – เหมาะสำหรับการใช้ไลบรารีหรือโมดูลที่ไม่มีโค้ดต้นฉบับให้แก้ไข
  • ปรับปรุงการอ่านโค้ด – แทนที่จะเขียนโค้ดแปลงข้อมูลในหลาย ๆ ที่ การใช้ Adapter ทำให้โค้ดเป็นระบบมากขึ้น

❌ ข้อเสีย

  • เพิ่มความซับซ้อน – ต้องสร้างโครงสร้าง Adapter เพิ่มขึ้นมา ทำให้มีโค้ดเพิ่มขึ้น
  • อาจมี overhead – ในบางกรณี การแปลงข้อมูลหรือเรียก method ผ่าน Adapter อาจทำให้ประสิทธิภาพลดลง

🎯 Use Case ที่เหมาะสม

  • ต้องการ ใช้ไลบรารีเก่ากับโค้ดใหม่ โดยไม่ต้องแก้ไขไลบรารี
  • ต้องการ รองรับหลาย API ที่แตกต่างกัน โดยใช้ Adapter เป็นตัวกลาง
  • ต้องการ แปลงข้อมูลระหว่างระบบ เช่น แปลง JSON จาก API ภายนอกให้ตรงกับโครงสร้างของแอปพลิเคชัน

🔢 วิธีการสร้าง

  1. สร้าง interface ต้นทางและปลายทาง
  2. สร้าง Adapter เพื่อแปลง interface

ตัวอย่าง:

นำข้อมูลจากบริการสภาพอากาศภายนอกมาแปลงให้ตรงกับโครงสร้างที่เราต้องการ

package main

import "fmt"

// WeatherResponse คือโครงสร้างที่ API ของเราต้องการ
type WeatherResponse struct {
	Temperature float64 `json:"temperature"`
	Condition   string  `json:"condition"`
}

// ExternalWeatherService จำลองบริการภายนอกที่ให้ข้อมูลในรูปแบบ map
type ExternalWeatherService struct{}

func (e *ExternalWeatherService) GetWeather(location string) map[string]interface{} {
	// จำลองการเรียก API ภายนอก
	return map[string]interface{}{
		"temp": 30.5,
		"desc": "Sunny",
	}
}

// WeatherServiceAdapter ปรับข้อมูลจาก ExternalWeatherService ให้เป็น WeatherResponse
type WeatherServiceAdapter struct {
	external *ExternalWeatherService
}

func (a *WeatherServiceAdapter) GetWeather(location string) WeatherResponse {
	data := a.external.GetWeather(location)
	return WeatherResponse{
		Temperature: data["temp"].(float64),
		Condition:   data["desc"].(string),
	}
}

func main() {
	external := &ExternalWeatherService{}
	adapter := &WeatherServiceAdapter{external: external}
	weather := adapter.GetWeather("Phuket")

	fmt.Printf("Temperature: %.2f\n", weather.Temperature)
	fmt.Printf("Condition: %s\n", weather.Condition)
}

ผลลัพธ์:

Temperature: 30.50
Condition: Sunny

7. Decorator Pattern

Decorator Pattern เป็นการเพิ่มความสามารถ (behavior) ให้กับอ็อบเจ็กต์โดยไม่ต้องแก้ไขโค้ดเดิมหรือสร้างคลาสใหม่ที่สืบทอดมาจากอ็อบเจ็กต์ต้นฉบับต้นฉบับ ผ่านการห่อหุ้ม (wrap) อ็อบเจ็กต์เดิมด้วยอ็อบเจ็กต์ใหม่

✅ ข้อดี

  • ยืดหยุ่น – สามารถเพิ่มฟังก์ชันใหม่ได้โดยไม่ต้องแก้ไขโค้ดเดิม
  • หลีกเลี่ยงการใช้ Inheritance – ลดการสร้างคลาสที่ซับซ้อนจากการสืบทอดหลายชั้น
  • แยกความรับผิดชอบ (Separation of Concerns) – แต่ละ decorator มีหน้าที่เฉพาะ เช่น logging, caching, validation
  • สามารถใช้ร่วมกันได้ – Decorator หลายตัวสามารถรวมกันเพื่อสร้างพฤติกรรมใหม่ ๆ ได้

❌ ข้อเสีย

  • เพิ่มความซับซ้อน – มีหลายเลเยอร์ของ decorator อาจทำให้โค้ดอ่านยาก
  • ต้องมีการจัดการหลายระดับ – ต้องแน่ใจว่าเรียก decorator ในลำดับที่ถูกต้อง

🎯 Use Case ที่เหมาะสม

  • เพิ่มการล็อก (Logging) ให้กับฟังก์ชันที่มีอยู่
  • เพิ่มการแคช (Caching) สำหรับ API call หรือ database query
  • เพิ่มการตรวจสอบสิทธิ์ (Authorization) ก่อนเรียกใช้งานบางฟังก์ชัน
  • เพิ่มการจำกัดความเร็ว (Rate Limiting) สำหรับ API หรือ service call

🔢 วิธีการสร้าง

  1. สร้าง interface หลัก
  2. สร้าง Decorator ที่ implement interface

ตัวอย่าง:

เพิ่มการล็อก (Logging) ให้กับฟังก์ชันที่มีอยู่

package main

import "fmt"

// Notifier interface
type Notifier interface {
	Send(message string)
}

// BasicNotifier (Concrete Component)
type BasicNotifier struct{}

func (n *BasicNotifier) Send(message string) {
	// จำลองการทำงาน
	fmt.Println("Sending message")
}

// LoggingDecorator (Decorator)
type LoggingDecorator struct {
	notifier Notifier
}

func NewLoggingDecorator(n Notifier) Notifier {
	return &LoggingDecorator{notifier: n}
}

func (l *LoggingDecorator) Send(message string) {
	fmt.Println("[Log] Message:", message)
	l.notifier.Send(message)
}

// EncryptionDecorator (Decorator)
type EncryptionDecorator struct {
	notifier Notifier
}

func NewEncryptionDecorator(n Notifier) Notifier {
	return &EncryptionDecorator{notifier: n}
}

func (e *EncryptionDecorator) Send(message string) {
	encryptedMessage := "Encrypted(" + message + ")"
	e.notifier.Send(encryptedMessage)
}

func main() {
	notifier := &BasicNotifier{}

	// เพิ่ม Logging
	logNotifier := NewLoggingDecorator(notifier)

	// เพิ่ม Encryption
	secureNotifier := NewEncryptionDecorator(logNotifier)

	secureNotifier.Send("Hello, World!")
}

ผลลัพธ์:

[Log] Message: Encrypted(Hello, World!)
Sending message

8. Worker Pool Pattern

Worker Pool Pattern ใช้สำหรับการประมวลผลงานจำนวนมาก โดยการกระจายงานไปยัง Worker หลายตัวทำพร้อมกัน (concurrency) ทำให้ประสิทธิภาพของระบบเพิ่มขึ้น

✅ ข้อดี

  • ควบคุมจำนวน goroutines – ลดการใช้หน่วยความจำและการบริหารจัดการ goroutines ที่มากเกินไป
  • ปรับปรุงประสิทธิภาพ – งานสามารถประมวลผลพร้อมกันได้แบบขนาน
  • หลีกเลี่ยงปัญหา resource exhaustion – ป้องกันการสร้าง goroutines มากเกินไปจนกิน CPU หรือ RAM
  • สามารถใช้งานได้กับงานที่ต้องทำซ้ำ ๆ – เช่น การประมวลผลไฟล์, การร้องขอ API, หรือการทำ data processing

❌ ข้อเสีย

  • เพิ่มความซับซ้อน – ต้องออกแบบโค้ดให้สามารถรับ-ส่งงานระหว่าง workers และ main thread ได้อย่างถูกต้อง
  • ต้องจัดการการปิด worker pool – หากไม่ปิด properly อาจเกิดปัญหา memory leak หรือ deadlock

🎯 Use Case ที่เหมาะสม

  • การประมวลผลข้อมูลจำนวนมาก (เช่น แปลงไฟล์, แปลงรูปภาพ)
  • การโหลดหรือดึงข้อมูลจาก API แบบขนาน
  • การส่งอีเมลจำนวนมาก
  • การทำงานที่ต้องการจำกัดจำนวน concurrent workers

🔢 วิธีการสร้าง

  1. กำหนดจำนวน Worker และงาน (Jobs)
  2. ใช้ Channel ในการรับส่งข้อมูลระหว่าง Worker
  3. ใช้ go ในการสร้าง Goroutine สำหรับ Worker แต่ละตัว

ตัวอย่าง:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

// Worker function
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()
	for job := range jobs {
		fmt.Printf("Worker %d กำลังประมวลผลงาน %d\n", id, job)
		time.Sleep(time.Duration(rand.Intn(3)) * time.Second) // จำลองเวลาประมวลผล
		fmt.Printf("Worker %d จบการทำงาน %d\n", id, job)
		results <- job * 2
	}
}

func main() {
	numWorkers := 3   // จำนวน worker ที่ต้องการใช้
	numJobs := 5     // จำนวนงานที่ต้องประมวลผล

	jobs := make(chan int, numJobs)
	results := make(chan int, numJobs)
	var wg sync.WaitGroup

	// สร้าง workers
	for i := 1; i <= numWorkers; i++ {
		wg.Add(1)
		go worker(i, jobs, results, &wg)
	}

	// ส่งงานเข้า queue
	for j := 1; j <= numJobs; j++ {
		jobs <- j
	}
	close(jobs) // ปิด channel เพื่อบอก worker ว่าไม่มีงานใหม่แล้ว

	// รอให้ worker ทำงานเสร็จทั้งหมด
	wg.Wait()
	close(results)

	// แสดงผลลัพธ์
	for result := range results {
		fmt.Println("ผลลัพธ์:", result)
	}
}

ผลลัพธ์ (ลำดับอาจเปลี่ยนตามการประมวลผล):

Worker 3 กำลังประมวลผลงาน 1
Worker 1 กำลังประมวลผลงาน 2
Worker 2 กำลังประมวลผลงาน 3
Worker 2 จบการทำงาน 3
Worker 2 กำลังประมวลผลงาน 4
Worker 2 จบการทำงาน 4
Worker 2 กำลังประมวลผลงาน 5
Worker 2 จบการทำงาน 5
Worker 3 จบการทำงาน 1
Worker 1 จบการทำงาน 2
ผลลัพธ์: 6
ผลลัพธ์: 8
ผลลัพธ์: 10
ผลลัพธ์: 2
ผลลัพธ์: 4

สรุป

ในบทความนี้เราได้เรียนรู้ Design Patterns หลายรูปแบบในภาษา Go พร้อมตัวอย่างการใช้งานที่ครอบคลุมสถานการณ์จริง ได้แก่:

Design Patternวัตถุประสงค์Use Caseหลักการสร้าง
Factory Patternสร้างอ็อบเจ็กต์หลายประเภทผ่านฟังก์ชันกลางสร้าง Notification (Email, SMS)สร้าง interface และ factory function
Repository Patternแยกการเข้าถึงข้อมูลออกจาก Business Logicจัดการข้อมูลในฐานข้อมูล (CRUD Operations)สร้าง interface และ implementation
Singleton Patternรับประกันว่ามี instance เดียวในโปรแกรมDatabase Connection, Configurationใช้ sync.Once เพื่อสร้างอ็อบเจ็กต์ครั้งเดียว
Builder Patternสร้างอ็อบเจ็กต์ที่มีฟิลด์หลายตัวแบบเป็นขั้นตอนสร้าง User Object ที่มีหลายฟิลด์สร้าง builder struct พร้อม chain methods
Adapter Patternทำให้ interface ที่เข้ากันไม่ได้ทำงานร่วมกันได้ใช้งาน API ของบุคคลที่สาม (Third-party API)สร้าง adapter struct เพื่อแปลง interface
Decorator Patternเพิ่มความสามารถให้กับอ็อบเจ็กต์โดยไม่แก้โค้ดเดิมเพิ่ม logging, caching หรือ encryptionสร้าง decorator struct ที่ wrap object
Worker Pool Patternประมวลผลงานจำนวนมากแบบขนานการประมวลผลที่ต้องทำซ้ำ เช่น ส่งอีเมลจำนวนมากใช้ Channel และ Goroutines

การเลือกใช้ Design Patterns ที่เหมาะสมจะช่วยให้โค้ดมีความยืดหยุ่น พร้อมรับการเปลี่ยนแปลง และขยายตัวได้ง่ายขึ้นในอนาคต