Agile Design คืออะไรและหลักการสำคัญ
Agile Design คือแนวทางการออกแบบ software ที่เน้น iterative development, continuous feedback และ adaptability แทนที่จะออกแบบทุกอย่างล่วงหน้า (Big Design Up Front — BDUF) Agile Design ออกแบบเท่าที่จำเป็นแล้วปรับปรุงตามการเรียนรู้จากการใช้งานจริง
หลักการสำคัญของ Agile Design ได้แก่ YAGNI (You Aren't Gonna Need It) ไม่สร้างสิ่งที่ยังไม่ต้องการ, KISS (Keep It Simple Stupid) ทำให้เรียบง่ายที่สุด, DRY (Don't Repeat Yourself) ไม่ทำซ้ำ, SOLID principles สำหรับ object-oriented design และ Refactoring ปรับปรุง code structure อย่างต่อเนื่อง
Agile Design ต่างจาก Traditional Design ตรงที่ Traditional Design ออกแบบเสร็จก่อนแล้วค่อย implement ส่วน Agile Design ออกแบบและ implement ไปพร้อมกัน ปรับเปลี่ยนได้ตลอด Traditional Design ใช้เวลามากกับ documentation Agile Design เน้น working code และ automated tests เป็น documentation
Agile Design ไม่ได้หมายความว่าไม่ออกแบบเลย แต่ออกแบบเพียงพอสำหรับ iteration ปัจจุบัน ใช้ emergent design ให้ architecture เกิดขึ้นจากการ refactor อย่างต่อเนื่อง ทำให้ได้ design ที่เหมาะกับ requirements จริงไม่ใช่ requirements ที่คาดเดา
Agile Design Principles สำหรับ Software Development
SOLID Principles พร้อมตัวอย่าง code
#!/usr/bin/env python3
# solid_principles.py — SOLID Principles Examples
# === S: Single Responsibility Principle ===
# แต่ละ class มีเหตุผลเดียวในการเปลี่ยนแปลง
# BAD: class ทำหลายหน้าที่
class UserBad:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_db(self): # persistence logic
pass
def send_email(self): # notification logic
pass
def generate_report(self): # reporting logic
pass
# GOOD: แยกหน้าที่ออก
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
class UserRepository:
def save(self, user: User):
# Database logic only
print(f"Saved {user.name} to database")
def find_by_email(self, email: str):
# Query logic only
return User("found", email)
class EmailService:
def send_welcome(self, user: User):
# Email logic only
print(f"Sent welcome email to {user.email}")
class UserReportGenerator:
def generate(self, users: list):
# Report logic only
return f"Report: {len(users)} users"
# === O: Open/Closed Principle ===
# เปิดสำหรับ extension, ปิดสำหรับ modification
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process(self, amount: float) -> bool:
pass
class CreditCardProcessor(PaymentProcessor):
def process(self, amount: float) -> bool:
print(f"Processing via credit card")
return True
class PayPalProcessor(PaymentProcessor):
def process(self, amount: float) -> bool:
print(f"Processing via PayPal")
return True
class CryptoProcessor(PaymentProcessor):
def process(self, amount: float) -> bool:
print(f"Processing via crypto")
return True
# เพิ่ม payment method ใหม่โดยไม่ต้องแก้ code เดิม
class PaymentService:
def __init__(self, processor: PaymentProcessor):
self.processor = processor
def pay(self, amount: float):
return self.processor.process(amount)
# === L: Liskov Substitution Principle ===
# Subclass ต้องใช้แทน parent class ได้
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
# ใช้ Shape ได้โดยไม่สนว่าเป็น Rectangle หรือ Circle
def print_area(shape: Shape):
print(f"Area: {shape.area():.2f}")
# === I: Interface Segregation Principle ===
# แยก interface ให้เล็ก ไม่บังคับ implement สิ่งที่ไม่ต้องการ
class Readable(ABC):
@abstractmethod
def read(self) -> str: pass
class Writable(ABC):
@abstractmethod
def write(self, data: str): pass
class Deletable(ABC):
@abstractmethod
def delete(self): pass
# implement เฉพาะที่ต้องการ
class ReadOnlyFile(Readable):
def read(self) -> str:
return "file content"
class FullAccessFile(Readable, Writable, Deletable):
def read(self) -> str:
return "file content"
def write(self, data: str):
print(f"Writing: {data}")
def delete(self):
print("File deleted")
# === D: Dependency Inversion Principle ===
# depend on abstractions, not concretions
class Logger(ABC):
@abstractmethod
def log(self, message: str): pass
class FileLogger(Logger):
def log(self, message: str):
print(f"[FILE] {message}")
class CloudLogger(Logger):
def log(self, message: str):
print(f"[CLOUD] {message}")
class Application:
def __init__(self, logger: Logger): # depend on abstraction
self.logger = logger
def run(self):
self.logger.log("Application started")
# Swap implementations easily
app = Application(FileLogger())
app.run()
app = Application(CloudLogger())
app.run()
Iterative Design Process ด้วย Code
ตัวอย่างการออกแบบแบบ iterative
#!/usr/bin/env python3
# iterative_design.py — Iterative Design Example
# === Iteration 1: Simplest Thing That Works ===
# เริ่มจาก simple function
def calculate_price_v1(base_price):
"""V1: Just return the price"""
return base_price
# === Iteration 2: Add Discount ===
# Requirements เพิ่ม: ต้องรองรับ discount
def calculate_price_v2(base_price, discount_percent=0):
"""V2: Added discount support"""
discount = base_price * (discount_percent / 100)
return base_price - discount
# === Iteration 3: Refactor to Class ===
# Requirements เพิ่ม: หลาย discount types, tax, currency
from dataclasses import dataclass
from typing import List, Optional
from abc import ABC, abstractmethod
@dataclass
class Product:
name: str
price: float
category: str
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, price: float) -> float:
pass
class PercentageDiscount(DiscountStrategy):
def __init__(self, percent: float):
self.percent = percent
def calculate(self, price: float) -> float:
return price * (self.percent / 100)
class FixedDiscount(DiscountStrategy):
def __init__(self, amount: float):
self.amount = amount
def calculate(self, price: float) -> float:
return min(self.amount, price)
class BuyOneGetOneDiscount(DiscountStrategy):
def calculate(self, price: float) -> float:
return price * 0.5
class TaxCalculator:
TAX_RATES = {
"TH": 0.07,
"US": 0.08,
"UK": 0.20,
"JP": 0.10,
}
def calculate(self, price: float, country: str = "TH") -> float:
rate = self.TAX_RATES.get(country, 0.07)
return price * rate
class PriceCalculator:
def __init__(self):
self.tax_calculator = TaxCalculator()
def calculate(self, product: Product,
discounts: Optional[List[DiscountStrategy]] = None,
country: str = "TH",
include_tax: bool = True) -> dict:
base_price = product.price
total_discount = 0.0
if discounts:
for discount in discounts:
total_discount += discount.calculate(base_price - total_discount)
subtotal = base_price - total_discount
tax = self.tax_calculator.calculate(subtotal, country) if include_tax else 0
total = subtotal + tax
return {
"product": product.name,
"base_price": round(base_price, 2),
"discount": round(total_discount, 2),
"subtotal": round(subtotal, 2),
"tax": round(tax, 2),
"total": round(total, 2),
}
# Usage
calc = PriceCalculator()
product = Product("Laptop", 35000, "electronics")
result = calc.calculate(
product,
discounts=[PercentageDiscount(10), FixedDiscount(500)],
country="TH",
)
print(f"{result['product']}: {result['total']:,.2f} THB")
# === Iteration 4: Add Validation and Events ===
class PriceEvent:
def __init__(self, event_type, data):
self.event_type = event_type
self.data = data
self.timestamp = __import__("datetime").datetime.utcnow()
class EventBus:
def __init__(self):
self._handlers = {}
def subscribe(self, event_type, handler):
self._handlers.setdefault(event_type, []).append(handler)
def publish(self, event: PriceEvent):
for handler in self._handlers.get(event.event_type, []):
handler(event)
# Design evolves with requirements
# Each iteration adds just enough complexity
Design Patterns ใน Agile Development
Patterns ที่ใช้บ่อยใน Agile projects
#!/usr/bin/env python3
# agile_patterns.py — Common Design Patterns in Agile
# === 1. Repository Pattern ===
# Decouple data access from business logic
from abc import ABC, abstractmethod
from typing import Optional, List
from dataclasses import dataclass
@dataclass
class Task:
id: Optional[int]
title: str
status: str = "pending"
priority: int = 0
class TaskRepository(ABC):
@abstractmethod
def save(self, task: Task) -> Task: pass
@abstractmethod
def find_by_id(self, task_id: int) -> Optional[Task]: pass
@abstractmethod
def find_all(self, status: Optional[str] = None) -> List[Task]: pass
@abstractmethod
def delete(self, task_id: int) -> bool: pass
class InMemoryTaskRepository(TaskRepository):
def __init__(self):
self._tasks = {}
self._next_id = 1
def save(self, task: Task) -> Task:
if task.id is None:
task.id = self._next_id
self._next_id += 1
self._tasks[task.id] = task
return task
def find_by_id(self, task_id: int) -> Optional[Task]:
return self._tasks.get(task_id)
def find_all(self, status: Optional[str] = None) -> List[Task]:
tasks = list(self._tasks.values())
if status:
tasks = [t for t in tasks if t.status == status]
return tasks
def delete(self, task_id: int) -> bool:
return self._tasks.pop(task_id, None) is not None
# SQLite implementation (swap without changing business logic)
# class SQLiteTaskRepository(TaskRepository):
# def __init__(self, db_path):
# self.conn = sqlite3.connect(db_path)
# ...
# === 2. Service Layer Pattern ===
class TaskService:
def __init__(self, repo: TaskRepository):
self.repo = repo
def create_task(self, title: str, priority: int = 0) -> Task:
if not title.strip():
raise ValueError("Title cannot be empty")
task = Task(id=None, title=title.strip(), priority=priority)
return self.repo.save(task)
def complete_task(self, task_id: int) -> Task:
task = self.repo.find_by_id(task_id)
if not task:
raise ValueError(f"Task {task_id} not found")
task.status = "completed"
return self.repo.save(task)
def get_pending_tasks(self) -> List[Task]:
return self.repo.find_all(status="pending")
# === 3. Observer Pattern ===
class EventEmitter:
def __init__(self):
self._listeners = {}
def on(self, event: str, callback):
self._listeners.setdefault(event, []).append(callback)
def emit(self, event: str, data=None):
for callback in self._listeners.get(event, []):
callback(data)
class TaskManager:
def __init__(self, service: TaskService):
self.service = service
self.events = EventEmitter()
def create(self, title: str) -> Task:
task = self.service.create_task(title)
self.events.emit("task:created", task)
return task
def complete(self, task_id: int) -> Task:
task = self.service.complete_task(task_id)
self.events.emit("task:completed", task)
return task
# === 4. Builder Pattern (fluent API) ===
class QueryBuilder:
def __init__(self, table: str):
self._table = table
self._conditions = []
self._order = []
self._limit = None
def where(self, condition: str):
self._conditions.append(condition)
return self
def order_by(self, column: str, direction: str = "ASC"):
self._order.append(f"{column} {direction}")
return self
def limit(self, n: int):
self._limit = n
return self
def build(self) -> str:
sql = f"SELECT * FROM {self._table}"
if self._conditions:
sql += " WHERE " + " AND ".join(self._conditions)
if self._order:
sql += " ORDER BY " + ", ".join(self._order)
if self._limit:
sql += f" LIMIT {self._limit}"
return sql
query = (QueryBuilder("tasks")
.where("status = 'pending'")
.where("priority > 3")
.order_by("created_at", "DESC")
.limit(10)
.build())
print(query)
# Usage
repo = InMemoryTaskRepository()
service = TaskService(repo)
manager = TaskManager(service)
manager.events.on("task:created", lambda t: print(f"Created: {t.title}"))
manager.events.on("task:completed", lambda t: print(f"Done: {t.title}"))
task = manager.create("Implement login")
manager.complete(task.id)
Testing-Driven Design ใน Agile
TDD workflow สำหรับ Agile Design
#!/usr/bin/env python3
# tdd_example.py — Test-Driven Design Example
import unittest
from dataclasses import dataclass
from typing import List, Optional
# === TDD Cycle: Red -> Green -> Refactor ===
# Step 1: Write a failing test (RED)
# Step 2: Write minimal code to pass (GREEN)
# Step 3: Refactor while keeping tests green (REFACTOR)
# === Shopping Cart TDD Example ===
@dataclass
class CartItem:
name: str
price: float
quantity: int = 1
class ShoppingCart:
def __init__(self):
self._items: List[CartItem] = []
def add_item(self, name: str, price: float, quantity: int = 1):
if price < 0:
raise ValueError("Price cannot be negative")
if quantity < 1:
raise ValueError("Quantity must be at least 1")
for item in self._items:
if item.name == name:
item.quantity += quantity
return
self._items.append(CartItem(name, price, quantity))
def remove_item(self, name: str):
self._items = [i for i in self._items if i.name != name]
@property
def item_count(self) -> int:
return sum(item.quantity for item in self._items)
@property
def subtotal(self) -> float:
return sum(item.price * item.quantity for item in self._items)
def apply_discount(self, percent: float) -> float:
if percent < 0 or percent > 100:
raise ValueError("Discount must be 0-100")
discount = self.subtotal * (percent / 100)
return round(self.subtotal - discount, 2)
@property
def items(self) -> List[CartItem]:
return self._items.copy()
def clear(self):
self._items = []
class TestShoppingCart(unittest.TestCase):
def setUp(self):
self.cart = ShoppingCart()
# === Basic functionality ===
def test_new_cart_is_empty(self):
self.assertEqual(self.cart.item_count, 0)
self.assertEqual(self.cart.subtotal, 0)
def test_add_single_item(self):
self.cart.add_item("Laptop", 35000)
self.assertEqual(self.cart.item_count, 1)
self.assertEqual(self.cart.subtotal, 35000)
def test_add_multiple_items(self):
self.cart.add_item("Laptop", 35000)
self.cart.add_item("Mouse", 500)
self.assertEqual(self.cart.item_count, 2)
self.assertEqual(self.cart.subtotal, 35500)
def test_add_item_with_quantity(self):
self.cart.add_item("Cable", 200, quantity=3)
self.assertEqual(self.cart.item_count, 3)
self.assertEqual(self.cart.subtotal, 600)
def test_add_same_item_increases_quantity(self):
self.cart.add_item("Pen", 50)
self.cart.add_item("Pen", 50, quantity=2)
self.assertEqual(self.cart.item_count, 3)
# === Edge cases ===
def test_negative_price_raises_error(self):
with self.assertRaises(ValueError):
self.cart.add_item("Bad", -100)
def test_zero_quantity_raises_error(self):
with self.assertRaises(ValueError):
self.cart.add_item("Bad", 100, quantity=0)
# === Remove items ===
def test_remove_item(self):
self.cart.add_item("Laptop", 35000)
self.cart.add_item("Mouse", 500)
self.cart.remove_item("Laptop")
self.assertEqual(self.cart.item_count, 1)
self.assertEqual(self.cart.subtotal, 500)
def test_remove_nonexistent_item(self):
self.cart.remove_item("Nothing")
self.assertEqual(self.cart.item_count, 0)
# === Discount ===
def test_apply_discount(self):
self.cart.add_item("Laptop", 10000)
total = self.cart.apply_discount(10)
self.assertEqual(total, 9000)
def test_invalid_discount_raises_error(self):
self.cart.add_item("Item", 1000)
with self.assertRaises(ValueError):
self.cart.apply_discount(150)
# === Clear cart ===
def test_clear_cart(self):
self.cart.add_item("A", 100)
self.cart.add_item("B", 200)
self.cart.clear()
self.assertEqual(self.cart.item_count, 0)
if __name__ == "__main__":
unittest.main(verbosity=2)
เครื่องมือและ Workflow สำหรับ Agile Design
เครื่องมือที่ช่วย Agile Design
# === Agile Design Tools and Workflow ===
# 1. Code Quality Tools
# ===================================
# Python: ruff (fast linter + formatter)
pip install ruff
ruff check . # Lint
ruff format . # Format
ruff check --fix . # Auto-fix
# Pre-commit hooks
# .pre-commit-config.yaml
# repos:
# - repo: https://github.com/astral-sh/ruff-pre-commit
# rev: v0.3.0
# hooks:
# - id: ruff
# args: [--fix]
# - id: ruff-format
#
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.8.0
# hooks:
# - id: mypy
pip install pre-commit
pre-commit install
# 2. Architecture Decision Records (ADRs)
# ===================================
# docs/adr/001-use-postgresql.md
# # 1. Use PostgreSQL as Primary Database
#
# ## Status: Accepted
# ## Context
# We need a reliable database for our application.
# ## Decision
# Use PostgreSQL for its ACID compliance, JSON support, and ecosystem.
# ## Consequences
# - Need PostgreSQL expertise on team
# - Good tooling and community support
# - May need read replicas for scale
# 3. Sprint Design Workflow
# ===================================
# Sprint Planning:
# 1. Review user stories for the sprint
# 2. Quick design discussion (15-30 min max)
# 3. Identify design patterns needed
# 4. Create technical tasks
#
# During Sprint:
# 1. Write tests first (TDD)
# 2. Implement simplest solution
# 3. Refactor as patterns emerge
# 4. Code review focusing on design
#
# Sprint Review:
# 1. Demo working software
# 2. Review design decisions
# 3. Update ADRs if needed
# 4. Plan refactoring for next sprint
# 4. Refactoring Checklist
# ===================================
# Before refactoring:
# - [ ] All tests pass
# - [ ] Identified code smells
# - [ ] Planned target design
#
# During refactoring:
# - [ ] Small steps (compile/test between each)
# - [ ] One refactoring at a time
# - [ ] Run tests after each change
#
# After refactoring:
# - [ ] All tests still pass
# - [ ] No new functionality added
# - [ ] Code is simpler/cleaner
# - [ ] Committed with descriptive message
# 5. Code Review Checklist for Design
# ===================================
# - [ ] Single Responsibility: Does each class/function do one thing?
# - [ ] Naming: Are names descriptive and consistent?
# - [ ] Dependencies: Are they injected, not hard-coded?
# - [ ] Testability: Can this be tested in isolation?
# - [ ] Duplication: Is there copy-paste code?
# - [ ] Complexity: Is there a simpler approach?
# - [ ] Error handling: Are errors handled appropriately?
echo "Agile Design workflow configured"
FAQ คำถามที่พบบ่อย
Q: Agile Design กับ No Design ต่างกันอย่างไร?
A: Agile Design ไม่ใช่การไม่ออกแบบ แต่เป็นการออกแบบ just enough สำหรับ current iteration ใช้ TDD, refactoring และ design patterns อย่างมีวินัย No Design คือการเขียน code ไปเรื่อยๆ ไม่คิด structure ไม่ refactor ทำให้ได้ code ที่ maintain ยาก Agile Design ให้ code ที่ clean, testable และ maintainable ผ่าน continuous improvement
Q: เมื่อไหร่ควรใช้ Big Design Up Front (BDUF)?
A: BDUF เหมาะสำหรับ systems ที่เปลี่ยนแปลงยาก (embedded systems, hardware interfaces), safety-critical systems (medical devices, aviation), ระบบที่มี regulatory requirements เข้มงวด และ projects ที่ requirements ชัดเจนและไม่เปลี่ยนแปลง สำหรับ web applications, mobile apps และ SaaS products ที่ requirements เปลี่ยนบ่อย Agile Design เหมาะกว่า
Q: จะรู้ได้อย่างไรว่าต้อง refactor?
A: สัญญาณที่บอกว่าต้อง refactor ได้แก่ duplicate code (copy-paste), long methods (>20 lines), large classes (>200 lines), deep nesting (>3 levels), feature envy (class ใช้ data ของ class อื่นมากกว่าของตัวเอง), shotgun surgery (เปลี่ยน feature ต้องแก้หลายที่) และ test difficulty (ยากที่จะเขียน unit test) ใช้ metrics tools เช่น radon (Python) หรือ SonarQube เพื่อวัด code complexity
Q: SOLID Principles ใช้ทุก project ไหม?
A: ไม่จำเป็น สำหรับ scripts เล็กๆ หรือ prototypes SOLID อาจ over-engineering ได้ ใช้ SOLID เมื่อ project มีหลายคนทำ, code จะ maintain ระยะยาว, ต้องการ testability สูง และมี business logic ซับซ้อน เริ่มจาก Single Responsibility ก่อนเพราะให้ผลมากที่สุด แล้วค่อยใช้ principles อื่นเมื่อจำเป็น อย่า apply blindly ทุก principle กับทุก class
