Nội dung

Các Design Pattern phổ biến trong Golang

Sê-ri - Lập trình Go(Golang)

Trong thế giới lập trình ngày càng phát triển, việc viết mã không chỉ dừng lại ở việc làm cho chương trình chạy được — mà còn phải dễ đọc, dễ bảo trì và có khả năng mở rộng. Đây chính là lúc các Design Pattern (mẫu thiết kế phần mềm) trở thành vũ khí bí mật của lập trình viên chuyên nghiệp.

Với Golang – một ngôn ngữ đơn giản, mạnh mẽ và được thiết kế để giải quyết những hệ thống phức tạp – việc áp dụng Design Pattern đúng cách không chỉ giúp mã nguồn gọn gàng, mà còn nâng tầm kiến trúc phần mềm. Tuy nhiên, không phải tất cả các mẫu thiết kế đều phù hợp với triết lý “tối giản mà hiệu quả” của Go.

Vậy những Design Pattern nào thực sự “hợp gu” với Golang? Làm sao để vận dụng chúng một cách linh hoạt mà không làm mất đi sự đơn giản vốn có của ngôn ngữ này?

Hãy cùng khám phá những mẫu thiết kế phổ biến, hiệu quả và được ưa chuộng nhất trong cộng đồng Golang – qua từng ví dụ thực tiễn rõ ràng và dễ hiểu.

  • Định nghĩa: Là giải pháp tổng quát cho những vấn đề mà lập trình viên hay gặp phải. Design pattern không thể được chuyển đổi trực tiếp thành code mà nó chỉ là một khuôn mẫu cho một vấn đề cần được giải quyết

  • Ưu điểm:

    • Code readability: giúp chúng ta viết code dễ hiểu hơn bằng việc sử dụng những tên biến liên quan đến những gì chúng ta đang thực hiện
    • Communication: giúp cho các lập trình viên có thể dễ dàng tiếp cận trao đổi về giái pháp cho một vấn đề bằng việc sử dụng một design pattern bất kỳ
    • Code reuse: Code được viết theo design pattern có thể được sử dụng lại nhiều lần
  • Nhược điểm:

    • More complexity: Làm phức tạp hóa quá trình code, khiến lập trình viên vất vả hơn khi phải suy nghĩ nên dùng mấu thiết kế nào để giải quyết vấn đề của mình khi mà anh ta chưa thực sự viết được 1 dòng code nào trong khi deadline đang cận kề
    • Different variants: Có nhiều biến thể cho mỗi mấu đối tượng khiến cho việc áp dụng các mẫu thiết kế đôi khi không đông nhất nên gây bất đồng khi làm việc giữa các lập trình viên

References:

Phân loại


Creational pattern

Creational Patterns(Mẫu thiết kế khởi tạo) là nhóm các mẫu thiết kế tập trung vào việc tạo ra đối tượng trong chương trình một cách linh hoạt, hiệu quả và có kiểm soát.

Mục tiêu chính:

  • Tránh việc tạo đối tượng một cách cứng nhắc bằng từ khóa new hoặc &Struct{}.
  • Giúp bạn ẩn đi logic khởi tạo phức tạp và thay vào đó là một cách khởi tạo rõ ràng, có thể mở rộng.
  • Hỗ trợ việc thay đổi cách tạo đối tượng mà không cần sửa mã sử dụng chúng.

🧠 Nói dễ hiểu hơn: Hãy tưởng tượng bạn cần một chiếc ô tô:

  • Cách thông thường: bạn tự mua từng bộ phận, lắp ráp tay – như gọi new(Car) → lỗi dễ xảy ra, tốn công.
  • Creational Pattern: bạn chỉ cần nói: “Tôi cần xe thể thao!” → một hệ thống sẽ lo từ A đến Z, và trả lại đúng chiếc bạn muốn.

🧩 Một số Creational Patterns phổ biến:

Tên Pattern Giải thích ngắn gọn
Singleton Đảm bảo chỉ có một thể hiện của một đối tượng tồn tại trong toàn chương trình.
Simple Factory Ẩn logic tạo đối tượng bên trong một hàm – đơn giản nhất.
Factory Method Cho phép lớp con tự định nghĩa cách tạo đối tượng, hỗ trợ mở rộng linh hoạt.
Abstract Factory Tạo ra các họ đối tượng có liên quan với nhau, dùng chung cấu trúc.
Builder Tạo đối tượng phức tạp theo từng bước, có thể tùy chỉnh linh hoạt.

Chỉ duy nhất một đối tượng của một type bất kì được khởi tạo trong suốt quá trình hoạt động của một chương trình

  • Các ứng dụng:
    • Chúng ta muốn sử dụng lại kết nối đến database khi truy vấn
    • Khi chúng ta mở một kết nối SSH đến một server để thực hiện một số công việc và chúng ta không muốn mở một kết nối khác mà sử dụng lại kết nối trước đó
    • Khi chúng ta cần hạn chế sự truy cập đến một vài biến số, chúng ta sử dụng mẫu singleton là trung gian để truy cập biến này
package DB

var (
  once sync.Once
  singleton *db.Connection
)

func GetDBConnection(config *mysql.Config) *db.Connection {
  once.Do(func() {
    singleton  = mysql.Dial(config)
  })
  return singleton
}
  • Lưu ý: Là mấu thiết kế phổ biến nhất nhưng đôi khi bị lạm dụng. Nên tránh việc sử dụng mẫu singleton vì khiến cho việc test cụ thể là việc tạo mock/stub trở nên khó khăn hơn

  • Có thể tạo ra nhiều biến thể khác nhau cho cùng một đối tượng
  • Ví dụ: Có 1 anh chàng nọ đang làm việc trong một team phát triển các hệ thống cho một ngân hàng và anh ta được giao nhiệm vụ cần tạo một kiểu dữ liệu BankAccout để chứa thông tin các khách đã mở tài khoản tại ngân hàng này
type BankAccount struct {
	ownerName string
	ownerIdentificationNumber uint64
	balance int64
}

func NewBankAccount(ownerName string, idNo uint64, balance int64) {
	return &BankAccount{ownerName, idNo, balance}
}

Rất dễ dàng để mở một tài khoản

var account BankAccount = NewBankAccount("Tuan", 123456789, 10000);

tuy nhiên đời không như mơ, Sếp lớn xỉ vả anh ta vì kiểu BankAccout thiếu chi nhánh ngân hàng và tỉ lệ lãi suất. Thế là anh ta lại hớt hải chỉnh sủa lại

type BankAccount struct {
	ownerName string
	ownerIdentificationNumber uint64
	balance int64
	interestRate float64 // ti suat
	branch string // chi nhanh
}

func NewBankAccount(ownerName string, idNo uint64, balance int64, interestRate float64, branch string) {
	return &BankAccount{ownerName, idNo, balance, interestRate, branch}
}

Một ngừời khác sử dụng code của anh ta viết để thực hiện task của mình và anh ta đã sử dụng như sau

var account BankAccount = NewBankAccount("Tuan",1000, 123456789, , 0.8, "Sai Gon");

anh này đã vô tình đổi số CMND(123456789), ra sau số dư(1000) và khiến ngân hàng thất thoát tiền. anh ta vô tình nhưng compiler không phát hiện ra vì cả 2 đều có kiểu dữ liệu uint64 và int64. Anh này đã đổ lỗi cho cho người đã thiết kế kiểu dữ liệu BankAccount chính là anh chàng trước đó.

  • Cách giải quyết được đặt ra:
    • Tạo các setter: Tuy nhiên người dùng đôi khi sẽ quên gọi các setter này

Anh chàng bị sếp trừ lương và quá uất ức anh sử dụng bí kíp design pattern Builder

type bankAccount struct {
	ownerName            string
	identificationNumber uint64
	branch               string
	balance              int64
} 

type BankAccount interface {
	WithDraw(amt uint64)
	Deposit(amt uint64)
	GetBalance() uint64
}

type BankAccountBuilder interface {
	WithOwnerName(name string) BankAccountBuilder
	WithOwnerIdentity(identificationNumber uint64) BankAccountBuilder
	AtBranch(branch string) BankAccountBuilder
	OpeningBalance(balance uint64) BankAccountBuilder
	Build() BankAccount
}

func (acc *bankAccount) WithDraw(amt uint64) {

}

func (acc *bankAccount) Deposit(amt uint64) {

}

func (acc *bankAccount) GetBalance() uint64 {
	return 0
}

func (acc *bankAccount) WithOwnerName(name string) BankAccountBuilder {
	acc.ownerName = name
	return acc
}

func (acc *bankAccount) WithOwnerIdentity(identificationNumber uint64) BankAccountBuilder {
	acc.identificationNumber = identificationNumber
	return acc
}

func (acc *bankAccount) AtBranch(branch string) BankAccountBuilder {
	acc.branch = branch
	return acc
}

func (acc *bankAccount) OpeningBalance(balance uint64) BankAccountBuilder {
	acc.balance = int64(balance)
	return acc
}

func (acc *bankAccount) Build() BankAccount {
	return acc
}

func NewBankAccountBuilder() BankAccountBuilder {
	return &bankAccount{}
}

func main() {
	account := NewBankAccountBuilder().
		WithOwnerName("Tuan").
		WithOwnerIdentity(123456789).
		AtBranch("Sai Gon").
		OpeningBalance(1000).Build()

	account.Deposit(10000)
	account.WithDraw(50000)
}

Bằng việc sử dụng builder pattern, code của anh chàng tuy có hơi dai dòng hơn nhưng đã khiến code trở nên dễ đọc hơn rất nhiều

Reference: https://dzone.com/articles/design-patterns-the-builder-pattern


  • Tạo một đối tượng mà không cần thiết chỉ ra một cách chính xác lớp nào sẽ được tạo bằng cách nhóm các đối tượng liên quan đến nhau và sử dụng 1 đối tượng trung gian để khởi tạo đối tượng cần tạo
  • ví dụ: chúng ta sẽ tạo các phương thức thanh toán cho 1 shop bằng factory method, hình thức thanh toán có thể bằng tiền mặt hoặc bằng thẻ debit
type PaymentMethod interface {
  Pay(amount float32) string
}

type PaymentType int

const (
  Cash PaymentType = iota
  DebitCard 
)

type CashPM struct {}
type DebitCardPM struct{}

func (c *CashPM) Pay(amount float32) string {
  return ""
}

func (c *DebitCardPM) Pay(amount float32) string {
  return ""
}

func GetPaymentMethod(t  PaymentType) PaymentMethod {
  siwtch t {
  case Cash:
    return new(CashPM)
  case DebitCard:
    return new(DebitCardPM)
  }
}

// usage
payment := GetPaymentMethod(DebitCard)
payment.Pay(20)
payment := GetPaymentMethod(Cash)
payment.Pay(50)

Behavioural Patterns

Behavioural Patterns (mẫu thiết kế hành vi) là nhóm các mẫu thiết kế tập trung vào cách các đối tượng giao tiếp, tương tác và cộng tác với nhau trong hệ thống. Mục tiêu là làm rõ ai làm gì, khi nào và bằng cách nào, giúp mã nguồn trở nên dễ hiểu, linh hoạt và dễ mở rộng hơn khi thay đổi hành vi hoặc luồng xử lý.

🔍 Nói đơn giản hơn: Nếu ví dụ hệ thống phần mềm là một dàn nhạc giao hưởng, thì:

  • Structural Patterns giống như cách các nhạc cụ được sắp xếp (ai nằm ở đâu, nhóm nào chơi với nhóm nào).
  • Behavioural Patterns sẽ giống như cách các nhạc công phối hợp chơi nhạc với nhau, theo đúng tiết tấu, lệnh nhạc trưởng, và phản ứng linh hoạt khi có thay đổi.

Mục đích chính của Behavioural Patterns:

  • Tách riêng logic xử lý hành vi khỏi đối tượng cốt lõi.
  • Tăng khả năng tái sử dụng mã và dễ bảo trì khi hành vi thay đổi.
  • Tổ chức luồng xử lý có cấu trúc, rõ ràng và dễ mở rộng.

🧩 Một số Behavioural Patterns phổ biến:

Tên Pattern Tóm tắt chức năng
Observer Một đối tượng thay đổi → tự động thông báo cho những đối tượng liên quan.
Strategy Cho phép hoán đổi thuật toán hoặc hành vi một cách linh hoạt trong lúc chạy.
Command Đóng gói hành động dưới dạng đối tượng để dễ xử lý, xếp hàng, hoàn tác…
Mediator Một đối tượng trung gian đứng ra điều phối các thành phần mà không để chúng phụ thuộc trực tiếp vào nhau.
Chain of Responsibility Các đối tượng được liên kết thành chuỗi để xử lý yêu cầu theo thứ tự.
State Thay đổi hành vi của đối tượng tùy thuộc vào trạng thái hiện tại của nó.

🤝 Vì sao đặc biệt phù hợp trong Golang?

Golang không hỗ trợ kế thừa lớp như OOP truyền thống, nên các pattern giúp tách hành vi như Strategy, Observer hay Command rất phù hợp để tận dụng interface, struct và composition – vốn là điểm mạnh của Go.


Observer Pattern(Nghe ngóng và phản hồi) giống như một hệ thống “theo dõi và phản hồi”. Khi một đối tượng trung tâm (Subject) thay đổi trạng thái, tất cả các “người theo dõi” (Observers) của nó sẽ tự động nhận được thông báophản ứng lại nếu cần.

Ví dụ thực tế: Hãy tưởng tượng bạn theo dõi một kênh YouTube (Subject). Khi kênh đăng video mới (thay đổi trạng thái), bạn – cùng với hàng ngàn subscriber khác (Observers) – sẽ được thông báo ngay lập tức. Mỗi người có thể chọn xem hoặc bỏ qua video đó, tùy theo cách họ phản ứng.

Cấu trúc chính:

  • Subject: đối tượng chính được theo dõi. Nó có:

    • Trạng thái nội tại
    • Danh sách các observer đang “đăng ký theo dõi”
    • Các phương thức để thêm/xoá/thông báo đến observers
  • Observer: những đối tượng đăng ký theo dõi subject và sẽ phản ứng lại khi được thông báo.

Tóm tắt cơ chế hoạt động:

  1. Các observer đăng ký vào subject.
  2. Khi trạng thái của subject thay đổi, nó sẽ gửi thông báo (notify) đến tất cả observer.
  3. Mỗi observer sẽ thực hiện hành động riêng của mình (cập nhật giao diện, lưu log, gửi email…).
  • Sơ đồ:
classDiagram
    class Subject {
        +observerCollection
        +registerObserver(observer)
        +unregisterObserver(observer)
        +notifyObservers()
    }
    class Observer {
        +update()
    }
    class ConcreteObserverA {
        +update()
    }
    class ConcreteObserverB {
        +update()
    }

    Subject "1" o-- "*" Observer : observerCollection
    Observer <|-- ConcreteObserverA
    Observer <|-- ConcreteObserverB

    %% notifyObservers() pseudocode:
    %% for observer in observerCollection
    %%     call observer.update()
  • Ví dụ: Giả sử chúng ta có 1 kênh youtube và mối khi chúng ta release 1 video mới, các subscriber đều được thông báo về thông tin của video mới này. Ta coi youtube channel là 1 subject và những subscriber trong channel này là các observer. Khi các observer nhận được thông tin về video mới, app của các subscriber sẽ chịu trách nhiệm update lại UI để người dùng có thể click vào những video vừa được đăng tải
  • Sơ đồ:
classDiagram
    class Observer {
        +update()
    }

    class Subject {
        +Observers
        +registerObserver(observer)
        +removeObserver(observer)
        +notifyObservers()
    }

    class Video {
        +title
    }

    class YoutubeChannel {
        +Observers
        +NewVideo
        +registerObserver(observer)
        +removeObserver(observer)
        +notifyObservers()
        +ReleaseNewVideo(video)
    }

    class UserInterface {
        +UserName
        +Videos
        +update()
    }

    Subject <|-- YoutubeChannel
    Observer <|-- UserInterface
    YoutubeChannel "1" o-- "*" Observer : Observers
    YoutubeChannel --> Video : NewVideo
    UserInterface --> Video : Videos

    %% notifyObservers pseudocode:
    %% for observer in Observers
    %%     call observer.update(NewVideo)
  • Code:
type Observer interface {
	update(interface{})
}

type Subject interface {
	registerObserver(obs Observer)
	removeObserver(obs Observer)
	notifyObservers()
}

type Video struct {
	title string
}

// YoutubeChannel is a concrete implementation of Subject interface
type YoutubeChannel struct {
	Observers []Observer
	NewVideo  *Video
}

func (yt *YoutubeChannel) registerObserver(obs Observer) {
	yt.Observers = append(yt.Observers, obs)
}

func (yt *YoutubeChannel) removeObserver(obs Observer) {
	//
}

// notify to all observers when a new video is released
func (yt *YoutubeChannel) notifyObservers() {
	for _, obs := range yt.Observers {
		obs.update(yt.NewVideo)
	}
}

func (yt *YoutubeChannel) ReleaseNewVideo(video *Video) {
	yt.NewVideo = video
	yt.notifyObservers()
}

// UserInterface is a concrete implementation of Observer interface
type UserInterface struct {
	UserName string
	Videos   []*Video
}

func (ui *UserInterface) update(video interface{}) {
	ui.Videos = append(ui.Videos, video.(*Video))
	for video := range ui.Videos {
		View.AddChildNode(NewVideoComponent(video))
	}
	fmt.Printf("UI %s - Video: '%s' has just been released\n", ui.UserName, video.(*Video).title)
}

func NewUserInterface(username string) Observer {
	return &UserInterface{UserName: username, Videos: make([]*Video, 0)}
}

// usage

func main() {
	var ytChannel Subject = &YoutubeChannel{}
	ui1 := NewUserInterface("Bob")
	ui2 := NewUserInterface("Peter")
	ytChannel.registerObserver(ui1)
	ytChannel.registerObserver(ui2)
	ytChannel.(*YoutubeChannel).ReleaseNewVideo(&Video{title: "Avatar 2 trailer"})
	ytChannel.(*YoutubeChannel).ReleaseNewVideo(&Video{title: "Avengers Endgame trailer"})
}

Result

UI Bob - Video: 'Avatar 2 trailer' has just been released
UI Peter - Video: 'Avatar 2 trailer' has just been released
UI Bob - Video: 'Avengers Endgame trailer' has just been released
UI Peter - Video: 'Avengers Endgame trailer' has just been released

  • Là mẫu thiết kế cho phép chọn thuật toán trong 1 nhóm các thuật toán liên quan đến nhau ngay tại lúc chương trình đang chạy(at runtime) để thực hiện một hoạt động nào đó

  • Giả sử: chúng ta cần xây dựng 1 thư viện để mã hóa một đoạn tin bằng các phương pháp asymmetric chúng ta có thể sử dụng 1 trong 2 thuật toán sau: RSA hoặc Elliptic curve

package encryption

type AsymEncryptionStrategy interface {
	Encrypt(data interface{}) (byte[] cipher, error)
}

type EllipticCurvestrategy struct {} 
type RSA struct {}

func (strat *EllipticCurvestrategy) Encrypt(data interface{}) (byte[] cipher, error) {
	// some complex math
	...
	return cipher, err 
}

func (strat *RSAstrategy) Encrypt(data interface{}) (byte[] cipher, error) {
	// some complex math
	...
	return cipher, err 
} 

func encryptMessage(msg string, strat AsymEncryptionStrategy) (byte[] cipher, error) {
	return strat.Encrypt(msg)
}

// usage
msg := "this is a confidential message"
cipher, err := encryptMessage(msg, encryption.EllipticCurvestrategy)
cipher, err := encryptMessage(msg, encryption.RSAstrategy)

  • Mẫu này được sử dụng để truy cập vào các phần tử của 1 collection(array, map, set) một cách tuần tự mà không cần phải hiểu biết về nó.
  • Iterate 1 collection sử dụng callback:
func iterateEvenNumbers(max int, cb func(n int) error) error {
    if max < 0 {
        return fmt.Errorf("'max' is %d, must be >= 0", max)
    }
    for i := 2; i <= max; i += 2 {
        err := cb(i)
        if err != nil {
            return err
        }
    }
    return nil
}

func printEvenNumbers(max int) {
    err := iterateEvenNumbers(max, func(n int) error {
        fmt.Printf("%d\n", n)
        return nil
    })
    if err != nil {
        log.Fatalf("error: %s\n", err)
    }
}

printEvenNumbers(10)

Pattern này được sử dụng trong go standard library: filepath.Walk

  • Iterate với Next()
// EvenNumberIterator generates even numbers
type EvenNumberIterator struct {
    max       int
    currValue int
    err       error
}

// NewEvenNumberIterator creates new number iterator
func NewEvenNumberIterator(max int) *EvenNumberIterator {
    var err error
    if max < 0 {
        err = fmt.Errorf("'max' is %d, should be >= 0", max)
    }
    return &EvenNumberIterator{
        max:       max,
        currValue: 0,
        err:       err,
    }
}

// Next advances to next even number. Returns false on end of iteration.
func (i *EvenNumberIterator) Next() bool {
    if i.err != nil {
        return false
    }
    i.currValue += 2
    return i.currValue <= i.max
}

// Value returns current even number
func (i *EvenNumberIterator) Value() int {
    if i.err != nil || i.currValue > i.max {
        panic("Value is not valid after iterator finished")
    }
    return i.currValue
}

// Err returns iteration error.
func (i *EvenNumberIterator) Err() error {
    return i.err
}

func printEvenNumbers(max int) {
	iter := NewEvenNumberIterator(max)
	for iter.Next() {
		fmt.Printf("n: %d\n", iter.Value())
	}
	if iter.Err() != nil {
		log.Fatalf("error: %s\n", iter.Err())
	}
}

func main() {
	fmt.Printf("Even numbers up to 8:\n")
	printEvenNumbers(8)
	fmt.Printf("Even numbers up to 9:\n")
	printEvenNumbers(9)
	fmt.Printf("Error: even numbers up to -1:\n")
	printEvenNumbers(-1)
}

  • Mỗi đối tượng có 1 trạng thái gắn với nó và trạng thái có thể được thay đổi thông qua SetState method
  • Ví dụ: Giả sử điện thoại có 2 trạng thái nhắc nhở: Im lặng hoặc Rung
  • code
type MobileAlertState interface {
	alert()
}

type AlertStateContext struct {
	currentState MobileAlertState
}

func NewAlertStateContext() *AlertStateContext {
	return &AlertStateContext{currentState: &Vibration{}}
}

func (ctx *AlertStateContext) SetState(state MobileAlertState) {
	ctx.currentState = state
}

func (ctx *AlertStateContext) Alert() {
	ctx.currentState.alert()
}

type Vibration struct{}

func (v *Vibration) alert() {
	fmt.Println("vibrating....")
}

type Silence struct{}

func (s *Silence) alert() {
	fmt.Println("silent ....")
}

func main() {
	stateContext := NewAlertStateContext()
	stateContext.Alert()
	stateContext.Alert()
	stateContext.Alert()
	stateContext.SetState(&Silence{})
	stateContext.Alert()
	stateContext.Alert()
	stateContext.Alert()
}

// result
vibrating....
vibrating....
vibrating....
silent ....
silent ....
silent ....

Reference: https://www.geeksforgeeks.org/state-design-pattern/


Structural Patterns

Structural Patterns(Mẫu thiết kế cấu trúc) là nhóm mẫu thiết kế tập trung vào cách các thành phần trong hệ thống được sắp xếp và kết nối với nhau để tạo thành cấu trúc lớn hơn, linh hoạt hơn, dễ mở rộng và bảo trì hơn.

Mục tiêu chính:

  • Tổ chức quan hệ giữa các đối tượng hoặc class theo cách rõ ràng và hiệu quả.
  • Cho phép bạn kết hợp các đối tượng hiện có theo những cách mới mà không cần thay đổi mã gốc.
  • Tăng tính tái sử dụnggiảm phụ thuộc chặt chẽ (tight coupling).

Giải thích dễ hiểu: Hãy tưởng tượng bạn đang xây nhà:

  • Viên gạch (struct, object) là đơn vị nhỏ.
  • Bạn không thay đổi viên gạch, mà thay vào đó bạn xây tường, trát xi măng, lắp khung cửa, lợp mái… để tạo thành một ngôi nhà hoàn chỉnh – đó chính là Structural Pattern!

Bạn dùng những thành phần có sẵn, nhưng ghép nối thông minh hơn, để có cấu trúc tổng thể hợp lý và dễ thay đổi.

Một số Structural Patterns phổ biến:

Tên Pattern Giải thích ngắn gọn
Adapter “Bộ chuyển đổi” – giúp hai thành phần không tương thích làm việc với nhau.
Bridge Tách phần trừu tượng và phần hiện thực để có thể phát triển độc lập.
Composite Cho phép xử lý một nhóm đối tượng giống như một đối tượng đơn lẻ.
Decorator Thêm chức năng cho đối tượng mà không thay đổi lớp gốc.
Facade Cung cấp giao diện đơn giản để che giấu hệ thống phức tạp bên dưới.
Flyweight Tái sử dụng đối tượng giống nhau để tiết kiệm bộ nhớ.
Proxy Một lớp thay thế đứng giữa để kiểm soát truy cập đến đối tượng thật.

  • Cho phép các interface không liên quan đến nhau có thể làm việc cùng nhau

  • Liên tưởng: Giả sử bạn cần cắm sạc điện thoại vào một ổ cắm điện không tương thích khi đó bạn cần phải dùng một bộ chuyển đổi(adapter)

  • Ví dụ: Mèo và cá sấu

package animal

type Animal interface {
  Move()
}

// Cat is a concrete animal since it implements the method Move
type Cat struct {}

func (c *Cat) Move() {}

// and somewhere in the code we need to use the crocodile type which is often not our code and this Crocodile type does not implement the Animal interface
// but we need to use a crocodile as an animal

type Crocodile struct {}

func (c *this.Crocodile) Slither() {}

// we create an CrocodileAdapter struct that dapts an embeded crocodile so that it can be usedd as an Animal

type CrocodileAdapter struct {
  *Crocodile
}

func NeweCrocodile() *CrocodileAdapter {
  return &CrocodileAdapter{new(Crocodile)}
}

func (this *CrocodileAdapter) Move() {
  this.Slither()
}

// usage
import "animal"

var animals []animal.Animal
animals = append(animals, new(animals.Cat))
animals = append(animals, new(animals.NewCrocodile()))

for  entity := range animals {
  entity.Move()
} 

Reference: Design Patterns Explained: Adapter Pattern With Code Examples - DZone Java


  • cho phép tương tác với tất cả các đối tượng tương tự nhau giống như là 1 đối tượng đơn hoặc 1 nhóm các đối tượng

  • Hình dung: Đối tượng File sẽ là 1 đối tượng đơn nếu bên trong nó không có file nào khác, nhưng đối tượng file sẽ được đối xử giống như 1 collection nếu bên trong nó lại có những File khác.

  • Sơ đồ:

erDiagram
    COMPONENT ||--o{ COMPOSITE : has_child
    COMPONENT ||--|{ LEAF : is_a
    COMPONENT ||--|{ COMPOSITE : is_a
  • Cấu trúc:

    • Component (Thành phần):
      • Khai báo interface hoặc abstract chung cho các thành phần đối tượng.
      • Chứa các method thao tác chung của các thành phần đối tượng.
    • Leaf (Lá):
      • Biểu diễn các đối tượng lá (ko có con) trong thành phần đối tượng.
    • Composite (Hỗn hợp):
      • Định nghĩa một thao tác cho các thành phần có thành phần con.
      • Lưu trữ và quản lý các thành phần con
  • Ví dụ khác: Khi vẽ, chúng ta được cung cấp các đối tượng Square, Circle, các đối tượng này đều là Shape. Giả sử chúng ta muốn vẽ nhiều loại hình cùng 1 lúc chúng ta sẽ tạo một Layer chứa các Shape này và thực hiện vòng lặp để vẽ chúng. Ở đây composite được hiểu là chúng ta có thể sử dụng các đối tượng Square, Circle riêng biệt nhưng khi cần chúng ta có thể gom chúng lại thành 1 nhóm

// Shape is the component
type Shape interface {
	Draw(drawer *Drawer) error
}

// Square and Circle are leaves
type Square struct {
	Location Point
	Size float64
}

func (square *Square) Draw(drawer *Drawer) error {
	return drawer.DrawRect(Rect{
		Location: square.Location,
		Size: Size{
			Height: square.Side,
			Width:  square.Side,
		},
	})
}

type Circle struct {
	Center Point
	Radius float64
}

func (circle *Circle) Draw(drawer *Drawer) error {
	rect := Rect{
		Location: Point{
			X: circle.Center.X - circle.Radius,
			Y: circle.Center.Y - circle.Radius,
		},
		Size: Size{
			Width:  2 * circle.Radius,
			Height: 2 * circle.Radius,
		},
	}

	return drawer.DrawEllipseInRect(rect)
}

// Layer is the composite
type Layer struct {
	Shapes []Shape
}

func (layer *Layer) Draw(drawer *Drawer) error {
	for _, shape := range layer.Shapes {
		if err := shape.Draw(drawer); err != nil {
			return err
		}
		fmt.Println()
	}

	return nil
}

// usage
circle := &photoshop.Circle{
	Center: photoshop.Point{X: 100, Y: 100},
	Radius: 50,
}

square := &photoshop.Square{
	Location: photoshop.Point{X: 50, Y: 50},
	Side:     20,
}

layer := &photoshop.Layer{
	Elements: []photoshop.Shapes{
		circle,
		square,
	},
}

circle.Draw(&photoshop.Drawer{})
square.Draw(&photoshop.Drawer{})
// or
layer.Draw(&photoshop.Drawer{})

  • Tách 1 class lớp thành 2 phần interface và reprensentation để 2 phần này không bị phụ thuộc vào nhau và có thể được phát triển song song
  • Vấn đề: Giả sử chúng ta cần xây dựng 1 package UI hỗ trợ vẽ nhiều hình dạng trên màn hình bằng cả 2 công nghệ rendering direct2d và opengl. Trong trường hợp này là vẽ hình tròn dùng cả 2 công nghệ. Ta tách struct Circle khỏi phần drawing
type Circle struct {
  DrawingContext drawer
  Center Point
  Radius float64
}

func (circle *Circle) Draw() error {
  rect := Rect{
    Location: Point{
      X: circle.Center.X - circle.Radius,
      Y: circle.Center.Y - circle.Radius,
    },
    Size: Size{
      Width: 2 * circle.Radius,
      Height: 2 * circle.Radius,
    }
  }
  return circle.DrawingContext.DrawEllipseInRect(rect)
}

type Drawer interface {
  DrawEllipseInRect(Rect) error
}

// OpenGL drawer
type OpenGL struct{}
// DrawEllipseInRect draws an ellipse in rectangle
func (gl *OpenGL) DrawEllipseInRect(r Rect) error {
	fmt.Printf("OpenGL is drawing ellipse in rect %v", r)
	return nil
}

// Direct2D drawer
type Direct2D struct{}

// DrawEllipseInRect draws an ellipse in rectangle
func (d2d *Direct2D) DrawEllipseInRect(r Rect) error {
	fmt.Printf("Direct2D is drawing ellipse in rect %v", r)
	return nil
}

// usage
openGL := &uikit.OpenGL{}
direct2D := &uikit.Direct2D{}

circle := &uikit.Circle{
	Center: uikit.Point{X: 100, Y: 100},
	Radius: 50,
}

circle.DrawingContext = openGL
circle.Draw()

circle.DrawingContext = direct2D
circle.Draw()

  • Mục đích
    • Kiểm soát quyền truy suất các phương thức của đối tượng
    • Bổ xung thêm chức năng trước khi thực thi phương thức gốc
    • Tạo ra đối tượng mới có chức năng cao hơn đối tượng ban đầu
    • Giảm chi phí khi có nhiều truy cập vào đối tượng có chi phí khởi tạo ban đầu lớn
type Image interface {
	display()
}

type HighResolutionImage struct {
	imageFilePath string
}

type ImageProxy struct {
	imageFilePath string
	realImage     Image
}

func (this *HighResolutionImage) loadImage(path string) {
	// load image from disk into memory
	// this is a heavy and costly operation
	fmt.Printf("load image %s from disk\n", path)
}

func (this *HighResolutionImage) display() {
	this.loadImage(this.imageFilePath)
	fmt.Printf("display high resolution image\n")
}

func (this *ImageProxy) display() {
	this.realImage = &HighResolutionImage{imageFilePath: this.imageFilePath}
	this.realImage.display()
}

func NewImageProxy(path string) Image {
	return &ImageProxy{imageFilePath: path}
}

// usage

highResolutionImage := NewImageProxy("sample/img1.png")
// the realImage won't be loaded until user calls display
// later
highResolutionImage.display()

Reference: https://www.oodesign.com/proxy-pattern.html

Design Pattern không phải là thứ gì quá phức tạp hay “hàn lâm” – mà thực ra, nó là tập hợp những cách giải quyết vấn đề thông minh, có sẵn, đã được kiểm chứng trong quá trình viết phần mềm.

Trong bài viết này, chúng ta đã cùng điểm qua ba nhóm chính:

  • Creational – cách tạo ra đối tượng hiệu quả và linh hoạt hơn
  • Structural – cách sắp xếp và kết nối các thành phần sao cho rõ ràng, dễ mở rộng
  • Behavioural – cách các đối tượng “giao tiếp” với nhau một cách trật tự

Mỗi pattern giống như một công cụ trong “hộp đồ nghề” của lập trình viên. Không phải lúc nào cũng cần dùng hết, nhưng khi dùng đúng lúc, đúng chỗ – chúng sẽ giúp bạn viết code dễ đọc hơn, dễ bảo trì hơn, và “xịn” hơn rất nhiều.

Golang, với triết lý đơn giản và mạnh mẽ, có thể không áp dụng mọi pattern theo kiểu truyền thống như trong Java hay C++, nhưng chính nhờ đó, bạn sẽ học cách tư duy lại và áp dụng chúng một cách gọn gàng, tinh tế hơn – đúng với tinh thần “Go idiomatic”.

Hy vọng bài viết này giúp bạn có cái nhìn rõ ràng hơn về Design Pattern trong Go. Hãy thử áp dụng một vài pattern vào dự án thực tế của bạn – bạn sẽ thấy sự khác biệt rõ rệt!