Bài 0: Nguyên lý nền tảng — Trước khi vào code¶
Lưu ý: Hãy nhâm nhi bài này trước khi bạn bắt tay vào gõ dòng code đầu tiên ở Bài 1. Ở đây, chúng ta sẽ không bàn về syntax (cú pháp) hay project, mà chỉ tập trung vào từ vựng (vocabulary) và tư duy thiết kế (mental model). Những nguyên lý cốt lõi này sẽ "ám ảnh" bạn trong suốt chặng đường từ Bài 1 đến Bài 10.
Tại sao phải học nguyên lý, thay vì chỉ học vẹt các pattern?¶
| Mẫu thiết kế (Pattern) | Nguyên lý (Principle) | |
|---|---|---|
| Bản chất | Là câu trả lời đóng gói sẵn cho một bài toán cụ thể. | Là tư duy gốc rễ giúp bạn tự tìm ra (hoặc chọn đúng) pattern. |
| Ví dụ | "Dùng kiến trúc 3 lớp: Router → Service → Repo". | Separation of Concerns (Tách biệt mối quan tâm) + Dependency Inversion (Đảo ngược phụ thuộc). |
| Khả năng áp dụng | Chỉ copy-paste được khi gặp bài toán y hệt. | Áp dụng được mọi lúc mọi nơi (kể cả khi bạn code GraphQL, Microservices, hay Realtime...). |
Thử tưởng tượng, nếu ngày mai bạn chuyển sang một dự án khác (ví dụ: dùng GraphQL không có khái niệm "Router", hay kiến trúc Microservice nơi "Repository" thực chất là một lời gọi gRPC), những pattern cũ bạn thuộc lòng có thể trở nên vô dụng. Nhưng nếu bạn nắm vững nguyên lý, bạn hoàn toàn có thể tự mình thiết kế lại một cấu trúc mới "chuẩn bài" cho bất kỳ hoàn cảnh nào.
Bộ nguyên lý trong bài này chính là ngôn ngữ giao tiếp chung của giới kỹ sư phần mềm. Nếu không nắm rõ, bạn sẽ chỉ biết diễn đạt kiểu "tôi chia 3 tầng giống project KCDS". Nhưng khi đã hiểu, bạn sẽ giao tiếp chuyên nghiệp hẳn lên: "Ở đoạn này, tôi dùng DIP để cô lập logic nghiệp vụ khỏi database".
1. Separation of Concerns (SoC) — Tách biệt mối quan tâm¶
Định nghĩa nôm na: Cứ mỗi module, class, hoặc function thì chỉ nên gánh vác duy nhất một trách nhiệm tách biệt. Đừng biến nó thành một "tạp hóa" cái gì cũng bán.
Các "mối quan tâm" (concern) phổ biến trong một ứng dụng web:
- Phân tích HTTP (bóc tách request/response)
- Kiểm tra logic nghiệp vụ (business validation)
- Truy vấn cơ sở dữ liệu (database query)
- Tương tác với ổ cứng (File I/O)
- Xác thực người dùng (Authentication)
- Ghi log / Theo dõi sức khỏe hệ thống (Observability)
Dấu hiệu bạn đang làm sai (Anti-pattern):
def handle_upload(request):
# HTTP: parse body
# Validation: check tên file
# DB: query dung lượng đã dùng
# Business: check xem có vượt quá giới hạn lưu trữ không
# File: ghi file vào ổ cứng
# DB: insert data mới
# HTTP: trả về response
→ Nhìn xem, có tới 6 công việc khác nhau bị nhồi nhét vào chung 1 hàm. Việc viết test cho hàm này sẽ là một cơn ác mộng vì bạn phải giả lập (mock) toàn bộ 5 phần còn lại chỉ để test 1 phần. Hơn nữa, sửa một chỗ rất dễ làm đứt gãy những chỗ khác.
Cách nhận biết vi phạm SoC: Hãy tự hỏi: "Nếu dự án đổi framework Web, đổi database, hoặc sếp đổi luật kinh doanh, cái function này có bị ảnh hưởng không?" Nếu câu trả lời là "Có" cho nhiều hơn 1 yếu tố → nó đang ôm đồm quá nhiều việc rồi.
Nguyên lý này đang ẩn nấp ở đâu trong bộ guide KCDS:
- Bài 2: FastAPI chỉ làm đúng phận sự của một tiếp tân (lo chuyện HTTP), phần code của bạn lo việc xử lý bên trong.
- Bài 4: Sự ra đời của kiến trúc 3 lớp: Router (HTTP) / Service (Nghiệp vụ) / Repo (Lưu trữ).
- Bài 5 (Frontend): Tách bạch rõ ràng giữa
api.js(lo gọi HTTP) / biến state (lo logic) /render()(lo vẽ giao diện). - Bài 6 (Bot): File
scheduler.py(lo bộ đếm thời gian) vànotifier.py(lo việc gửi tin nhắn) được tách riêng biệt.
2. DRY — Don't Repeat Yourself (Đừng lặp lại chính mình)¶
Định nghĩa chuẩn xác: Mỗi một mảng kiến thức hoặc logic cốt lõi trong hệ thống chỉ nên có một nguồn sự thật duy nhất (single source of truth).
Hiểu sai lầm phổ biến: "Cứ thấy hai đoạn code gõ giống nhau là phải lập tức gộp lại (refactor) thành một hàm chung." Hiểu đúng bản chất: "Logic giống nhau thì gộp lại. Nhưng nếu code giống nhau chỉ mang tính ngẫu nhiên thì hãy cứ để yên đấy."
Ví dụ về việc code giống nhau, nhưng logic khác nhau — KHÔNG được gộp:
def calculate_tax(price):
return price * 0.1 # Thuế VAT 10%
def calculate_tip(price):
return price * 0.1 # Tiền tip mặc định 10%
Nếu bạn nổi hứng "lười" và gộp chúng thành một cái hàm chung tên là multiply_by_10_percent(), đến khi nhà nước tăng thuế VAT lên 15%, tiền tip của nhân viên cũng tự động nhảy lên 15% theo. Hai hàm này dù code y hệt nhau, nhưng mang ngữ nghĩa và lý do thay đổi hoàn toàn khác biệt.
Áp dụng trong dự án KCDS:
- Bài 5:
api.jslà nơi hội tụ duy nhất để gọi HTTP. Ngày mai sếp bảo đổi domain API, bạn chỉ cần sửa đúng 1 chỗ là xong (DRY chuẩn mực). - Bài 2: File
deps.pychứa các hàm khởi tạo (factory) được xài chung cho từ 2 router trở lên. - Repository: Hàm
create()vàupdate()nhìn lướt qua khá giống nhau, nhưng ý nghĩa nghiệp vụ lại khác xa nhau → TUYỆT ĐỐI KHÔNG gộp.
Quy tắc đi kèm — Luật quá tam ba bận (Rule of Three): Sự lặp lại xuất hiện đến lần thứ 3 thì mới nên xem xét viết lại (abstract). Nếu mới lặp lại 2 lần: copy-paste vẫn là chân ái nếu ngữ nghĩa của chúng khác nhau.
3. YAGNI — You Aren't Gonna Need It (Bạn sẽ chẳng cần đến nó đâu)¶
Định nghĩa: Đừng bao giờ nai lưng ra viết code để đón đầu cho những tình huống/tính năng mà bạn nghĩ là tương lai có thể sẽ cần đến.
Làm sai (Anti-pattern):
class AudioService:
def __init__(self, repo, cache=None, metrics=None, feature_flags=None):
# Hì hục truyền sẵn cache, metrics, feature_flags... "phòng khi sau này dự án phình to"
Cái giá phải trả của việc "viết lố" (over-engineering): Không chỉ tốn thời gian gõ phím. Code thừa sinh ra bug ẩn, ép bạn phải viết thêm test vô bổ, viết thêm docs. Đồng nghiệp vào sau đọc code sẽ hoa mắt vì không hiểu đống logic thừa mứa kia phục vụ cho cái tính năng quái quỷ nào. Tệ nhất là, khi yêu cầu thực tế ập đến, cái thiết kế "đón đầu" của bạn thường trật lất, và bạn lại ngậm ngùi đập đi xây lại từ đầu.
Áp dụng trong dự án KCDS:
- Bài 3: Không thèm dùng ORM phức tạp vì hiện tại xài Core là đủ giải quyết vấn đề.
- Bài 5: Dùng Vanilla JS thuần thay vì vác dao mổ trâu React/Vue ra làm, vì dự án admin này chỉ lèo tèo vài ba màn hình.
- Bài 4 & Bài 9: "Những file script chỉ chạy 1 lần thì cứ viết thẳng tuột từ trên xuống dưới, chia 3 layer làm gì cho rườm rà."
Triết lý của dự án: "Đừng đẻ thêm tính năng, đừng vội refactor, và đừng nhét thêm bất kỳ tầng trừu tượng nào nếu task hiện tại không bắt buộc." — Đó chính là đỉnh cao của YAGNI.
4. SOLID — 5 trụ cột của Lập trình Hướng đối tượng (OOP)¶
S — Single Responsibility Principle (Đơn trách nhiệm)¶
"Một class hoặc function chỉ nên có một lý do duy nhất để thay đổi." SRP chính là phiên bản thu nhỏ của SoC áp dụng cho cấp độ class/function.
- Nếu
AudioServicephải sửa code vì quy tắc upload nhạc thay đổi → Rất hợp lý (1 lý do). - Nhưng nếu
AudioServicelại phải sửa code vì API gửi tin nhắn của Discord cập nhật → Bạn đã vi phạm SRP.
O — Open/Closed Principle (Đóng / Mở)¶
"Mở rộng thì thoải mái (Open), nhưng hạn chế sửa đổi code cũ (Closed)." Khi sếp yêu cầu thêm tính năng mới → viết code mới, đừng vọc vạch làm vỡ code cũ đang chạy ổn định.
# ❌ Vi phạm: Cứ mỗi lần công ty thêm kênh gửi tin mới là bạn phải chui vào sửa hàm này
def send_notification(type, data):
if type == "email": ...
elif type == "sms": ...
elif type == "discord": ...
# ✅ Tuân thủ: Khai báo class mới, tuyệt đối không đụng chạm đến phần code cũ
class Notifier(ABC):
@abstractmethod
def send(self, data): ...
class EmailNotifier(Notifier): ...
class DiscordNotifier(Notifier): ... # Hôm sau có kênh mới thì cứ thế tạo thêm class
L — Liskov Substitution Principle (Thay thế Liskov)¶
"Class con phải thay thế được class cha ở mọi ngóc ngách mà không gây ra lỗi hay hành vi lươn lẹo nào."
Nếu SqliteAudioRepository kế thừa AudioRepository, thì phần code gọi nó (Tầng Service) không cần biết (và cũng không được phép biết) bên dưới thực chất là SQLite. Không được phép có những mánh khóe kiểu: "Ê Service, gọi hàm này đi, nhưng lưu ý hàm này tui viết riêng chỉ chạy được nếu dùng SQLite thôi nhé".
I — Interface Segregation Principle (Chia nhỏ Interface)¶
"Nhiều interface nhỏ, chuyên biệt sẽ tốt hơn vạn lần một interface khổng lồ ôm đồm mọi thứ."
Thay vì làm một cái IRepository "to tổ chảng" chứa đủ 20 hàm thao tác, hãy chẻ nó ra thành IReadable (chỉ đọc), IWritable (chỉ ghi), IDeletable (chỉ xóa). Chỗ nào cần cái gì thì lấy cái đó.
Dự án KCDS cực kỳ tuân thủ nguyên lý này: Chúng ta có AudioRepository, CategoryRepository, UserRepository hoạt động độc lập, chứ không nhồi chung vào một cái IMegaRepository nào cả.
D — Dependency Inversion Principle (Đảo ngược phụ thuộc)¶
"Hãy phụ thuộc vào thứ trừu tượng (interface), đừng phụ thuộc vào kẻ làm thuê cụ thể (implementation)." Bài 4 chính là ví dụ hoàn hảo nhất cho nguyên lý này:
class AudioService:
def __init__(self, repo: AudioRepository): # Phụ thuộc vào cái vỏ hợp đồng (ABC), chứ không phụ thuộc vào ruột (SqliteAudioRepository)
self.repo = repo
(Lưu ý nhẹ: DIP là nguyên lý tư duy. Còn DI - Dependency Injection ở Bài 2 - là công cụ/phương tiện để bạn thực hiện DIP).
5. Inversion of Control (IoC) — Đảo ngược luồng điều khiển¶
Luồng điều khiển ngây thơ (Bình thường):
Luồng điều khiển của tay chơi thứ thiệt (IoC):
Việc của bạn chỉ là ngoan ngoãn khai báo các hàm theo đúng định dạng (interface, decorator, event...) mà framework yêu cầu. Sau đó, framework sẽ là người cầm trịch, tự quyết định khi nào thì bóp cò gọi hàm của bạn. Giới giang hồ hay gọi đây là Nguyên lý Hollywood: "Đừng gọi cho chúng tôi, chúng tôi sẽ tự gọi cho bạn khi cần".
Các cơ chế IoC hay gặp nhất:
| Cơ chế | Chỗ áp dụng trong dự án KCDS |
|---|---|
| Dependency Injection (DI) | Bài 2 — FastAPI đóng vai quản gia tự nhét (inject) các món đồ nghề vào hàm của bạn. |
| Event handlers | Bài 6 — @client.event async def on_ready — Framework tự réo tên hàm này khi bot lên sóng. |
| Decorators / Hooks | @router.get("/...") — Framework tự gọi hàm này khi có khách truy cập đúng đường dẫn URL. |
| Middleware | FastAPI middleware — Framework tự động chèn code của bạn chạy trước/sau mỗi request. |
Mẹo đi phỏng vấn: Khi nghe chém gió "Framework này hỗ trợ IoC", hãy tỉnh táo hỏi lại: "À, thế nó hỗ trợ IoC thông qua cơ chế cụ thể nào vậy?"
6. Fail Fast vs Fail Soft¶
Fail Fast (Đứt cầu chì sớm)¶
"Một khi đã lỗi là phải sập ngay lập tức, đừng để mầm mống ấp ủ làm hỏng cả hệ thống."
# ❌ Thất bại muộn (Fail late - Ôm bom nổ chậm)
def config():
return os.getenv("DB_URL", "") # Cố tình thiếu cấu hình DB mà app vẫn cứ nhơn nhơn chạy
def connect():
return create_engine(config()) # Cả nửa ngày sau, có khách truy cập mới nổ lỗi sập server
# ✅ Thất bại sớm (Fail fast - Cắt điện cầu chì)
def validate():
if not os.getenv("DB_URL"):
raise RuntimeError("Phải có cấu hình DB_URL mới cho chạy")
# Rào hàm này ngay lúc vừa bật server. Thiếu cấu hình là tịt luôn không cho khởi động.
Fail Soft (Xử lý mềm mỏng)¶
Cứng quá thì gãy, không phải lỗi nào cũng lôi nhau ra cho sập hệ thống. Đối với các tiến trình chạy ngầm dằng dặc (long-running process), ta phải cài cắm cơ chế tự phục hồi:
- Bài 6: Khi API của Discord bị rớt mạng chốc lát (
_TICK_RECOVERABLE), con bot chỉ âm thầm ghi log lại rồi chạy tiếp vòng lặp sau chứ không được phép chết ngỏm. - Bài 6: Hàm
notifier.pygửi tin nhắn thông báo thất bại → Cứ bơ đi mà sống, tuyệt đối không được làm sập tính năng phát nhạc cốt lõi.
Bí kíp thực chiến:
- Chọn Fail fast khi: Thiếu cấu hình gốc, sai logic code trầm trọng (KeyError), vi phạm luồng xử lý chuẩn → Cho sập để dev thấy mà fix ngay.
- Chọn Fail soft khi: Lỗi hạ tầng mạng chập chờn, hoặc mấy tính năng râu ria (như gửi thông báo) bị rớt.
7. Principle of Least Surprise (POLS) — Nguyên lý ít gây bất ngờ nhất¶
"Code của bạn phải hoạt động đúng y chang những gì đồng nghiệp kỳ vọng khi họ đọc cái tên của nó."
- Nếu hàm tên là
get_user(nghĩa là lấy thông tin user) → tuyệt đối không được lén lút cập nhật thời gian đăng nhập hay chèn thêm dữ liệu vào DB (side effect). - Hàm có dán mác
async def→ thiên hạ đều kỳ vọng bên trong nó sẽ có nhường quyền cho hệ thống (await I/O). Đừng dại dột nhét cái hàmtime.sleep()vào đó chặn đứng cả cái server. - Biến tên
total_size→ người ta kỳ vọng nó là con số (int byte), đừng có trả về một chuỗi string ất ơ kiểu"100MB".
Bạn đã dẫm đạp lên POLS khi code của bạn khiến đồng nghiệp review xong phải thảng thốt: "Ủa, sao cái hàm tên thế này mà ruột lại làm thêm cái trò kia?".
8. Composition over Inheritance — Ưu tiên Lắp ráp hơn Kế thừa¶
# ❌ Kế thừa (Inheritance) — Dễ tạo ra một gia phả rối như tơ vò
class AudioService(BaseService, LoggingMixin, CachingMixin, ValidationMixin):
... # Rốt cuộc nhìn vào không biết AudioService bản chất nó là cái gì? Rất khó giải thích.
# ✅ Lắp ráp (Composition) — Rõ ràng, dễ thay đổi linh kiện như chơi Lego
class AudioService:
def __init__(self, repo, logger, cache, validator):
self.repo = repo # Service "sở hữu" (has-a) một kho lưu trữ
self.logger = logger # Service "sở hữu" một bộ ghi log
Kế thừa tạo ra sự ràng buộc bằng xương bằng thịt (tight coupling) — class con phải bú bám vào class cha. Ngược lại, Lắp ráp (Composition) cho phép bạn nhổ "linh kiện" này ra và thay bằng "linh kiện" khác một cách cực kỳ mượt mà ngay trong lúc app đang chạy (runtime).
Để ý mà xem, dự án KCDS này gần như không đụng đến Kế thừa (ngoại trừ xài nó để dựng khung interface ABC). Tất cả các Service đều được xây lên bằng triết lý Lắp ráp.
9. Explicit is Better Than Implicit — Rõ ràng hơn là Ngầm định (Phép thuật)¶
Trích từ kinh thánh "Thiền của Python" (Zen of Python): "Rõ ràng, minh bạch thì tốt hơn là ngầm định (phép thuật)."
- Bài 3: Chúng ta quyết định chọn SQLAlchemy Core thay vì xài ORM bóng bẩy vì câu SQL gõ ra nhìn thấy ngay rành rành, dễ debug hơn vạn lần cái kiểu để ORM tự sinh code ngầm đằng sau.
- Bài 4: Quyết định việc ghi file trước hay DB trước là quy tắc sinh tử của nghiệp vụ. Ta viết rõ ràng trình tự đó ra bằng code trong Service, chứ không giấu nhẹm nó sau một cái
@decorator_magicnào đó.
Sự đánh đổi: Chấp nhận viết code rõ ràng đôi khi khiến file dài hơn một chút (verbose), nhưng bù lại nó triệt tiêu được sự "phép thuật" — giúp code dễ review, dễ debug và thân thiện với anh em newbie vào sau hơn rất nhiều.
10. Bài tập — Bắt mạch chẩn đoán nguyên lý¶
Hãy lướt qua đoạn code sau và tinh mắt tìm xem gã dev này đang vi phạm những nguyên lý nào:
class UserService:
def __init__(self):
self.db = SqliteDatabase("users.db") # Gắn cứng DB
def register(self, email, password):
# Kiểm tra tính hợp lệ
if "@" not in email:
raise ValueError("Email sai")
# Mã hóa mật khẩu
hashed = hashlib.md5(password.encode()).hexdigest()
# Lưu vào cơ sở dữ liệu
self.db.execute("INSERT ...")
# Gửi email chào mừng
smtp = SMTP("mail.example.com")
smtp.send(email, "Welcome!")
# Ghi log ra màn hình
print(f"Registered {email}")
# Trả về chuỗi JSON
return json.dumps({"email": email})
Bắt mạch chẩn đoán:
- SoC / SRP: Mới có mỗi một hàm
registermà ôm đồm tới 5 việc (validate, băm mật khẩu, gọi DB, gửi mail, format JSON). Quá sức tạp nham! - DIP: Gắn chết (
hardcode) cái ruột SqliteDatabase và giao thức SMTP vào thẳng trong hàm. Khỏi có đường nào mà viết Unit Test tráo đồ giả (mock) cho hàm này được. - POLS: Hàm nghiệp vụ
registercớ sao lại ói ra một chuỗi JSON? Người ta kỳ vọng nó trả về đối tượng user (dict/model), cái vụ nhào nặn ra JSON là phận sự của tầng Router. - Fail Fast / Observability: Xài lệnh
printphèn chúa thay vì dùng hệ thốngloggingtiêu chuẩn. - Security (Phụ thêm): Dám lấy MD5 đi băm mật khẩu thì đúng là thảm họa bảo mật (thời đại này người ta xài bcrypt/argon2 hết rồi).
Hướng cấu trúc lại (Refactor) cho sạch sẽ:
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 # Ngoan ngoãn trả về dict, Router sẽ tự lo việc xào nấu thành JSON
Câu hỏi tự kiểm tra (Trả lời được thì pass)¶
- Cứ hễ thấy code trùng là phang DRY, có khi nào trò này mâu thuẫn với YAGNI (Đừng làm lố) không? Cho ví dụ thử xem?
- Giữa SoC và SRP ranh giới nằm ở đâu? Điểm cốt lõi khác biệt là gì?
- IoC và DI có dính dáng họ hàng gì với nhau? Bạn có kể tên được một cơ chế IoC nào khác ngoài DI không?
- Đứt cầu chì sớm (Fail-fast) nghe thì oai, nhưng tại sao không phải lúc nào cũng xài được?
- DIP và Liskov đều tôn thờ tính trừu tượng — vậy hai đứa nó tung hứng, bổ sung cho nhau như thế nào?
Đọc tiếp¶
Đến đây, kho vũ khí từ vựng và tư duy của bạn đã được nạp đầy đủ. Bạn hoàn toàn sẵn sàng tiến thẳng vào Bài 2. Bật mí nhỏ: Ở cuối mỗi bài học (từ Bài 2 đến Bài 8), bạn sẽ thấy một mục mang tên "Nguyên lý tổng quát" — đó là lúc chúng ta làm động tác "ánh xạ" những dòng code thực tế vừa học về lại gốc rễ của những nguyên lý nằm trong bài này.
Riêng Bài 9 (Testing) và Bài 10 (Trade-off) sẽ là thao trường khốc liệt nhất để bạn rèn luyện sự linh hoạt trong việc tung hứng các nguyên lý này trước những bài toán thiết kế cam go!