Bài 0: Nguyên lý nền tảng — Trước khi vào code¶
Đọc trước bài 1. Bài này không có code project — chỉ là vocabulary và mental model. Mọi nguyên lý ở đây sẽ xuất hiện lặp lại trong bài 1-10.
Tại sao học nguyên lý, không chỉ học pattern?¶
| Pattern | Nguyên lý | |
|---|---|---|
| Là gì | Câu trả lời cụ thể cho 1 bài toán cụ thể | Cách suy nghĩ để chọn/tạo pattern |
| Ví dụ | "3 layer Router→Service→Repo" | Separation of Concerns + Dependency Inversion |
| Chuyển giao | Copy được khi bài toán giống | Áp dụng được kể cả khi bối cảnh đổi (GraphQL, microservice, realtime...) |
Khi bạn sang project khác (GraphQL không có "Router", microservice "Repository" là gRPC call), pattern cũ không copy được. Nhưng nguyên lý vẫn áp dụng — bạn tự thiết kế cấu trúc phù hợp.
Bộ nguyên lý trong bài này là vocabulary chung của kỹ sư phần mềm. Không học thì mãi nói "tôi chia 3 tầng giống KCDS" thay vì "tôi áp dụng DIP để isolate business logic".
1. Separation of Concerns (SoC)¶
Định nghĩa: Mỗi module/class/function chỉ lo 1 "concern" — 1 mảng kiến thức tách biệt.
Concern phổ biến trong web app:
- HTTP parsing (request/response)
- Business validation (rule nghiệp vụ)
- Database query
- File I/O
- Authentication
- Logging / observability
Anti-pattern:
def handle_upload(request):
# HTTP: parse body
# Validation: check filename
# DB: query storage usage
# Business: check storage limit
# File: write disk
# DB: insert row
# HTTP: return response
→ 6 concerns trong 1 function. Test 1 concern đòi hỏi setup 5 cái kia. Thay đổi 1 concern có thể phá vỡ cái khác.
Nhận biết SoC bị vi phạm: Function thay đổi khi HTTP framework đổi, khi DB đổi, khi business rule đổi, khi logging đổi → nó có quá nhiều concerns.
Áp dụng ở đâu trong bộ guide:
- Bài 2: FastAPI lo HTTP, code của bạn lo logic
- Bài 5: Router (HTTP) / Service (business) / Repo (persistence) → 3 concerns → 3 layers
- Bài 6:
api.js(HTTP) / module state (logic) /render()(DOM) - Bài 7:
scheduler.py(timing + mutex) /notifier.py(Discord message) — 2 concerns tách
2. DRY — Don't Repeat Yourself¶
Định nghĩa (chính xác): Mỗi piece of knowledge chỉ có 1 nguồn đáng tin (single source of truth).
Hiểu sai phổ biến: "Code giống nhau → refactor ngay." Hiểu đúng: "Logic giống nhau → consolidate. Code giống nhau ngẫu nhiên → để yên."
Ví dụ code giống, logic khác — KHÔNG nên merge:
def calculate_tax(price):
return price * 0.1 # VAT 10%
def calculate_tip(price):
return price * 0.1 # tip mặc định 10%
Nếu merge thành multiply_by_10_percent() — khi VAT đổi sang 15% thì tip cũng bị đổi sai. Hai hàm có ngữ nghĩa khác dù code giống.
Áp dụng trong project:
- Bài 6:
api.jscentralized HTTP client — đổi base URL chỉ sửa 1 chỗ (DRY đúng, 1 kiến thức "how to call API") - Bài 3:
deps.pycho factory dùng ≥ 2 router (DRY đúng) - Repository:
create()vàupdate()trông tương tự nhưng ngữ nghĩa khác → KHÔNG merge
Nguyên tắc đồng hành — Rule of Three: Duplication xuất hiện lần 3 mới nên abstract. 2 lần: copy OK nếu ngữ nghĩa khác nhau (như ví dụ tax/tip trên) — ít nhất còn thấy "2 chỗ khác nhau" để hiểu intent. Nhưng copy chỉ vì lười biếng — không có lý do về ngữ nghĩa — thì vẫn là DRY violation.
3. YAGNI — You Aren't Gonna Need It¶
Định nghĩa: Đừng code cho use case chưa chắc sẽ có.
Anti-pattern:
class AudioService:
def __init__(self, repo, cache=None, metrics=None, feature_flags=None):
# cache, metrics, feature_flags chưa dùng — "phòng khi cần"
Cost thật của over-engineering không chỉ là viết nhiều code hơn — mà là nhiều bug hơn, nhiều test hơn, nhiều tài liệu hơn cần maintain. Code khó đọc hơn vì người đọc không hiểu tại sao thứ đó tồn tại. Tệ hơn nữa, khi requirement thực sự đến, abstraction đã chọn trước thường không đúng — lúc đó phải refactor lại từ đầu.
Áp dụng trong project:
- Bài 4: Không dùng ORM vì chưa phức tạp
- Bài 6: Vanilla JS thay React vì < 20 màn hình
- Bài 5: "Script 1 lần không cần 3 layer"
CLAUDE.md của project có câu chốt: "Don't add features, refactor, or introduce abstractions beyond what the task requires." — YAGNI codified.
4. SOLID — 5 nguyên lý OOP¶
S — Single Responsibility Principle¶
"Class/function chỉ có 1 lý do để thay đổi."
SRP là SoC ở scale nhỏ hơn (class/function). Ví dụ:
AudioServicethay đổi khi business rule upload đổi → OK (1 lý do)- Nếu
AudioServicecũng thay đổi khi Discord API đổi → vi phạm SRP
O — Open/Closed Principle¶
"Mở cho extension, đóng cho modification."
Thêm feature → thêm code mới, không sửa code cũ.
# ❌ Vi phạm: thêm type mới phải sửa function
def send_notification(type, data):
if type == "email": ...
elif type == "sms": ...
elif type == "discord": ... # sửa hàm cũ
# ✅ Tuân thủ: thêm class mới, không đụng code cũ
class Notifier(ABC):
@abstractmethod
def send(self, data): ...
class EmailNotifier(Notifier): ...
class DiscordNotifier(Notifier): ... # class mới
L — Liskov Substitution Principle¶
"Subclass phải dùng được ở mọi nơi parent class được dùng — không có surprise."
Nếu SqliteAudioRepository extend AudioRepository, code dùng AudioRepository không được cần biết đó là Sqlite. Không có trick "method này chỉ Sqlite impl support".
Vi phạm Liskov thực tế: List → ImmutableList → subclass nhưng raise exception khi gọi .append(). Code dùng List sẽ crash.
I — Interface Segregation Principle¶
"Nhiều interface nhỏ > 1 interface to."
Thay vì IRepository với 20 method, tách IReadable (list, get), IWritable (create, update), IDeletable (delete). Client chỉ implement/depend cái cần.
Project này ngầm tuân thủ: AudioRepository, CategoryRepository, UserRepository — mỗi interface nhỏ, tương ứng 1 entity, không có IMegaRepository.
D — Dependency Inversion Principle¶
"Phụ thuộc vào abstraction, không phụ thuộc vào concrete."
Bài 5 là ví dụ DIP thuần:
class AudioService:
def __init__(self, repo: AudioRepository): # ABC, không phải SqliteAudioRepository
self.repo = repo
→ Service không biết đang dùng SQLite hay Postgres. Đổi DB → không cần sửa service.
Liên kết: DIP là nguyên lý. DI (Dependency Injection) là cơ chế hiện thực DIP. Bài 3 chính là DI — cách FastAPI inject dependency vào handler.
5. Inversion of Control (IoC) — DI chỉ là 1 hiện thực¶
Control flow thông thường:
Control flow đảo ngược:
Bạn định nghĩa các hàm theo đúng interface mà framework yêu cầu — decorator, event handler, hay constructor signature — rồi để framework tự quyết định khi nào gọi chúng. Đây là "Hollywood Principle": Don't call us, we'll call you.
Các cơ chế IoC phổ biến:
| Cơ chế | Ví dụ trong project |
|---|---|
| Dependency Injection | Bài 3 — framework inject dependency vào handler |
| Event handlers | Bài 7 — @client.event async def on_ready — framework gọi khi ready |
| Decorators / Hooks | @router.get("/...") — framework gọi khi URL match |
| Template method | Scheduler._before_loop() — tasks.loop gọi 1 lần trước |
| Middleware | FastAPI middleware — framework gọi trước/sau handler |
Khi nói "framework có IoC", hãy cụ thể: IoC qua cơ chế gì?
6. Fail Fast vs Fail Soft¶
Fail Fast¶
"Lỗi phải nổ ra sớm nhất có thể."
# ❌ Fail late
def config():
return os.getenv("DB_URL", "") # rỗng cũng OK, app chạy
def connect():
return create_engine(config()) # lỗi ở đây — 30 phút sau startup
# ✅ Fail fast
def validate():
if not os.getenv("DB_URL"):
raise RuntimeError("DB_URL bắt buộc")
# Gọi trong lifespan startup → app không start nếu thiếu config
Áp dụng trong project: Bài 2 config.validate() + Bài 8 JWT_SECRET check.
Fail Soft (khi nào chọn)¶
Không phải mọi lỗi đều nên crash. Với long-running process, cần recovery:
- Bài 7
_TICK_RECOVERABLE— catchaiosqlite.Error,discord.HTTPException... và log + tiếp tục - Bài 7
notifier.py— Discord send lỗi → chỉ log, không crash playback
Nguyên tắc:
- Fail fast với: config thiếu, bug logic (KeyError, AttributeError), invariant vi phạm → crash để phát hiện
- Fail soft với: I/O transient (network, DB tạm down), side effect không critical (notification)
7. Principle of Least Surprise (POLS)¶
"Code nên làm điều người đọc mong đợi."
- Hàm tên
get_user→ không được có side effect (không insert DB) - Hàm
async def→ người đọc mong đợi I/O, không nên cótime.sleep()blocking - Biến
total_size→ int bytes, không phải string"100MB" - Constructor không raise exception (trừ khi invariant vi phạm)
POLS là nguyên lý về API design. Khi code của bạn khiến reader phải "à, thì ra nó làm X" — bạn đã vi phạm POLS.
8. Composition over Inheritance¶
"Khi cần reuse, thích composition hơn inheritance."
# Inheritance — dễ vướng deep hierarchy
class AudioService(BaseService, LoggingMixin, CachingMixin, ValidationMixin):
... # AudioService "is-a" cái gì? Khó giải thích
# Composition — rõ ràng, flexible
class AudioService:
def __init__(self, repo, logger, cache, validator):
self.repo = repo # "has-a" repository
self.logger = logger # "has-a" logger
Lý do: Inheritance là tight coupling — subclass biết quá nhiều về parent. Composition cho phép thay đổi thành phần runtime.
Project này rất ít dùng inheritance ngoài ABC (interface). Tất cả service đều composition.
9. Explicit is Better Than Implicit¶
Zen of Python: "Explicit is better than implicit."
- Bài 4 chọn Core thay ORM vì SQL explicit hơn, debug dễ hơn
- Bài 5 transaction order là business rule → viết rõ trong service, không ẩn vào decorator
- Bài 3
Depends(get_session)rõ ràng hơn auto-inject via class annotation
Trade-off: Explicit hơi verbose nhưng giảm "magic" — code dễ review, dễ debug.
10. Bài tập — Nhận dạng nguyên lý¶
Đọc code sau, xác định vi phạm/tuân thủ nguyên lý nào:
class UserService:
def __init__(self):
self.db = SqliteDatabase("users.db") # hardcode
def register(self, email, password):
# Validate shape
if "@" not in email:
raise ValueError("Email sai")
# Hash password
hashed = hashlib.md5(password.encode()).hexdigest()
# Insert DB
self.db.execute("INSERT ...")
# Send email
smtp = SMTP("mail.example.com")
smtp.send(email, "Welcome!")
# Log
print(f"Registered {email}")
# Return JSON for HTTP
return json.dumps({"email": email})
Vi phạm:
- SoC / SRP — 1 method làm 5 concerns (validation, hashing, DB, email, HTTP format)
- DIP — hardcode
SqliteDatabasevàSMTP, không nhận qua constructor → không test được - POLS —
registertrả JSON string? Reader mong đợi dict/model, để router format - Fail Fast / Observability —
printthaylogging - Security (bonus) — MD5 cho password (dùng bcrypt/argon2)
Refactor gợi ý:
class UserService:
def __init__(self, repo: UserRepository, mailer: Mailer, hasher: Hasher):
self.repo = repo
self.mailer = mailer
self.hasher = hasher
async def register(self, session, email: str, password: str) -> dict:
if "@" not in email:
raise ValueError("Email sai")
user = await self.repo.create(session, email=email, hashed=self.hasher.hash(password))
await self.mailer.send_welcome(email)
logger.info("Registered user %s", email)
return user # dict, router tự format JSON
Câu hỏi tự kiểm tra¶
- DRY và YAGNI có khi mâu thuẫn không? Ví dụ?
- SoC và SRP khác nhau chỗ nào?
- IoC và DI liên quan thế nào? Ngoài DI còn cơ chế IoC nào?
- Tại sao fail-fast không phải lúc nào cũng đúng?
- DIP và Liskov cùng liên quan đến abstraction — chúng bổ sung nhau thế nào?
Đọc tiếp¶
Với vocabulary đã có, bạn sẵn sàng vào bài 2. Mỗi bài 2-8 có section "Nguyên lý tổng quát" ở cuối — map pattern cụ thể về principle đã học ở đây.
Bài 9 (Testing) và bài 10 (Trade-off) sẽ dùng rộng rãi các principle này.