Bạn đang code flow rất tốt, tiến độ nhanh gọn, rồi những câu hỏi về thiết kế ập đến:
- Logic này nên là một method hay một function độc lập?
- Mình nên lưu cái này vào
selfhay chỉ để là biến cục bộ (local)? - Mình nên tiêm (inject) dependency này hay cứ khởi tạo nó bên trong class?
- Thực sự class này chịu trách nhiệm cho việc gì?
- Nếu mình thêm một lệnh
if/elsenữa, mình đang làm điều thực tế… hay đang tạo ra một “mớ bòng bong” (blob)?
Bài viết này cung cấp cho bạn một danh sách kiểm tra tư duy (mental checklist) có thể lặp lại với các ví dụ Python nhỏ mà bạn có thể copy-paste.
Một nguyên tắc chung dễ áp dụng
Trước khi đi vào 5 câu hỏi, hãy nhớ khung tư duy sau:
- Function (Hàm) = “Đưa đầu vào → trả về đầu ra.” Không có trạng thái (state) ẩn.
- Class (Lớp) = “Một đối tượng với một công việc.” Nó giữ các bất biến (invariants), trạng thái, và dependencies cần thiết để thực hiện công việc đó một cách nhất quán.
self.attribute= “Cái này phải tồn tại lâu hơn một lần gọi” hoặc “Các method khác cần nó.”- Dependency injection (Tiêm phụ thuộc) = “Tôi muốn dễ dàng thay đổi thứ này (cho test, các môi trường khác nhau, các phiên bản).”
- Class/Module mới = “Đây là một công việc khác.”
1) Logic này nên nằm trong class hay bên ngoài như một function?
Dùng method khi logic:
- cần đến trạng thái của đối tượng (
self.*) - phải bảo vệ/xác thực các bất biến (invariants)
- là một phần của public API của đối tượng
Dùng function ở mức module (module-level) khi logic:
- là pure (thuần túy) hoặc gần như pure (đầu vào → đầu ra)
- hữu ích và có thể dùng chung cho nhiều class khác nhau
- không cần trạng thái private
Ví dụ: giữ các tính toán toán học thuần túy ở bên ngoài
| |
Tại sao cách này lại tốt:
apply_discountcó thể tái sử dụng ở bất cứ đâu.CartItem.discounted_price()đọc rất dễ hiểu và giữ cho class gọn nhẹ.
Ví dụ: đặt logic trong class khi nó bảo vệ các bất biến (invariants)
| |
Đoạn code này thuộc về class vì class chịu trách nhiệm giữ cho _balance luôn hợp lệ.
Bài test nhanh:
Nếu bạn xóa class đi và viết withdraw(balance, amount) -> new_balance, bạn có làm mất các quy tắc quan trọng về đối tượng không? Nếu có, hãy dùng method.
2) Khi nào thì nên lưu một thứ gì đó vào self.attribute so với để nó là biến cục bộ (local)?
Lưu vào self khi:
- nó được cần đến qua nhiều lần gọi method
- nó được nhiều method khác nhau sử dụng
- nó đại diện cho cấu hình (configuration), dependency, hoặc cache
- nó giúp thực thi một bất biến
Giữ nó ở mức cục bộ (local) khi:
- nó là một tính toán tạm thời
- nó có thể được suy ra (derived) từ các trạng thái khác
- lưu trữ nó sẽ tạo ra trạng thái (state) mà bạn phải tự đồng bộ (keep in sync)
Ví dụ: cấu hình thuộc về self
| |
Ví dụ: đừng lưu trữ các giá trị có thể suy ra (derived values) trừ khi bắt buộc
| |
Nếu bạn lưu self.area = self.w * self.h, lúc này bạn sẽ phải cập nhật nó mỗi khi w hoặc h thay đổi.
Ví dụ: caching là một lý do hợp lý để lưu trữ
| |
3) Khi nào nên tiêm (inject) một dependency thay vì khởi tạo nó bên trong class?
Tiêm (Inject) khi:
- bạn muốn dễ dàng viết test
- bạn muốn các implementations khác nhau (ví dụ: prod vs dev)
- dependency đó đắt đỏ (về tài nguyên) hoặc được dùng chung
- bạn muốn tránh việc “hard-code” thực tế vào bên trong class của bạn
Khởi tạo nội bộ (Create internally) khi:
- nó là một chi tiết nhỏ và ổn định
- nó rất ít khả năng bị thay thế
- bạn không cần phải “làm giả” (fake) nó trong các bài test
Không tốt cho việc test: dependency được tạo bên trong
| |
Tốt hơn: tiêm (inject) HTTP client
| |
Còn sạch hơn nữa (Cleaner): tiêm một gateway object
| |
4) Một class thực sự nên chịu trách nhiệm cho những việc gì?
Một class nên đại diện cho một công việc mạch lạc (coherent job) với:
- đầu vào/đầu ra rõ ràng
- các bất biến rõ ràng
- một API nhỏ, dễ hiểu
Nếu bạn mô tả class với từ “và” (“and”), đó là một dấu hiệu cảnh báo.
Mùi code tồi (Code smell) ví dụ: “Class này gửi email VÀ format HTML VÀ retry VÀ log VÀ đọc template…”
Ví dụ: chia nhỏ trách nhiệm
| |
Mỗi thành phần chỉ có một lý do duy nhất để thay đổi.
5) Thêm một if/else… hay tách ra thành một thứ mới?
Thêm nhánh (branch) khi:
- sự thay đổi là nhỏ và ổn định
- chỉ có một vài trường hợp
- hành vi rõ ràng là một phần của cùng một công việc (job)
Tách ra (Extract) khi:
- bạn đang thêm nhánh thứ 3 hoặc thứ 4 và còn nhiều nhánh nữa sẽ tới
- mỗi nhánh có độ phức tạp nội bộ đáng kể
- bạn liên tục chỉnh sửa cùng một method khổng lồ
- bạn đang truyền các cờ (flags) như
mode,type, hoặcstrategy
Dùng if/else đơn giản thì ổn
| |
Tách ra thành strategy (chiến lược) khi logic lớn dần lên
| |
Giờ thì việc thêm một khuyến mãi mới sẽ không yêu cầu bạn phải chỉnh sửa PriceCalculator nữa.
Bảng tra cứu thu gọn (Compact decision cheat-sheet)
Method vs function (Hàm):
- Cần trạng thái của object hoặc các bất biến → method
- Pure và có thể tái sử dụng → function
self.attribute vs local (Cục bộ):
- Dùng qua nhiều lần gọi hoặc nhiều method → self
- Tạm thời hoặc suy ra (derived) → local
Inject (Tiêm) vs Khởi tạo bên trong:
- Cần sự linh hoạt để tráo đổi (swapping) hoặc dễ test → inject
- Thực sự là một chi tiết nội bộ ổn định → create internals (khởi tạo tại chỗ)
Thêm nhánh vs tách ra (Extract):
- Hai trường hợp ổn định → if/else đơn giản
- Sự biến thể ngày càng tăng → tách class hoặc áp dụng pattern strategy
