Skip to content
Go back

Design pattern dùng để tạo object

Một số design pattern phổ biến, dùng cho việc tạo object.

Factory

Factory dùng khi chỗ gọi chỉ cần nói “tôi cần loại nào”, còn việc tạo class cụ thể để nơi khác lo.

Factory Method

Giả sử hệ thống export report có nhiều format:

def export_report(rows, fmt):
    if fmt == "csv":
        exporter = CsvExporter()
    elif fmt == "xlsx":
        exporter = XlsxExporter()
    elif fmt == "pdf":
        exporter = PdfExporter()
    else:
        raise ValueError(f"unsupported format: {fmt}")

    return exporter.export(rows)

Một đoạn if/elif như vậy không có gì sai.

Vấn đề xuất hiện khi logic này lặp lại ở nhiều nơi: API, cron job và CLI. Khi thêm format mới, bạn phải sửa tất cả các chỗ đó. Chỉ cần quên một chỗ là behavior giữa các nơi có thể khác nhau.

Có thể gom phần tạo object vào một factory:

_EXPORTERS = {
    "csv": CsvExporter,
    "xlsx": XlsxExporter,
    "pdf": PdfExporter,
}

def make_exporter(fmt: str):
    try:
        return _EXPORTERS[fmt]()
    except KeyError:
        raise ValueError(f"unsupported format: {fmt}")

def export_report(rows, fmt):
    return make_exporter(fmt).export(rows)

Bây giờ việc chọn class chỉ nằm ở một chỗ.

Thêm JSON chỉ cần tạo JsonExporter và thêm vào _EXPORTERS. Những chỗ đang gọi export_report() không cần biết class nào được tạo.

Đổi lại, khi đọc make_exporter("pdf"), bạn không biết ngay class nào được dùng. Muốn biết phải mở factory ra xem.

Trong Python, factory thường chỉ cần đơn giản như một function. Không nhất thiết phải tạo thêm abstract class hay một hệ thống phức tạp.

Nên dùng khi: có nhiều loại object và khả năng sẽ còn thêm loại mới.

Không nên dùng khi: chỉ có một hoặc hai loại và logic if vẫn dễ đọc. Factory chỉ tạo đúng một loại object thì thường cũng không có nhiều ý nghĩa.

Abstract Factory

Abstract Factory cũng là factory, nhưng thay vì tạo một object, nó tạo một bộ object có liên quan với nhau.

Ví dụ storage system cần ba thành phần:

blobs = ...
metadata = ...
locks = ...

Production dùng AWS:

blobs = S3BlobStore(session)
metadata = DynamoMetadata(session)
locks = DynamoLock(session)

Local dùng local storage:

blobs = DiskBlobStore(root)
metadata = SqliteMetadata(root)
locks = FileLock(root)

Vấn đề là ba object này phải đi cùng nhau. Nếu ai đó vô tình dùng:

blobs = DiskBlobStore(root)
metadata = SqliteMetadata(root)
locks = DynamoLock(session)

code vẫn có thể chạy ở local, nhưng thực tế đang dùng một bộ object không đồng nhất.

Abstract Factory gom chúng thành một backend:

class S3Backend:
    def blobs(self):
        return S3BlobStore(self._session)

    def metadata(self):
        return DynamoMetadata(self._session)

    def locks(self):
        return DynamoLock(self._session)


class LocalBackend:
    def blobs(self):
        return DiskBlobStore(self._root)

    def metadata(self):
        return SqliteMetadata(self._root)

    def locks(self):
        return FileLock(self._root)

Ở lúc khởi động:

backend = (
    S3Backend(session)
    if env == "production"
    else LocalBackend(root)
)

Từ đó, mọi object được lấy từ cùng một backend. Điểm mạnh của Abstract Factory là giữ các object liên quan luôn đi cùng nhau. Điểm yếu là khi thêm một loại object mới, ví dụ events(), bạn phải thêm method đó vào tất cả các factory. Vì vậy, Abstract Factory phù hợp khi bạn có nhiều bộ object khác nhau, và mỗi bộ phải luôn đồng nhất.


Dependency Injection

Dependency Injection cũng liên quan đến việc tạo object, nhưng mục tiêu chính là:

Class không tự tạo những thứ nó phụ thuộc vào.

Ví dụ:

class OrderService:
    def __init__(self):
        self.db = PostgresConnection(os.environ["DB_URL"])
        self.payments = VnpayGateway(os.environ["VNPAY_KEY"])
        self.clock = datetime

    def place(self, cart):
        ...

OrderService đang tự quyết định:

  • dùng database nào
  • dùng payment gateway nào
  • lấy config từ đâu
  • dùng clock nào

Điều này khiến class khó test và khó thay thế dependency.

Thay vào đó:

class OrderService:
    def __init__(
        self,
        db: Database,
        payments: PaymentGateway,
        clock: Clock,
    ):
        self.db = db
        self.payments = payments
        self.clock = clock

    def place(self, cart):
        ...

Production:

service = OrderService(
    PostgresDatabase(url),
    VnpayGateway(key),
    SystemClock(),
)

Test:

service = OrderService(
    InMemoryDatabase(),
    FakeGateway(),
    FrozenClock("2026-01-01"),
)

Bây giờ OrderService không cần biết những object đó được tạo từ đâu.

Điều này có ba lợi ích:

  1. Nhìn constructor là biết class cần gì.
  2. Có thể dễ dàng thay dependency khi test hoặc thay implementation.
  3. Việc tạo object được gom về một chỗ, thường là main() — hay còn gọi là composition root.

Trong Python, bạn thường không cần DI framework. Constructor injection như trên đã đủ cho phần lớn project.

Nên inject những dependency có thể thay đổi hoặc khó kiểm soát như:

  • database
  • HTTP client
  • external service
  • clock
  • random generator

Còn những thứ đơn giản và ổn định như format string hay JSON serializer thì thường không cần inject.


Builder

Builder giải quyết một vấn đề khác: object có quá nhiều option hoặc cần được dựng từng bước.

Ví dụ:

report = Report(
    "Q3",
    "2026-01-01",
    "2026-03-31",
    True,
    False,
    True,
    None,
    "pdf",
    3,
)

Nhìn vào True, False, True rất khó biết chúng có ý nghĩa gì.

Một cách khác là tạo object rỗng rồi set từng field:

report = Report()
report.title = "Q3"
report.include_charts = True

# ... rất nhiều dòng khác

render(report)

Cách này lại cho phép object chưa hoàn chỉnh tồn tại. Ví dụ quên set date range nhưng chỉ đến lúc render() mới phát hiện.

Builder gom việc dựng object vào một nơi:

class ReportBuilder:
    def __init__(self, title):
        self._cfg = {
            "title": title,
            "format": "pdf",
            "include_charts": False,
        }

    def date_range(self, start, end):
        self._cfg["start"] = start
        self._cfg["end"] = end
        return self

    def with_charts(self):
        self._cfg["include_charts"] = True
        return self

    def build(self):
        if "start" not in self._cfg:
            raise ValueError("date range is required")

        return Report(**self._cfg)

Cách dùng:

report = (
    ReportBuilder("Q3")
    .date_range("2026-01-01", "2026-03-31")
    .with_charts()
    .build()
)

Các method trả về self nên có thể gọi liên tiếp.

Builder cũng có thể validate trước khi tạo object, nên object hoàn chỉnh mới được trả ra ngoài.

Tuy nhiên, trong Python không phải lúc nào cũng cần Builder.

Nếu object chỉ có vài field, keyword arguments thường đủ:

@dataclass(frozen=True, kw_only=True)
class Report:
    title: str
    start: date
    end: date
    include_charts: bool = False
    format: str = "pdf"


report = Report(
    title="Q3",
    start=...,
    end=...,
    include_charts=True,
)

Dễ đọc hơn và không phải viết thêm một class Builder.

Nên dùng Builder khi: object phức tạp, có nhiều option, cần dựng qua nhiều bước hoặc cần validate nhiều field liên quan với nhau.

Nếu chỉ có vài field thì keyword arguments thường là đủ.


Prototype

Prototype dùng khi bạn có một object mẫu đã được cấu hình sẵn và muốn tạo ra nhiều biến thể từ nó.

Ví dụ có ba experiment gần giống nhau:

variant_a = ExperimentConfig(
    model="v3",
    batch_size=32,
    layers=[128, 64],
    seed=1,
)

variant_b = ExperimentConfig(
    model="v3",
    batch_size=32,
    layers=[128, 64],
    seed=2,
)

variant_c = ExperimentConfig(
    model="v3",
    batch_size=32,
    layers=[128, 64],
    seed=3,
)

Ba object gần như giống hệt nhau.

Nếu ngày mai đổi batch_size từ 32 thành 64, bạn phải nhớ sửa cả ba chỗ.

Có thể tạo một object mẫu:

base = ExperimentConfig(
    model="v3",
    batch_size=32,
    layers=[128, 64],
    seed=1,
)

Sau đó tạo các biến thể từ nó:

from dataclasses import replace

variants = [
    replace(base, seed=seed)
    for seed in (1, 2, 3)
]

Bây giờ cấu hình chung chỉ nằm ở một chỗ.

Prototype đặc biệt hữu ích khi object có nhiều configuration và các object mới chỉ khác một vài giá trị.

Nếu dùng copy, cần chú ý sự khác nhau giữa copy.copy() và copy.deepcopy().

v = copy.copy(base)
v.layers.append(32)

copy.copy() chỉ copy object bên ngoài. layers vẫn được dùng chung, nên thay đổi v.layers cũng làm base.layers thay đổi.

deepcopy() copy cả các object bên trong, nhưng không phải lúc nào cũng phù hợp. Nếu object đang giữ database connection, socket hoặc resource khác, việc copy toàn bộ object có thể gây ra vấn đề.

Trong Python, nếu có thể, immutable object kết hợp với dataclasses.replace() thường là cách đơn giản hơn.


Object Pool

Object Pool dùng khi tạo một object rất tốn thời gian, nhưng object đó có thể được tái sử dụng.

Database connection là ví dụ điển hình.

Nếu mỗi request đều tạo connection mới:

def handle_request(req):
    conn = psycopg.connect(DB_URL)

    try:
        return query(conn, req)
    finally:
        conn.close()

Bạn phải trả chi phí tạo connection cho mỗi request.

Thay vào đó, tạo sẵn một số connection và cho request mượn:

with pool.connection() as conn:
    return query(conn, req)

Sau khi dùng xong, connection được trả lại pool để request khác sử dụng.

Ý tưởng rất đơn giản:

Tạo một lần → sử dụng nhiều lần → trả lại để tái sử dụng.

Nhưng tự viết pool khó hơn nhìn bên ngoài.

Pool phải xử lý:

  • hết connection thì chờ hay fail?
  • chờ tối đa bao lâu?
  • connection còn sống không?
  • transaction cũ đã được reset chưa?
  • nhiều thread/process cùng truy cập thì sao?

Ví dụ quên reset connection có thể khiến trạng thái của request trước ảnh hưởng request sau.

Vì vậy, trong thực tế thường không nên tự viết Object Pool.

Database driver, HTTP client và nhiều thư viện khác đã có connection pool sẵn.

Bạn chủ yếu cần hiểu pattern này để cấu hình pool cho đúng:

  • pool tối đa bao nhiêu connection?
  • hết pool thì chờ hay fail?
  • connection có được kiểm tra trước khi tái sử dụng không?

Singleton — pattern nên cẩn thận

Singleton đảm bảo một class chỉ có một instance trong toàn bộ application.

Ví dụ:

class Config:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

Nhìn qua khá tiện:

Config().timeout

Ở đâu cũng lấy được cùng một object.

Nhưng chính điều này lại tạo ra vấn đề.

Ví dụ test:

def test_short_timeout():
    Config().timeout = 1

def test_normal_case():
    # timeout can still be 1

Test này có thể pass khi chạy riêng nhưng fail khi chạy cùng test khác.

Lý do là Singleton giữ state dùng chung giữa các test.

Ngoài ra, Singleton còn có một vấn đề lớn hơn: dependency bị ẩn.

def process(order):
    timeout = Config().timeout

Nhìn vào process(order) không biết function này thực sự cần Config.

Một cách đơn giản hơn là tạo config một lần ở lúc khởi động rồi truyền xuống:

def process(order, config: Config):
    timeout = config.timeout

Production:

config = Config.from_env()

process(order, config)

Test:

process(order, Config(timeout=1))

Đây chính là Dependency Injection.

Nếu mục tiêu chỉ là có một object dùng chung, Python cũng không nhất thiết cần Singleton class. Module đã được import và cache một lần:

# config.py

settings = Settings.from_env()

Sau đó:

from config import settings

Tuy nhiên, cách này vẫn tạo global state và vẫn có thể làm dependency bị ẩn.

Vì vậy, câu hỏi nên đặt ra không phải:

“Làm sao đảm bảo chỉ có một instance?”

Mà là:

“Có thực sự cần mọi nơi dùng chung một instance không?”

Nếu chỉ cần dùng chung, tạo object ở main() rồi truyền xuống thường đơn giản và dễ test hơn.

Trường hợp sử dụng?

Tình huốngPattern
Chỗ gọi không cần biết class cụ thểFactory Method
Một bộ object phải luôn đi cùng nhauAbstract Factory
Class không nên tự tạo dependencyDependency Injection
Object có nhiều option và cần dựng từng bướcBuilder
Tạo nhiều biến thể từ một object mẫuPrototype
Resource đắt và có thể tái sử dụngObject Pool
Nhiều nơi cần dùng chung một instanceModule hoặc Dependency Injection

Điểm quan trọng là đừng bắt đầu bằng pattern.

Hãy bắt đầu bằng vấn đề:

  • Có quá nhiều chỗ phải biết class cụ thể? → nghĩ đến Factory.
  • Có nhiều object phải luôn đi cùng nhau? → nghĩ đến Abstract Factory.
  • Class tự tạo database, HTTP client…? → nghĩ đến Dependency Injection.
  • Object quá nhiều option? → nghĩ đến Builder.
  • Có một object mẫu và nhiều biến thể? → nghĩ đến Prototype.
  • Tạo resource quá đắt và có thể tái sử dụng? → nghĩ đến Pool.
  • Chỉ muốn mọi nơi dùng chung một object? → chưa chắc cần Singleton.

Pattern tốt không phải pattern được dùng nhiều nhất, mà là pattern giải quyết đúng vấn đề mà không làm code phức tạp hơn.

Previous Post
Next Post