Design Patterns

Back to Software Development Index

Domain-Driven Design (DDD)

Domain-driven design (DDD) เป็นแนวคิดในการพัฒนา software ที่เน้นให้ความสำคัญกับ domain (ธุรกิจหรือโจทย์ที่ต้องแก้) เป็นหลัก โดยให้ทุกคนในทีมมีความเข้าใจตรงกันในภาษา domain นั้นๆ ผ่านกระบวนการสื่อสารและทำงานร่วมกัน

Core Concepts

1. Domain Types

DDD แบ่ง domain ออกเป็น 3 ประเภท:

Core Domain

  • เป็นธุรกิจหลัก (core business) ของบริษัท
  • จุดเด่นที่ทำให้แตกต่างจากคู่แข่ง
  • ต้องการ developers ที่เก่งที่สุด
  • Example: สำหรับ e-commerce → recommendation engine, pricing algorithms

Supporting Subdomain

  • สนับสนุนการทำงานของ core domain
  • สร้างแยกได้ หรือจ้างบุคคลภายนอก (third-party)
  • มีความซับซ้อนปานกลาง
  • Example: Inventory management, shipping logistics

Generic Subdomain

  • เป็นงานทั่วไปที่ไม่ได้มี business value มาก
  • มักใช้ solution สำเร็จจากภายนอก
  • Example: Authentication, logging, email services

2. Strategic Design

Ubiquitous Language

  • ภาษาที่ทุกคนในทีมใช้ร่วมกัน (developer, business stakeholders)
  • ต้องสอดคล้องกับ code, documentation, และการสื่อสาร

Bounded Context

  • กำหนดขอบเขตของ model แต่ละส่วน
  • แต่ละ context มี model และภาษาเป็นของตัวเอง
  • Example: “Customer” ใน Sales Context vs “User” ใน Support Context

Context Mapping

  • กำหนดความสัมพันธ์ระหว่าง bounded contexts
  • Patterns: Shared Kernel, Customer-Supplier, Conformist, Anticorruption Layer

3. Tactical Design

Entities

  • Objects ที่มี identity ที่ไม่ซ้ำ
  • มี lifecycle
  • Example: User, Order, Product

Value Objects

  • Objects ที่ไม่มี identity
  • เปรียบเทียบด้วยค่าที่เก็บ (immutable)
  • Example: Address, Money, DateRange

Aggregates

  • กลุ่มของ entities และ value objects
  • มี Aggregate Root เป็นตัวควบคุม
  • รักษา business invariants

Repositories

  • จัดการการเก็บและดึง aggregates
  • แยก domain logic จาก infrastructure

Domain Events

  • เหตุการณ์สำคัญที่เกิดขึ้นใน domain
  • ใช้สำหรับการสื่อสารระหว่าง bounded contexts

Services

  • Operations ที่ไม่เหมาะกับ entities หรือ value objects
  • Stateless operations

Event Storming

เป็นเทคนิคการ workshop ที่ทีมมารวมตัวกันเพื่อทำความเข้าใจ domain

Process:

  1. ระบุ Domain Events (สิ่งที่เกิดขึ้น)
  2. หา Commands (สิ่งที่ trigger events)
  3. กำหนด Aggregates (ที่จัดการ commands)
  4. แบ่ง Bounded Contexts

Benefits:

  • สร้าง shared understanding
  • ค้นพบ domain complexity
  • ระบุ bounded contexts
  • เข้าใจ business process

DDD Implementation Guidelines

When to Use DDD ✅

  • Complex business domains
  • Long-term projects
  • Need for close collaboration with domain experts
  • Multiple teams working on different areas

When NOT to Use DDD ❌

  • Simple CRUD applications
  • Technical/infrastructure problems
  • Tight deadlines with simple requirements
  • Small projects

DDD vs Clean Architecture

AspectDDDClean Architecture
FocusDomain modelingDependency management
CoreBounded contextsBusiness rules isolation
ApproachModel-drivenLayer-driven
ComplexityHigh (model complexity)Medium (structural)
Best ForComplex domainsAny application size

They Complement Each Other:

  • DDD helps model the domain
  • Clean Architecture helps structure the code
  • Use DDD for entities/aggregates layer
  • Use Clean Architecture for overall structure

Factory Pattern

Factory Pattern เป็น design pattern ที่ช่วยในการสร้าง object โดยไม่ต้องระบุ class ที่แน่นอนที่จะสร้าง

Benefits

  1. Encapsulation - ซ่อนการสร้าง object ไว้ใน factory
  2. Flexibility - ง่ายต่อการเพิ่ม product types ใหม่
  3. Single Responsibility - แยก creation logic ออกจาก business logic
  4. Dependency Inversion - Client ไม่ต้องรู้จัก concrete classes

Go Implementation

package main
 
import "fmt"
 
// Product Interface
type IWeapon interface {
    GetName() string
    GetPower() int
}
 
// Concrete Product 1: Sword
type Sword struct {
    name  string
    power int
}
 
func (s Sword) GetName() string {
    return s.name
}
 
func (s Sword) GetPower() int {
    return s.power
}
 
// Concrete Product 2: Bow
type Bow struct {
    name  string
    power int
}
 
func (b Bow) GetName() string {
    return b.name
}
 
func (b Bow) GetPower() int {
    return b.power
}
 
// Factory
type WeaponFactory struct{}
 
func (wf WeaponFactory) CreateWeapon(weaponType string) IWeapon {
    switch weaponType {
    case "sword":
        return Sword{
            name:  "Steel Sword",
            power: 50,
        }
    case "bow":
        return Bow{
            name:  "Wooden Bow",
            power: 35,
        }
    default:
        return nil
    }
}
 
// Client Code
func main() {
    factory := WeaponFactory{}
    
    // Create sword
    sword := factory.CreateWeapon("sword")
    fmt.Printf("Created: %s with power %d\n", 
        sword.GetName(), sword.GetPower())
    
    // Create bow
    bow := factory.CreateWeapon("bow")
    fmt.Printf("Created: %s with power %d\n", 
        bow.GetName(), bow.GetPower())
}

Output:

Created: Steel Sword with power 50
Created: Wooden Bow with power 35

Factory Pattern Variations

1. Simple Factory

  • Single factory method
  • Returns different types based on parameter
  • ตัวอย่างข้างบนคือ Simple Factory

2. Factory Method

  • Abstract factory method in base class
  • Subclasses implement to create specific products
  • Follows Open/Closed Principle

3. Abstract Factory

  • Factory of factories
  • Creates families of related objects
  • Multiple factory methods

When to Use Factory Pattern

Use When:

  • Object creation is complex
  • Need to return different types based on context
  • Want to hide creation logic
  • Following dependency inversion

Avoid When:

  • Simple object creation (use new directly)
  • No variation in creation logic
  • Over-engineering simple problems

Related:

References: