Bỏ qua

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.js centralized 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.py cho factory dùng ≥ 2 router (DRY đúng)
  • Repository: create()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ụ:

  • AudioService thay đổi khi business rule upload đổi → OK (1 lý do)
  • Nếu AudioService cũ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:

Your code → calls → Library

Control flow đảo ngược:

Framework → calls → Your code

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 — catch aiosqlite.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:

  1. SoC / SRP — 1 method làm 5 concerns (validation, hashing, DB, email, HTTP format)
  2. DIP — hardcode SqliteDatabaseSMTP, không nhận qua constructor → không test được
  3. POLSregister trả JSON string? Reader mong đợi dict/model, để router format
  4. Fail Fast / Observabilityprint thay logging
  5. 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

  1. DRYYAGNI có khi mâu thuẫn không? Ví dụ?
  2. SoCSRP khác nhau chỗ nào?
  3. IoCDI liên quan thế nào? Ngoài DI còn cơ chế IoC nào?
  4. Tại sao fail-fast không phải lúc nào cũng đúng?
  5. DIPLiskov 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.