Skip to main content

🎨 Phong Cách Nấu - Design Patterns

Chào mừng đến bài học về Design Patterns - những phong cách nấu ăn đã được kiểm chứng trong Bếp Á!

🎯 Món Ăn Hôm Nay

Trong bếp chuyên nghiệp, mỗi đầu bếp đều có những "phong cách nấu" riêng - những cách tổ chức bếp núc, chuẩn bị nguyên liệu, và phục vụ món ăn đã được chứng minh hiệu quả qua thời gian. Trong lập trình, chúng ta gọi đó là Design Patterns - những giải pháp thiết kế đã được kiểm chứng cho các vấn đề phổ biến!

🥘 Design Pattern Là Gì?

Design Pattern = Phong cách nấu ăn của các đầu bếp chuyên nghiệp

Giống như nhà hàng có nhiều phong cách tổ chức khác nhau:

  • Chỉ một đầu bếp trưởng → Singleton Pattern
  • Nhà máy sản xuất món ăn → Factory Pattern
  • Theo dõi đơn hàng → Observer Pattern
  • Trang trí món ăn → Decorator Pattern

💡 Lợi Ích: Không cần "phát minh lại bánh xe" - học từ kinh nghiệm của những đầu bếp giỏi!

👨‍🍳 Câu Chuyện Trong Bếp

Bạn vừa được thăng chức làm đầu bếp trưởng của một nhà hàng lớn. Bạn phát hiện:

  • Có 5 người cùng quản lý kho → hỗn loạn! → Cần Singleton
  • Mỗi món phở đều nấu thủ công từ đầu → chậm! → Cần Factory
  • Khách đặt món nhưng bếp không biết → mất đơn! → Cần Observer
  • Mỗi món cần trang trí khác nhau → code lặp lại! → Cần Decorator

Hãy cùng học các "phong cách nấu" giải quyết những vấn đề này!

📝 1. Singleton Pattern - Chỉ Một Đầu Bếp Trưởng

Vấn Đề

Nhà hàng chỉ nên có một đầu bếp trưởng quản lý cấu hình toàn bộ bếp. Nếu có nhiều người cùng quản lý → xung đột, hỗn loạn!

Giải Pháp - Singleton

package main

import (
"fmt"
"sync"
)

// Config nhà hàng - chỉ có DUY NHẤT một instance
type RestaurantConfig struct {
TenNhaHang string
DiaChi string
SoBan int
GioMoCua string
}

var (
instance *RestaurantConfig
once sync.Once
)

// GetConfig - Lấy thông tin cấu hình duy nhất
func GetConfig() *RestaurantConfig {
once.Do(func() {
fmt.Println("🏪 Bổ nhiệm đầu bếp trưởng lần đầu tiên!")
instance = &RestaurantConfig{
TenNhaHang: "Bếp Á Hà Nội",
DiaChi: "123 Phố Huế",
SoBan: 30,
GioMoCua: "08:00 - 22:00",
}
})
return instance
}

func main() {
// Lần 1: Tạo đầu bếp trưởng
config1 := GetConfig()
fmt.Printf("Nhà hàng: %s\n", config1.TenNhaHang)

// Lần 2: Vẫn là đầu bếp trưởng cũ (không tạo mới)
config2 := GetConfig()

// Kiểm tra: cùng một người
fmt.Printf("Cùng một đầu bếp? %v\n", config1 == config2) // true
}

🔍 Giải Thích:

  • sync.Once đảm bảo chỉ khởi tạo một lần duy nhất
  • Dù gọi GetConfig() bao nhiêu lần cũng chỉ có 1 instance
  • Giống như nhà hàng chỉ có 1 đầu bếp trưởng quản lý toàn bộ

Khi Nào Dùng?

  • ✅ Quản lý cấu hình toàn cục
  • ✅ Kết nối database duy nhất
  • ✅ Logger chung cho toàn ứng dụng

📝 2. Factory Pattern - Nhà Máy Sản Xuất Món Ăn

Vấn Đề

Khách gọi nhiều loại món khác nhau (phở, bún, gỏi...). Nếu mỗi món đều tự nấu từ đầu → code dài, khó bảo trì!

Giải Pháp - Factory

package main

import "fmt"

// Interface chung cho mọi món ăn
type MonAn interface {
CheBien() string
PhucVu()
}

// Món Phở
type Pho struct {
Loai string
}

func (p *Pho) CheBien() string {
return "Nấu nước dùng 6 tiếng + bánh phở"
}

func (p *Pho) PhucVu() {
fmt.Printf("🍜 %s sẵn sàng! %s\n", p.Loai, p.CheBien())
}

// Món Bún
type Bun struct {
Loai string
}

func (b *Bun) CheBien() string {
return "Luộc bún + sốt đặc biệt"
}

func (b *Bun) PhucVu() {
fmt.Printf("🍝 %s sẵn sàng! %s\n", b.Loai, b.CheBien())
}

// Món Gỏi
type Goi struct {
Loai string
}

func (g *Goi) CheBien() string {
return "Gói rau + nước chấm"
}

func (g *Goi) PhucVu() {
fmt.Printf("🥗 %s sẵn sàng! %s\n", g.Loai, g.CheBien())
}

// FACTORY - Nhà máy sản xuất món ăn
func TaoMonAn(loaiMon string) MonAn {
switch loaiMon {
case "pho":
return &Pho{Loai: "Phở Bò"}
case "bun":
return &Bun{Loai: "Bún Chả"}
case "goi":
return &Goi{Loai: "Gỏi Cuốn"}
default:
return nil
}
}

func main() {
// Khách gọi món → Factory tự động tạo
donHang := []string{"pho", "bun", "goi"}

for _, tenMon := range donHang {
mon := TaoMonAn(tenMon)
if mon != nil {
mon.PhucVu()
}
}
}

🔍 Giải Thích:

  • TaoMonAn()nhà máy - nhận tên món, trả ra món đã chuẩn bị
  • Không cần biết chi tiết cách nấu từng món
  • Dễ mở rộng: thêm món mới chỉ cần sửa Factory

Khi Nào Dùng?

  • ✅ Tạo nhiều loại object khác nhau
  • ✅ Logic tạo object phức tạp
  • ✅ Muốn tách biệt việc tạo và sử dụng object

📝 3. Observer Pattern - Theo Dõi Đơn Hàng

Vấn Đề

Khách đặt món → Bếp cần biết ngay để nấu → Thu ngân cần biết để in hóa đơn → Shipper cần biết để giao hàng. Làm sao thông báo cho tất cả?

Giải Pháp - Observer

package main

import "fmt"

// Observer - Người quan sát (Bếp, Thu ngân, Shipper...)
type Observer interface {
NhanThongBao(donHang string)
}

// Subject - Hệ thống đặt món
type DatMonService struct {
observers []Observer
}

// Đăng ký người quan sát
func (d *DatMonService) DangKy(o Observer) {
d.observers = append(d.observers, o)
}

// Thông báo cho tất cả
func (d *DatMonService) ThongBaoTatCa(donHang string) {
fmt.Printf("\n📢 ĐƠN HÀNG MỚI: %s\n", donHang)
for _, observer := range d.observers {
observer.NhanThongBao(donHang)
}
}

// Bếp - Observer 1
type Bep struct{}

func (b *Bep) NhanThongBao(donHang string) {
fmt.Printf("👨‍🍳 Bếp: Bắt đầu nấu '%s'\n", donHang)
}

// Thu Ngân - Observer 2
type ThuNgan struct{}

func (t *ThuNgan) NhanThongBao(donHang string) {
fmt.Printf("💰 Thu ngân: In hóa đơn cho '%s'\n", donHang)
}

// Shipper - Observer 3
type Shipper struct{}

func (s *Shipper) NhanThongBao(donHang string) {
fmt.Printf("🚗 Shipper: Chuẩn bị giao '%s'\n", donHang)
}

func main() {
// Khởi tạo hệ thống đặt món
heThong := &DatMonService{}

// Đăng ký các bộ phận theo dõi
heThong.DangKy(&Bep{})
heThong.DangKy(&ThuNgan{})
heThong.DangKy(&Shipper{})

// Khách đặt món → Tất cả được thông báo tự động
heThong.ThongBaoTatCa("2 Phở Bò + 1 Bún Chả")
heThong.ThongBaoTatCa("3 Gỏi Cuốn")
}

🔍 Giải Thích:

  • Subject (DatMonService): Hệ thống nhận đơn hàng
  • Observer (Bếp, Thu ngân, Shipper): Bộ phận cần được thông báo
  • Khi có đơn mới → tự động thông báo cho tất cả observers

Khi Nào Dùng?

  • ✅ Event handling (xử lý sự kiện)
  • ✅ Notification system (thông báo)
  • ✅ Real-time updates (cập nhật real-time)

📝 4. Decorator Pattern - Trang Trí Món Ăn

Vấn Đề

Một món phở có thể có nhiều tùy chọn: thêm trứng, thêm tái, thêm gân... Nếu tạo class riêng cho mỗi kết hợp → quá nhiều class!

Giải Pháp - Decorator

package main

import "fmt"

// Interface món ăn cơ bản
type MonAn interface {
MoTa() string
Gia() int
}

// Phở cơ bản
type PhoCoBan struct{}

func (p *PhoCoBan) MoTa() string {
return "Phở Bò"
}

func (p *PhoCoBan) Gia() int {
return 40000
}

// Decorator - Thêm trứng
type ThemTrung struct {
mon MonAn
}

func (t *ThemTrung) MoTa() string {
return t.mon.MoTa() + " + Trứng"
}

func (t *ThemTrung) Gia() int {
return t.mon.Gia() + 10000
}

// Decorator - Thêm tái
type ThemTai struct {
mon MonAn
}

func (t *ThemTai) MoTa() string {
return t.mon.MoTa() + " + Tái"
}

func (t *ThemTai) Gia() int {
return t.mon.Gia() + 15000
}

// Decorator - Thêm gân
type ThemGan struct {
mon MonAn
}

func (t *ThemGan) MoTa() string {
return t.mon.MoTa() + " + Gân"
}

func (t *ThemGan) Gia() int {
return t.mon.Gia() + 20000
}

func main() {
// Món cơ bản
pho := &PhoCoBan{}
fmt.Printf("%s: %d VND\n", pho.MoTa(), pho.Gia())

// Thêm trứng
phoTrung := &ThemTrung{mon: pho}
fmt.Printf("%s: %d VND\n", phoTrung.MoTa(), phoTrung.Gia())

// Thêm trứng + tái
phoTrungTai := &ThemTai{mon: phoTrung}
fmt.Printf("%s: %d VND\n", phoTrungTai.MoTa(), phoTrungTai.Gia())

// Thêm trứng + tái + gân (full option!)
phoFullOption := &ThemGan{mon: phoTrungTai}
fmt.Printf("%s: %d VND\n", phoFullOption.MoTa(), phoFullOption.Gia())
}

🔍 Giải Thích:

  • Base Component (PhoCoBan): Món gốc
  • Decorator (ThemTrung, ThemTai...): Các lớp "trang trí" thêm vào
  • Có thể kết hợp linh hoạt: trứng, tái, gân...
  • Không cần tạo class PhoTrungTaiGan - chỉ cần "xếp chồng" các decorator!

Khi Nào Dùng?

  • ✅ Thêm tính năng động cho object
  • ✅ Tránh tạo quá nhiều subclass
  • ✅ Kết hợp linh hoạt các tính năng

📝 5. Strategy Pattern - Chiến Lược Giảm Giá

Vấn Đề

Nhà hàng có nhiều chương trình giảm giá: sinh viên giảm 10%, thẻ VIP giảm 20%, giờ vàng giảm 15%... Làm sao tổ chức code gọn gàng?

Giải Pháp - Strategy

package main

import "fmt"

// Interface chiến lược giảm giá
type ChienLuocGiamGia interface {
TinhGiam(giaGoc int) int
}

// Giảm giá sinh viên - 10%
type GiamGiaSinhVien struct{}

func (g *GiamGiaSinhVien) TinhGiam(giaGoc int) int {
return giaGoc * 10 / 100
}

// Giảm giá VIP - 20%
type GiamGiaVIP struct{}

func (g *GiamGiaVIP) TinhGiam(giaGoc int) int {
return giaGoc * 20 / 100
}

// Giảm giá giờ vàng - 15%
type GiamGiaGioVang struct{}

func (g *GiamGiaGioVang) TinhGiam(giaGoc int) int {
return giaGoc * 15 / 100
}

// Hóa đơn - sử dụng strategy
type HoaDon struct {
tongTien int
chienLuocGiam ChienLuocGiamGia
}

func (h *HoaDon) SetChienLuoc(cl ChienLuocGiamGia) {
h.chienLuocGiam = cl
}

func (h *HoaDon) TinhTongCuoi() int {
if h.chienLuocGiam != nil {
giam := h.chienLuocGiam.TinhGiam(h.tongTien)
return h.tongTien - giam
}
return h.tongTien
}

func main() {
hoaDon := &HoaDon{tongTien: 100000}

// Khách là sinh viên
hoaDon.SetChienLuoc(&GiamGiaSinhVien{})
fmt.Printf("Sinh viên trả: %d VND\n", hoaDon.TinhTongCuoi())

// Khách có thẻ VIP
hoaDon.SetChienLuoc(&GiamGiaVIP{})
fmt.Printf("VIP trả: %d VND\n", hoaDon.TinhTongCuoi())

// Giờ vàng
hoaDon.SetChienLuoc(&GiamGiaGioVang{})
fmt.Printf("Giờ vàng trả: %d VND\n", hoaDon.TinhTongCuoi())
}

🔍 Giải Thích:

  • Strategy Interface: Định nghĩa cách tính giảm giá
  • Concrete Strategies: Các cách tính cụ thể (sinh viên, VIP, giờ vàng)
  • Context (HoaDon): Sử dụng strategy để tính tiền
  • Dễ thêm chiến lược mới mà không sửa code cũ!

Khi Nào Dùng?

  • ✅ Nhiều thuật toán khác nhau cho cùng một việc
  • ✅ Cần đổi thuật toán runtime
  • ✅ Tránh nhiều if-else phức tạp

📝 6. Builder Pattern - Xây Dựng Đơn Hàng Phức Tạp

Vấn Đề

Đơn hàng có nhiều tùy chọn: loại món, size, topping, ghi chú đặc biệt... Constructor có quá nhiều tham số!

Giải Pháp - Builder

package main

import "fmt"

// Đơn hàng phức tạp
type DonHang struct {
MonChinh string
Size string
Toppings []string
GhiChu string
GiaoTanNoi bool
}

// Builder
type DonHangBuilder struct {
donHang *DonHang
}

func NewDonHangBuilder() *DonHangBuilder {
return &DonHangBuilder{
donHang: &DonHang{
Toppings: []string{},
},
}
}

func (b *DonHangBuilder) ChonMon(mon string) *DonHangBuilder {
b.donHang.MonChinh = mon
return b
}

func (b *DonHangBuilder) ChonSize(size string) *DonHangBuilder {
b.donHang.Size = size
return b
}

func (b *DonHangBuilder) ThemTopping(topping string) *DonHangBuilder {
b.donHang.Toppings = append(b.donHang.Toppings, topping)
return b
}

func (b *DonHangBuilder) ThemGhiChu(ghiChu string) *DonHangBuilder {
b.donHang.GhiChu = ghiChu
return b
}

func (b *DonHangBuilder) GiaoTanNoi(co bool) *DonHangBuilder {
b.donHang.GiaoTanNoi = co
return b
}

func (b *DonHangBuilder) Build() *DonHang {
return b.donHang
}

func (d *DonHang) HienThi() {
fmt.Println("\n📋 ĐƠN HÀNG:")
fmt.Printf("Món: %s (%s)\n", d.MonChinh, d.Size)
if len(d.Toppings) > 0 {
fmt.Printf("Toppings: %v\n", d.Toppings)
}
if d.GhiChu != "" {
fmt.Printf("Ghi chú: %s\n", d.GhiChu)
}
if d.GiaoTanNoi {
fmt.Println("🚗 Giao tận nơi")
}
}

func main() {
// Xây dựng đơn hàng phức tạp - dễ đọc!
don := NewDonHangBuilder().
ChonMon("Phở Bò").
ChonSize("Lớn").
ThemTopping("Trứng").
ThemTopping("Tái").
ThemTopping("Gân").
ThemGhiChu("Ít hành, nhiều rau").
GiaoTanNoi(true).
Build()

don.HienThi()

// Đơn giản hơn
donGianDon := NewDonHangBuilder().
ChonMon("Bún Chả").
ChonSize("Vừa").
Build()

donGianDon.HienThi()
}

🔍 Giải Thích:

  • Builder: Xây dựng object từng bước
  • Fluent Interface: Method chaining (.ChonMon().ChonSize()...)
  • Dễ đọc, dễ hiểu hơn constructor với 10 tham số!
  • Linh hoạt: chỉ điền thông tin cần thiết

Khi Nào Dùng?

  • ✅ Object có nhiều tham số khởi tạo
  • ✅ Một số tham số optional
  • ✅ Muốn code dễ đọc, dễ maintain

🔥 Thực Hành Trong Bếp

Bài Tập 1: Singleton Logger

Tạo một logger duy nhất cho toàn bộ nhà hàng:

type Logger struct {
// ... thông tin logger
}

func GetLogger() *Logger {
// TODO: Implement Singleton pattern
}

Bài Tập 2: Factory Đồ Uống

Tạo factory cho các loại đồ uống (trà, cà phê, nước ngọt):

type DoUong interface {
Pha() string
}

func TaoDoUong(loai string) DoUong {
// TODO: Implement Factory pattern
}

Bài Tập 3: Observer Bếp

Khi món nấu xong, thông báo cho nhân viên phục vụ và thu ngân:

// TODO: Implement Observer pattern
// - MonXongObserver
// - NhanVienPhucVu
// - ThuNgan

⚠️ Những Lỗi Đầu Bếp Thường Gặp

Lỗi 1: Lạm Dụng Pattern

// ❌ SAI: Dùng pattern khi không cần
type SimpleCalculator struct{}

// Không cần Factory cho cái này!
func CalculatorFactory() *SimpleCalculator {
return &SimpleCalculator{}
}

// ✅ ĐÚNG: Simple problem, simple solution
calc := &SimpleCalculator{}

🔧 Cách sửa: Chỉ dùng pattern khi thực sự cần thiết. Đừng phức tạp hóa vấn đề đơn giản!

Lỗi 2: Singleton Không Thread-Safe

// ❌ SAI: Không an toàn với goroutines
var instance *Config

func GetConfig() *Config {
if instance == nil {
instance = &Config{} // Race condition!
}
return instance
}

// ✅ ĐÚNG: Dùng sync.Once
var once sync.Once

func GetConfig() *Config {
once.Do(func() {
instance = &Config{}
})
return instance
}

Lỗi 3: Decorator Quá Nhiều Lớp

// ❌ SAI: Quá nhiều decorator → khó debug
mon := &ThemA{&ThemB{&ThemC{&ThemD{&ThemE{&MonCoBan{}}}}}}

// ✅ ĐÚNG: Vừa đủ, có ý nghĩa
mon := &ThemTrung{&ThemTai{&PhoCoBan{}}}

💡 Bí Quyết Của Đầu Bếp

  1. KISS - Keep It Simple: Pattern là công cụ, không phải mục đích. Giải pháp đơn giản nhất thường là tốt nhất!

  2. Hiểu vấn đề trước khi áp dụng: Mỗi pattern giải quyết một vấn đề cụ thể. Đừng "búa tìm đinh"!

  3. Học từ code thực tế: Đọc code của các library nổi tiếng (Docker, Kubernetes) để thấy cách họ dùng patterns.

  4. Tài liệu rõ ràng: Khi dùng pattern, comment giải thích tại sao dùng pattern đó.

  5. Refactor dần dần: Không cần dùng pattern từ đầu. Code đơn giản → thấy vấn đề → refactor sang pattern.

🎓 Bạn Đã Học Được

  • Singleton: Đảm bảo chỉ có 1 instance duy nhất (config, logger)
  • Factory: Tạo object mà không cần biết chi tiết implementation
  • Observer: Thông báo tự động cho nhiều observers khi có sự kiện
  • Decorator: Thêm tính năng động cho object mà không sửa code gốc
  • Strategy: Đổi thuật toán runtime, tránh if-else phức tạp
  • Builder: Xây dựng object phức tạp từng bước, dễ đọc dễ maintain

🍜 Món Tiếp Theo

Đã biết các phong cách nấu! Giờ học cách nấu nhanh hơn, hiệu quả hơn:

👉 Tối Ưu Tốc Độ - Performance


💡 Lời Khuyên Cuối: Design Patterns như công thức của đầu bếp lão làng - học từ kinh nghiệm, áp dụng khôn ngoan, đừng lạm dụng. Code tốt nhất là code dễ hiểu, không phải code "ngầu" nhất!