Featured image of post Khi Nào Nên Dùng Class vs. Function Trong Python: Checklist Thiết Kế

Khi Nào Nên Dùng Class vs. Function Trong Python: Checklist Thiết Kế

Một danh sách kiểm tra tư duy (mental checklist) có thể lặp lại cho các lập trình viên Python để quyết định khi nào nên dùng class, function, instance state, và dependency injection.

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 self hay 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/else nữ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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from dataclasses import dataclass


def apply_discount(price: float, percent: float) -> float:
    if percent < 0 or percent > 100:
        raise ValueError("percent must be 0..100")
    return price * (1 - percent / 100)


@dataclass
class CartItem:
    name: str
    price: float

    def discounted_price(self, percent: float) -> float:
        # method này chỉ là một wrapper tiện lợi bọc lấy logic thuần túy
        return apply_discount(self.price, percent)

Tại sao cách này lại tốt:

  • apply_discount có 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)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class BankAccount:
    def __init__(self, balance: int = 0):
        if balance < 0:
            raise ValueError("balance must be >= 0")
        self._balance = balance

    def withdraw(self, amount: int) -> None:
        if amount <= 0:
            raise ValueError("amount must be > 0")
        if amount > self._balance:
            raise ValueError("insufficient funds")
        self._balance -= amount

Đ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

1
2
3
4
5
6
7
8
class CsvExporter:
    def __init__(self, delimiter: str = ","):
        self.delimiter = delimiter  # configuration: sống thọ (long-lived)

    def export_row(self, values: list[str]) -> str:
        # biến tạm: cục bộ (local)
        escaped = [v.replace('"', '""') for v in values]
        return self.delimiter.join(f'"{v}"' for v in escaped)

Ví dụ: đừng lưu trữ các giá trị có thể suy ra (derived values) trừ khi bắt buộc

1
2
3
4
5
6
7
8
class Rectangle:
    def __init__(self, w: float, h: float):
        self.w = w
        self.h = h

    def area(self) -> float:
        # suy ra từ state; giữ ở mức local
        return self.w * self.h

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ữ

1
2
3
4
5
6
7
8
9
class UserProfileService:
    def __init__(self, db):
        self.db = db
        self._cache: dict[int, dict] = {}

    def get_user(self, user_id: int) -> dict:
        if user_id not in self._cache:
            self._cache[user_id] = self.db.fetch_user(user_id)
        return self._cache[user_id]

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

1
2
3
4
5
6
7
import requests

class WeatherClient:
    def get_temp(self, city: str) -> float:
        resp = requests.get(f"https://example.com/weather?city={city}")
        resp.raise_for_status()
        return resp.json()["temp"]

Tốt hơn: tiêm (inject) HTTP client

1
2
3
4
5
6
7
8
class WeatherClient:
    def __init__(self, http_get):
        self.http_get = http_get  # dependency được tiêm (injected)

    def get_temp(self, city: str) -> float:
        resp = self.http_get(f"https://example.com/weather?city={city}")
        resp.raise_for_status()
        return resp.json()["temp"]

Còn sạch hơn nữa (Cleaner): tiêm một gateway object

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class HttpGateway:
    def get_json(self, url: str) -> dict:
        import requests
        r = requests.get(url)
        r.raise_for_status()
        return r.json()


class WeatherClient:
    def __init__(self, http: HttpGateway):
        self.http = http

    def get_temp(self, city: str) -> float:
        data = self.http.get_json(f"https://example.com/weather?city={city}")
        return data["temp"]

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class EmailFormatter:
    def format_welcome(self, name: str) -> str:
        return f"Hi {name}, welcome!"


class EmailSender:
    def __init__(self, smtp_client):
        self.smtp_client = smtp_client

    def send(self, to: str, body: str) -> None:
        self.smtp_client.send(to=to, body=body)


class WelcomeEmailService:
    def __init__(self, formatter: EmailFormatter, sender: EmailSender):
        self.formatter = formatter
        self.sender = sender

    def send_welcome(self, to: str, name: str) -> None:
        body = self.formatter.format_welcome(name)
        self.sender.send(to, body)

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ặc strategy

Dùng if/else đơn giản thì ổn

1
2
3
4
5
class PriceCalculator:
    def total(self, base: float, is_member: bool) -> float:
        if is_member:
            return base * 0.9
        return base

Tách ra thành strategy (chiến lược) khi logic lớn dần lên

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class PricingRule:
    def apply(self, base: float) -> float:
        raise NotImplementedError


class MemberDiscount(PricingRule):
    def apply(self, base: float) -> float:
        return base * 0.9


class BlackFriday(PricingRule):
    def apply(self, base: float) -> float:
        return base * 0.7


class PriceCalculator:
    def __init__(self, rule: PricingRule):
        self.rule = rule

    def total(self, base: float) -> float:
        return self.rule.apply(base)

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
Được tạo với sự lười biếng tình yêu 🦥

Subscribe to My Newsletter