Bài 10: Đánh đổi & Cạm bẫy (Trade-off & Anti-patterns) — Nghệ thuật "Phá luật"¶
Đọc ở cuối chặng đường. Bài này sẽ không dạy bạn thêm dòng code hay công nghệ mới nào cả. Nhiệm vụ của nó là dạy bạn khi nào KHÔNG NÊN áp dụng những kỹ xảo tuyệt đỉnh mà bạn vừa học từ Bài 1 đến Bài 9.
Pattern là câu trả lời, nhưng Trade-off mới là câu hỏi¶
Nhiều bạn Junior mới học được vài Design Pattern là mang đi "spam" khắp mọi nơi, bất chấp hoàn cảnh. Giới lập trình gọi đây là căn bệnh "Cargo Cult Programming" — code theo hệ tâm linh, làm theo hình thức một cách mù quáng mà không hiểu bản chất tại sao lại làm vậy.
Điểm khác biệt lớn nhất của một Senior Developer là họ luôn nhìn vào bối cảnh (context) trước. Họ biết cách chọn lựa, hoặc thậm chí là phá bỏ pattern dựa trên sự Đánh đổi (Trade-off).
Mọi quyết định thiết kế hệ thống đều có cái giá của nó (cost). Đừng bao giờ hỏi: "Pattern này viết thế này đã chuẩn chưa?". Hãy hỏi: "Trong hoàn cảnh dự án này, cái giá phải trả cho pattern này có đáng không?"
1. Kiến trúc 3 Lớp (Bài 5) — Khi nào thì phản tác dụng?¶
Cứ vứt 3 lớp đi nếu bạn gặp:¶
A. Những Script chạy đúng 1 lần (như tool dọn rác, import data, hay migration):
# ✅ Cách làm chuẩn thực dụng — Script chạy 1 lần rồi vứt
async def migrate_v2():
async with aiosqlite.connect(DB) as conn:
await conn.execute("ALTER TABLE audio ADD COLUMN tags TEXT")
await conn.commit()
Đẻ ra 3 lớp Router/Service/Repo ở đây chỉ tạo ra một đống code rác (boilerplate) vô nghĩa. Không có quy tắc nghiệp vụ nào ở đây để mà cô lập cả!
B. Những API siêu đơn giản (Chỉ chọc DB lấy lên, không có logic):
# API khám sức khỏe hệ thống — chả có rule gì sất
@router.get("/health")
async def health():
return {"status": "ok"}
# Lấy danh sách thuần túy — Gọi 1 câu SELECT là xong
@router.get("/categories")
async def list_categories(session=Depends(get_session)):
result = await session.execute(select(categories))
return {"data": [dict(r._mapping) for r in result.fetchall()]}
Nếu Tầng Service sinh ra chỉ để làm "người chuyển phát nhanh" (nhận từ Router rồi ném nguyên xi cho Repo) mà không đóng góp thêm tí giá trị (value) nào → Dẹp luôn Service cho nhẹ đầu. (Trong dự án KCDS, mình giữ lại đôi chỗ cho đồng bộ đội hình, nhưng nếu bạn quyết định bỏ thì vẫn hoàn toàn hợp lý).
C. Dự án Prototype / MVP (Sản phẩm khả dụng tối thiểu) quá nhỏ:
Sếp cho 2 tuần làm bản Demo xem thị trường có thích không (chưa tới 3 cái bảng DB). Hãy gom hết code vào 1-2 file cho nhanh! Khi nào có khách hàng thật, dự án chứng minh được giá trị (Product-Market fit) thì lôi ra chia layer sau.
Dấu hiệu cho thấy 3-layer đang làm khổ bạn:¶
- Hàm ở Service chả làm gì ngoài việc return lại đúng cái hàm của Repo.
- Repo lại có hàm copy y chang logic của Service.
- Mỗi lần khách hàng đổi yêu cầu, bạn phải mở cả 3 file Router, Service, Repo ra sửa cùng một lúc.
→ Đây là tiếng chuông báo động: Bạn đang chia lớp trên giấy tờ, chứ không chia tách theo Mối quan tâm thực sự (Separation of Concerns). Hãy mạnh dạn gộp chúng lại.
2. SQLAlchemy Core (Bài 4) — Khi nào thì "hết phép"?¶
Project KCDS chọn Core vì: Cơ sở dữ liệu SQLite quá đơn giản, việc kiểm soát từng câu SQL là cần thiết và quan trọng nhất là chặn đứng được căn bệnh "nổ query ngầm" (bẫy N+1).
Vậy khi nào nên quay xe sang ORM?¶
| Hoàn cảnh | Nỗi đau nếu ráng xài Core |
|---|---|
| Bảng nối bảng chằng chịt (User → Posts → Comments → Likes) | Core bắt bạn phải tự gõ JOIN thủ công, tự group dữ liệu bằng tay. Code cực kỳ dài dòng và dễ nhầm lẫn. |
| Kế thừa đa hình (Polymorphism) | ORM có phép thuật polymorphic_on làm mọi thứ tự động, Core bắt bạn phải vã mồ hôi gõ lệnh UNION. |
| Cần quản lý Transaction cực rối rắm (Unit of Work) | ORM có cái "bản đồ bộ nhớ" (identity map) tự theo dõi data, dùng Core thì bạn phải tự bơi. |
| Team bạn toàn siêu thủ Django/Rails chuyển sang | Bắt họ học Core là đi ngược lại đường cong học tập, năng suất team sẽ chạm đáy. |
Khi nào thì "sút" luôn cả SQLAlchemy (Bỏ cả Core lẫn ORM)?¶
- Ứng dụng lèo tèo 1-2 cái bảng, chọc trực tiếp bằng thư viện
aiosqlitegọn hơn nhiều. - Hệ thống phân tích dữ liệu siêu to khổng lồ (OLAP), toàn dùng hàm Window functions dị biệt → Tốt nhất là dùng
asyncpggõ câu SQL thuần (raw string) để tối ưu tới từng mili-giây. - Đổi sang dùng NoSQL (MongoDB, Redis) thì dĩ nhiên chia tay SQLAlchemy.
Chốt lại: Đừng xài SQLAlchemy chỉ vì giang hồ bảo nó là "Best Practice". Hãy dùng vì nó giải quyết được nỗi đau hiện tại của bạn.
3. Vanilla JS (Bài 6) — Ranh giới của sự mệt mỏi¶
Dấu hiệu báo động: Đã đến lúc gọi tên React/Vue!¶
| Triệu chứng | Căn bệnh | Thuốc đặc trị |
|---|---|---|
| Dự án phình ra > 20 màn hình, form với thẻ (card) xài lại liên tục. | Thiếu cơ chế gom nhóm (Component reuse). | React / Vue / Svelte |
| Quá nhiều module tự "bắn loa" (CustomEvent) cho nhau nghe. | State bị lỏng lẻo, khó dò lỗi (Coupling ngầm). | Redux / Pinia / Zustand |
Gọi hàm render() mà giật tung chảo. |
Vẽ lại (re-render) toàn bộ thẻ Div thay vì đắp thêm phần nhỏ (Diffing). | Cơ chế Virtual DOM |
| Form quá nhiều ô nhập, rule validate chằng chịt. | Gắn biến bằng tay (Manual binding) quá mệt mỏi. | React Hook Form / VeeValidate |
| Web bán hàng, cần lên Top 1 Google (SEO). | Cần Server render sẵn HTML (SSR). | Next.js / Nuxt.js |
Cạm bẫy kinh điển: Vanilla JS + jQuery + Nhồi <script> vào HTML¶
Làm ơn hiểu rõ: Bài 6 hướng dẫn bạn dùng Vanilla JS nhưng là ES Modules kết hợp sức mạnh Build của Vite. Nếu bạn viết theo kiểu ném một đống code JS thẳng vào thẻ <script> ở giữa file HTML, trộn thêm tí jQuery cho vui → Dự án của bạn sẽ sớm trở thành một bãi rác không ai muốn bảo trì!
4. JWT (Bài 8) — Ánh hào quang lừa dối¶
Điểm mặt sự Đánh đổi của JWT¶
| Ánh sáng (Ưu điểm) | Bóng tối (Nhược điểm) |
|---|---|
| Phi trạng thái (Stateless) — Server không tốn RAM nhớ user. | Phóng lao là phải theo lao — Không thể thu hồi token ngay lập tức (phải đợi nó tự hết hạn). |
| Dễ đẻ thêm Server (Scale ngang) không sợ đồng bộ session. | Hacker bắt được token ném vào base64 là đọc sạch Payload. |
| Băng qua nhiều hệ thống dễ dàng (Web, App, Microservices). | User ấn "Đổi mật khẩu" nhưng máy khác có token cũ vẫn vô tư xài tiếp (Trừ khi đẻ thêm logic versioning cực lằng nhằng). |
Khi nào Session Cookie (Lưu DB/Redis) "ăn đứt" JWT?¶
- App nội bộ, chỉ chạy trên 1-2 con Server → Xài Session Redis nhanh gọn, đuổi việc nhân viên là click chuột phát khóa nick ngay.
- Yêu cầu bảo mật cao (Đổi pass là đá văng mọi thiết bị khác) → JWT làm vụ này rất khổ, Session làm trong nửa nốt nhạc.
- Cần theo dõi xem User A đang online trên mấy cái điện thoại → Session có sẵn list này cho bạn ngó.
Vụ Refresh Token — Có thật sự cần?¶
Trong project KCDS, mình để JWT sống dai tận 7 ngày. Hết 7 ngày user tự văng ra bắt đăng nhập lại.
Nhưng ra ngoài làm Production thực tế, hệ thống lớn thường áp dụng combo:
- Access token (Ngắn hạn - 15 phút): Dùng để đi cổng. Lỡ bị lộ thì 15 phút sau cũng thành rác.
- Refresh token (Dài hạn - 30 ngày): Cất kỹ trong DB. Dùng để xin Access token mới. Lộ phát Admin có thể xóa trong DB để thu hồi quyền ngay.
Tại sao project KCDS không làm? Vì mình chọn một Giải pháp thực dụng (Pragmatic): Xài JWT để không tốn công cài Redis, nhưng Quyền hạn (Role) thì chọc thẳng vào DB lấy ra mỗi lần gọi API (Xem bài 8 §3). Vậy là Admin vừa có thể cắt quyền user ngay tắp lự, vừa không phải cồng kềnh setup hệ thống Refresh Token.
5. Async/Await (Bài 2, 6) — Đừng ảo tưởng sức mạnh¶
Ghi nhớ thần chú: Async CHỈ GIÚP bạn khi hệ thống đang "Ngồi chờ" (I/O wait - đợi tải file, đợi DB). Nếu hệ thống đang phải dùng não (ngốn CPU), Async hoàn toàn vô dụng và sẽ chặn đứng toàn bộ Server!
❌ Cạm bẫy: Ép CPU làm việc nặng trong hàm Async¶
# ❌ Sai lầm chết người: Bắt cả cái Event loop đứng đợi nửa giây!
@router.post("/compress")
async def compress(file: UploadFile):
data = await file.read()
# Hàm này ngốn CPU cực mạnh và chạy đồng bộ.
# Toàn bộ các User khác gửi request vào sẽ bị treo chờ theo!
compressed = zlib.compress(data, level=9)
return {"size": len(compressed)}
✅ Giải pháp: Ném việc nặng cho thằng khác (Thread Pool)¶
@router.post("/compress")
async def compress(file: UploadFile):
data = await file.read()
# Ném cục tạ sang một Thread khác để giải quyết. Event loop được giải phóng!
compressed = await asyncio.to_thread(zlib.compress, data, 9)
return {"size": len(compressed)}
(Mở rộng: Nếu việc quá nặng, bị vướng giới hạn GIL của Python, bạn phải ném nó sang một Process khác qua ProcessPoolExecutor).
Khi nào thì đập bỏ Async, xài Sync thuần?¶
- Ứng dụng của bạn chả gọi mạng mộc gì, toàn làm toán (Render Video, AI Inference, Đọc file Excel khổng lồ).
- Đội dev toàn người mới, nhìn Async không hiểu, thả bug kẹt xe (deadlock) lung tung → Gõ Sync cho dễ debug.
- Bạn định gánh tải bằng cách bật 100 cái Process/Container lên thay vì tối ưu Concurrency.
Tin vui: FastAPI chấp cả hai! Bạn viết hàm def bình thường (không có chữ async), FastAPI sẽ tự động nhét nó vào một Threadpool chạy an toàn.
6. Dependency Injection (Bài 3) — "Vẽ rắn thêm chân" (Premature Abstraction)¶
Căn bệnh "DI hóa" mọi thứ¶
# ❌ Rảnh rỗi sinh nông nổi
def get_logger() -> Logger: return logging.getLogger(__name__)
def get_datetime_now() -> Callable: return datetime.now
def get_uuid() -> Callable: return uuid.uuid4
@router.get("")
async def handler(
logger=Depends(get_logger),
now=Depends(get_datetime_now),
uid=Depends(get_uuid),
):
...
Nhìn có vẻ "pro" nhưng thực ra cực kỳ ngớ ngẩn. Bạn DI để làm gì? Để dễ viết Test?
- Muốn test giờ hệ thống? Dùng thư viện
freezegun. - Muốn test log? Dùng
caplogcủa pytest.
Đừng mang dao mổ trâu đi gọt táo. Không ai dùng DI cho các hàm có sẵn của Python cả.
Hãy dùng DI ĐÚNG CHỖ:¶
- Các hệ thống bên ngoài (Gọi HTTP, Database, Queue).
- Lớp nghiệp vụ (Repository, EmailSender, Cổng thanh toán).
- Nạp cấu hình (Config) để dễ tráo đổi môi trường lúc test.
Dẹp ngay DI nếu đó là:¶
- Hàm tính toán thuần túy (
hashlib,json). - Thư viện tiêu chuẩn đã có sẵn đồ chơi để test (
datetime,randomchỉ cần fix seed). - Những hằng số bất biến.
7. Lớp Trừu tượng (Abstract Base Class - Bài 5) — Khi nào 1 Vỏ + 1 Ruột là đủ?¶
# ❌ Bài ca làm màu
class AudioRepository(ABC):
# Tự nhiên đẻ ra cái Abstract class, trong khi dự án chỉ xài đúng mỗi thằng SqliteAudioRepository
@abstractmethod
async def list_all(self, session): ...
Nếu bạn dám thề độc là dự án này cả đời chỉ xài SQLite, đẻ ra ABC là overhead rác rưởi. Kiểu gõ linh hoạt (duck-typing) của Python là quá đủ.
Nhưng tại sao Project này vẫn khăng khăng xài ABC?
- Nền tảng viết Test: Có cái vỏ ABC thì truyền đồ giả (
Mock) vào mới kiểm chứng được độ chính xác (Xem bài 9 §4). - Làm hợp đồng: Vỏ ABC đóng vai trò như một bản tài liệu rõ ràng, thằng Service nhìn vào là biết có những hàm gì để gọi.
- Đường lui: Một dự án MVP bằng SQLite có xác suất rất cao sẽ bị sếp ép đổi sang Postgres khi nó thành công.
Chốt: Nếu bạn có ít nhất 2/3 lý do trên → Hãy dùng ABC. Nếu chỉ có 1 lý do → Đừng bày vẽ.
8. Polling 2s với SQLite (Bài 7) — Giới hạn sức chịu đựng¶
Project của chúng ta cho phép con Bot cứ 2 giây lại chọc vào bảng bot_commands một lần. Nó hoạt động hoàn hảo vì: Chỉ có 1 admin, 1 con bot, lâu lâu mới bấm 1 lệnh.
Nhưng trò này sẽ sụp đổ khi:¶
| Hiện tượng | Nguyên nhân |
|---|---|
| Admin bấm play, chờ hơn 2s nhạc mới lên (UX dở tệ). | Do giới hạn của Polling interval (thời gian trễ giữa 2 lần quét). |
| Bạn bật lên 3 con Bot cùng lúc để chia tải. | Cả 3 con cùng nhào vô giành lấy 1 cái lệnh → Race condition, đọc trùng lệnh, xử lý lặp. |
| Hệ thống có hàng trăm lệnh bắn vào mỗi giây. | DB SQLite kêu cứu vì bị tranh khóa (Lock contention) liên tục. |
Đã đến lúc mang "Đồ chơi nặng" ra:¶
- Redis Pub/Sub — Độ trễ gần bằng 0, tha hồ scale thêm bao nhiêu Bot cũng được.
- PostgreSQL LISTEN/NOTIFY — Hàng xịn có sẵn của Postgres, khỏi cài Redis.
- Message Queue chuyên nghiệp (RabbitMQ, SQS) — Đảm bảo tin nhắn không bao giờ rơi rớt, gửi được 1 lúc cho cả ngàn con Bot.
- WebSocket — Bắt Admin panel "nháy máy" trực tiếp xuống Bot.
Lý do KCDS vẫn xài SQLite: Sự đơn giản. Độ trễ 2s là hoàn toàn chấp nhận được với ứng dụng này. Không ai đẻ thêm con quái vật Redis ra để quản lý chỉ vì sợ cái rủi ro chưa bao giờ tới. Đây chính là YAGNI (Bạn không cần nó đâu).
9. Rollback: Ghi File trước hay ghi DB trước? (Bài 5)¶
Project này chốt thứ tự sinh tử:
- Lúc Upload: Lưu File ra ổ cứng TRƯỚC, lưu tên vào DB SAU. Nếu DB báo lỗi → Lập tức phi ra ổ cứng xóa file đi (
unlink). - Lúc Xóa bài: Xóa data trong DB TRƯỚC, xóa File trên ổ cứng SAU. Nếu File bị lỗi kẹt không xóa được → Bỏ qua, chỉ ghi Log (chấp nhận trên ổ cứng dư ra 1 cái file rác).
Vậy có khi nào làm ngược lại mới đúng?¶
A. Ghi DB TRƯỚC, quăng File lên ổ cứng SAU:
- Phù hợp khi bạn đẩy file lên tận S3 hoặc Cloud. Đẩy lên rồi mà phải xóa đi (rollback) thì tốn tiền băng thông mạng.
- Lúc này, nếu nhét file thất bại, bạn chấp nhận trong DB có một cái Record bị "mồ côi" (Không có file thật). Ứng dụng phải tự biết cách lơ nó đi.
B. Xóa File TRƯỚC, chém DB SAU:
- Phù hợp với luật pháp (như GDPR của châu Âu) — Yêu cầu dữ liệu cá nhân phải CHẮC CHẮN bị xóa khỏi hệ thống, sau đó mới được lưu log (DB) là đã làm việc đó.
- Khi dung lượng ổ đĩa của bạn tính tiền theo từng GB (Cloud bill xót ruột).
Chốt: Không có đúng hay sai tuyệt đối. Bạn phải tự lấy cân ra đo: Tiền băng thông đắt hơn hay cục rác trong ổ cứng ngứa mắt hơn?
10. Copy-Paste và Cám dỗ "Gộp code" (Abstraction)¶
Chép 2 lần: Xin cứ tự nhiên!¶
# Chỗ A cần lấy user
user = await user_repo.get_by_email(session, email)
if not user: raise HTTPException(404)
# Chỗ B ở một Service khác cũng cần y chang
user = await user_repo.get_by_email(session, email)
if not user: raise HTTPException(404)
Hai đoạn code giống hệt nhau. Bạn ngứa tay muốn viết thành 1 hàm gộp? KHOAN! Cứ để nó trùng nhau đi.
Chép đến lần thứ 3: Hãy dừng lại suy nghĩ¶
# Đến chỗ C vẫn lặp lại cái điệp khúc này...
user = await user_repo.get_by_email(session, email)
if not user: raise HTTPException(404)
OK, lúc này hệ thống mới "nháy đèn đỏ" xác nhận đây là một logic kinh điển. Đã đến lúc đẻ ra hàm await get_user_or_404(session, email).
Tại sao phải kiềm chế? Viết hàm gộp (Abstract) quá sớm thường tạo ra một cái hàm... trật lất. Có 3 ví dụ thì bạn mới đủ góc nhìn để bao quát vấn đề.
Dấu hiệu bạn gộp code quá đà (Abstraction thảm họa):¶
- Cái hàm xài chung của bạn nhận vào một rổ tham số tùy chọn (Optional param), mỗi chỗ xài lại truyền một kiểu.
- Trong hàm chứa đầy bẫy cờ hiệu (Flag) kiểu
if should_log: ...,if use_cache: ... - Đặt tên hàm vô thưởng vô phạt:
process_data(),handle_request().
→ Bạn đang cố nhét 3 bài toán khác nhau vào chung một cái rọ. Gỡ nó ra ngay!
11. Dám "vả" vào mặt SOLID có chủ đích¶
S — Đơn trách nhiệm (Single Responsibility)¶
Bạn nói: "Cái hàm AudioService.upload() vừa kiểm tra logic, vừa ghi ổ cứng, vừa chọc DB. Vi phạm SRP rõ ràng!".
Không hề! Ở góc độ người quản lý (Business level), "Tải lên một bài nhạc" là một trách nhiệm duy nhất. Nếu bạn cố cắt nhỏ nó ra thành FileWriter, MetadataInserter... thì bạn chỉ đang rước thêm sự cồng kềnh (verbose) vào người.
Bài học: Tính đơn trách nhiệm (SRP) phải được soi ở góc độ Nghiệp vụ, không phải bẻ vụn ra thành các thao tác kỹ thuật nhỏ lẻ.
O — Đóng/Mở (Open/Closed)¶
Luật nói: Thêm tính năng mới cấm được sửa code cũ. Nhưng ở dự án MVP này, nếu tốn 3 phút sửa thẳng vào đoạn code cũ mà giải quyết được vấn đề, nó ngon hơn gấp 10 lần việc ngồi đẻ ra một rổ Abstract Classes với Factory Patterns phức tạp. OCP chỉ thực sự sinh tử khi bạn viết thư viện (Library) cho người khác xài. Còn đóng cửa bảo nhau trong nội bộ team → YAGNI (Sự tối giản) thắng!
D — Đảo ngược phụ thuộc (Dependency Inversion)¶
Bạn cắm chết SqliteAudioRepository thẳng vào Router? Xong, vi phạm DIP!
Nhưng thực tế, nếu cả đời cái Router đó không bao giờ xài chung với thằng Repo nào khác → DIP ở đây không giải quyết khâu "thay thế dễ dàng". Trong KCDS, mình áp dụng DIP là để Viết Test (tráo đồ giả vào), chứ không phải để lôi ra khoe khoang kiến trúc!
13. Lưu File trên ổ cứng (Stateful) vs Bắn lên Mây (Stateless)¶
Project lưu file nhạc thẳng vào /data/audio/ trong Server (Bài 5). Siêu nhanh, siêu dễ, chạy 1 con Server thì không đối thủ.
Nhưng cái giá phải trả (Server biến thành Stateful):
- Đừng hòng reset hay đập đi xây lại cái Container Docker mà không gắn Ổ cứng ngoài (Volume), bay màu dữ liệu ngay!
- Muốn đẻ thêm con Server thứ 2 (Scale ngang) là toang, Server 2 đâu có nhìn thấy file của Server 1.
- Nâng cấp phiên bản mới kiểu Blue-Green cực kỳ chua chát.
Đã đến lúc quẹt thẻ xài S3 (Object Storage)?¶
| Bóp cò khi nào? | Lý do hợp lý |
|---|---|
| Bạn bắt buộc phải chạy 2 con Server trở lên. | Scale ngang thì Server phải "nhẹ nhõm" (Stateless), quăng hết gánh nặng lưu trữ ra ngoài. |
| Bạn bỏ Server vào hệ thống Kubernetes (K8s). | Ổ cứng trong K8s bị xóa trắng bất cứ lúc nào nó hứng lên. Bắt buộc phải xài S3 hoặc ổ đĩa mạng. |
| Dự án phình ra > 1TB. | Mua dung lượng S3 rẻ hơn nhiều so với việc nâng cấp ổ SSD cứng của VPS. |
| File nhạc/video giật lag, cần Mạng phân phối (CDN). | S3 dán chung với Cloudflare/CloudFront là combo quốc dân. |
Nhưng khi nào thì cứ cắm cọc ở Local cho lành?¶
- Bạn chỉ xài đúng 1 con VPS nghèo nàn — Giống hệt dự án KCDS.
- Dữ liệu lèo tèo vài chục GB, ngàn năm mới thêm một bài.
- Tốc độ đọc file là sinh tử (Đọc file Local mất chưa tới 1 mili-giây, chọc ra S3 bay ngay 20-50ms).
Nỗi đau của việc "Trở tay không kịp" (Migration)¶
Lúc đầu xài Local, đợi đến lúc rác ngập 100GB rồi mới tính đường chuyển nhà sang S3 thì bạn sẽ phải nếm đủ mùi:
- Treo máy ngồi Upload hàng chục ngàn file lên Cloud ròng rã mấy ngày.
- Đập đi xây lại cái DB để đổi đường link từ
/data/audio...thànhhttps://s3.... - Tét lại toàn bộ Flow nạp/rút từ đầu.
Bí kíp sinh tồn: Nếu thấy tương lai có mùi "Scale ngang" → Bấm bụng xài S3 ngay từ Day 1. Nếu mù mịt → Áp dụng YAGNI, cứ xài Local trước. Nhưng nhớ thiết kế cái Repository đàng hoàng để ngày mai nếu có đổi gió thì chỉ việc tráo linh kiện (Bài 5 §3 DIP).
14. Bài tập Thử thách tư duy phản biện¶
Đóng vai một Tech Lead, bạn sẽ "phản pháo" thế nào trong các tình huống sau:
- Tranh luận: Cậu em Junior khăng khăng "Tất cả các API kể cả cái hàm kiểm tra sức khỏe
GET /healthcũng phải chẻ ra đủ 3 lớp Router-Service-Repo cho nó... đồng bộ đội hình". - Tranh luận: Team Front-end đòi dùng JWT token cho một hệ thống Web quản lý nội bộ toàn nhân viên văn phòng xài.
- Tranh luận: Bạn phát hiện trong hệ thống có 3 class:
EmailValidator,PhoneValidator,UrlValidator— cùng kế thừa một interfaceIValidatorrất xịn xò. Nhưng ngặt nỗi, mỗi cái class này chỉ được lôi ra xài đúng một lần duy nhất ở 3 góc khác nhau của dự án. - Tranh luận: Một API xử lý ảnh tốn 100ms CPU cho mỗi tấm. Đội dev quyết định sửa hàm
defthànhasync defvà hy vọng Server sẽ gánh được 50 người cùng lúc. - Tranh luận: App đang có 10 triệu User. Sếp yêu cầu thêm một cột
Tuổi(Bắt buộc không được để trống - NOT NULL) vào bảng. Lão Dev quyết định quăng câu lệnhALTER TABLEnày vào hàm khởi động (Startup hook) của FastAPI theo đúng triết lý "Fail Fast".
Tự ngẫm lại mình (Checklist)¶
- Bạn có thể kể tên 3 dấu hiệu cho thấy việc chia 3 lớp (3-tier) đang biến thành thảm họa không?
- Hoàn cảnh nào thì nên thẳng tay xóa sạch Async khỏi dự án Python?
- JWT "yếu đuối" hơn Session Cookie truyền thống ở những điểm chết người nào?
- Thần chú "Copy 2 lần thì kệ, copy lần 3 thì hãy gộp" có ý nghĩa sâu xa gì?
- Một cái hàm dài 200 dòng, làm đủ thứ trò nhưng vẫn tuân thủ SRP (Đơn trách nhiệm). Bạn giải thích nghịch lý này thế nào?
Lời kết cho chặng đường 10 Bài học¶
Chúc mừng bạn đã lội bì bõm qua 10 bài viết cực nặng về mặt tư duy. Mang theo 3 câu thần chú này làm hành trang nhé:
- Mẫu thiết kế (Pattern) chỉ là viên thuốc giải cho đúng CĂN BỆNH của nó. Đừng kê đơn bừa bãi.
- Nguyên lý nền tảng (Bài 0) mới là cách bộ não của bạn vận hành. Có nội công thâm hậu, vớ được cành cây cũng thành kiếm bén (Tự nghĩ ra Pattern).
- Mọi thứ trên đời đều có giá của nó (Trade-off). Không tồn tại cái gọi là "Tuyệt chiêu code hoàn hảo nhất" — chỉ có "Tuyệt chiêu phù hợp nhất với túi tiền và hoàn cảnh hiện tại".
Khoảng cách giữa một Coder chập chững và một Senior lão làng thực chất không nằm ở việc ai thuộc lòng nhiều Pattern hơn. Nó nằm ở khả năng Nhìn thấu sự Đánh đổi để đưa ra quyết định sinh tử cho dự án!