1. Giới thiệu Clean Code là gì

Hãy tưởng tượng bạn đang chạy xe máy về nhà sau một ngày mệt mỏi. Trên đường, bạn gặp đủ loại tình huống: đèn đỏ, ngã ba, vạch kẻ đường, dòng người qua lại. Giờ thì bạn có thể làm gì? Bạn hoàn toàn có thể lấn làn, chạy xe vượt đèn đỏ, chạy ngược chiều và bấm còi inh ỏi rồi tạt đầu người khác. Và đoán xem? Bạn vẫn có thể về đến nhà an toàn. Nhưng điều đó không có nghĩa là bạn nên làm vậy. Người đi đường sẽ khó chịu với bạn, công an có thể thổi phạt bất cứ lúc nào, và quan trọng hơn là bạn đang tự đặt mình vào nguy cơ gây tai nạn không đáng có. Viết code cũng thế. Bạn có thể đặt tên biến là ==a==, ==b==, hoặc ==temp2==, nhồi nhét mọi logic vào một hàm dài hàng trăm dòng, hoặc copy-paste y nguyên một đoạn code đến năm chỗ khác nhau trong cùng một project. Code vẫn chạy, chương trình vẫn ra kết quả như mong muốn. Nhưng đến khi cần sửa, cần debug, hoặc cần bàn giao lại cho người khác mọi chuyện bắt đầu đổ vỡ. Code khó đọc, khó hiểu, khó mở rộng, và rất dễ mắc lỗi khi chỉnh sửa. Clean Code không phải là luật buộc phải tuân theo, nhưng giống như luật giao thông, nó giúp mọi người cùng vận hành trong một hệ thống suôn sẻ, an toàn và hiệu quả. Viết code sạch là cách bạn thể hiện sự tôn trọng với đồng nghiệp, với chính bản thân mình trong tương lai và với cả hệ thống phần mềm bạn đang góp phần xây dựng. image 85.png Clean Code là gì?

Clean Code là mã nguồn rõ ràng, dễ đọc, dễ bảo trì, có khả năng mở rộng và dễ dàng kiểm thử. Nó được viết theo cách mà người khác (đồng nghiệp) hoặc chính bạn trong tương lai có thể dễ dàng hiểu, cải tiến hoặc bảo trì.

1.1. Nguyên tắc cốt lõi của Clean Code (RMET)

Một đoạn code sạch không chỉ là code chạy được, mà là code có thể đọc được như một đoạn văn. Clean Code tuân theo bốn nguyên tắc cơ bản – viết tắt là RMET:

  • Readable (Dễ đọc, dễ hiểu): Một người khác (hoặc chính bạn sau 3 tháng quên sạch project) có thể đọc code và hiểu được ý đồ ban đầu mà không cần đoán mò.
  • Maintainable (Dễ bảo trì): Khi cần thêm tính năng hoặc chỉnh sửa, bạn có thể làm điều đó mà không làm hỏng thứ khác.
  • Extensible (Có khả năng mở rộng): Hệ thống được thiết kế để chấp nhận cái mới mà không đập bỏ cái cũ.
  • Testable (Dễ kiểm thử): Code sạch tách biệt đủ rõ ràng để dễ viết unit test, kiểm thử logic và đảm bảo chất lượng.

1.2. Tại sao Clean code lại quan trọng

image 1 45.png Ban đầu, việc viết code sạch có vẻ tốn thời gian hơn - vì bạn phải nghĩ về tên biến, cách chia hàm, cấu trúc thư mục. Nhưng về lâu dài, Clean Code sẽ giúp bạn tiết kiệm được rất nhiều thời gian. Nó giúp:

  • Giảm bug: Code rõ ràng thì ít lỗi ngớ ngẩn hơn. Khi có bug thì cũng dễ tìm ra nguyên nhân hơn.
  • Tăng tốc độ phát triển: Không ai phải mất nửa ngày để đọc hiểu một đoạn code “tối cổ”.
  • Cải thiện làm việc nhóm: Người khác có thể đọc hiểu và sửa code của bạn mà không chửi thề.
  • Dễ mở rộng: Khi business thay đổi (và sẽ thay đổi), hệ thống vẫn có thể thích ứng được.

1.3. Ví dụ về Clean Code

  1. Đặt tên biến phải có ý nghĩa Ta thấy ba biến ==a, b, và c==. Chúng được gán giá trị và cộng lại, nhưng không ai biết a là gì, b đại diện cho cái gì, và c có ý nghĩa ra sao.
\#Bad example: unclear variable names
a = 5
b = 10
c = a + b
print(f"a + b = {c}")

Ngược lại, các biến ở đây được đặt tên rõ ràng: ==apples==, ==oranges==, và ==total_fruits==

\#Good example: descriptive variable names:
apples = 5
oranges = 10
total_fruits = apples + oranges
print(f"Number of apples: {apples}")
print(f"Number of oranges: {oranges}")
print(f"Total fruits: {total_fruits}")
  1. Viết code có formating Hàm ==add_numbers== được viết dính liền toàn bộ: không có khoảng trắng giữa các dấu phẩy trong tham số, không có khoảng trắng xung quanh các toán tử. Mặc dù chương trình vẫn hoạt động đúng, nhưng tính dễ đọc gần như bằng 0.
\#Bad example: poor formatting:
def add_numbers(x,y,z):
    result=x+y+z
    return result

Hàm ==add_numbers_clean được viết đúng theo chuẩn PEP-8:==

  • Giữa các tham số có dấu cách sau dấu phẩy.
  • Biểu thức toán học ==x + y + z được cách đều, giúp dễ theo dõi và không gây hiểu nhầm==.
\#Good example: proper formatting with spaces:
def add_numbers_clean(x, y, z):
    result = x + y + z
    return result

1.4. Khi nào thì không cần Clean Code

image 2 43.png Sẽ có những tình huống mà bạn không cần Clean Code:

  • Khi làm prototype nhanh để kiểm thử ý tưởng. Nhưng hãy refactor nếu quyết định dùng code đó.
  • Khi viết script 1 lần duy nhất rồi xóa. Code để vọc vạnh thử hoặc học. Không sử dụng trong sản phẩm.
  • Khi fix nóng để cứu hệ thống đang down, hệ thống gặp sự cố nghiêm trọng. Giải quyết vấn đề trước, refactor sau.
  • Khi bạn chắc chắn chỉ một mình dùng đoạn code đó. Tuy nhiên, nên nhớ rằng “chỉ mình tôi đọc” hôm nay có thể trở thành “cả team đọc” ngày mai. Và “fix tạm” rất dễ bị quên để trở thành “chạy production luôn”. Hãy linh hoạt, nhưng đừng buông thả.

2. Tiêu chuẩn PEP-8

PEP-8 (Python Enhancement Proposal 8) là tiêu chuẩn định dạng mã nguồn chính thức của ngôn ngữ Python. Nó giống như sách giáo khoa về cách trình bày code sao cho thống nhất, dễ đọc và dễ làm việc nhóm. Được đề xuất bởi Guido van Rossum, cha đẻ của Python, PEP-8 không nói bạn nên code thế nào để giải bài toán, mà hướng dẫn trình bày code sao cho sạch sẽ, chuyên nghiệp và dễ bảo trì. image 3 41.png Guido van Rossum Tham khảo: https://peps.python.org/pep-0008/

Mục tiêu của PEP-8

  • Làm cho code dễ đọc, dễ hiểu, kể cả với người mới.
  • Giảm lỗi nhầm lẫn hoặc bug do cách trình bày kém.
  • Giúp các lập trình viên trong nhóm có thể đọc và hiểu code của nhau nhanh hơn, làm việc hiệu quả hơn.

2.1. Quy tắc đặt tên

Đặt tên đúng là bước đầu tiên để viết code dễ đọc. Nếu bạn đặt tên tốt, người khác không cần đọc từng dòng lệnh để hiểu bạn đang làm gì. Họ chỉ cần nhìn vào tên hàm, biến, hay hằng số là đã hình dung được ý định của bạn. PEP-8 đưa ra ba quy tắc quan trọng về cách đặt tên: 1. Biến và Hàm: ==**snake_case**== Biến và tên hàm trong Python nên dùng chữ thường và nối bằng dấu gạch dưới (_) để phân tách các từ.

# Khai báo biến
my_variable = 10
# Định nghĩa hàm
def my_function():
    print("Hello")

Quy tắc này giúp tên biến dễ đọc hơn, nhất là với những tên nhiều từ. Tránh viết dính (==myvariable==) hoặc dùng camelCase (==myVariable==) vì không đúng chuẩn Python. ==2. Class: **PascalCase**== Tên lớp (class) nên được viết hoa chữ cái đầu mỗi từ, không có dấu gạch dưới.

# Đặt tên class
class MyClass:
    def __init__(self, name):
        self.name = name

Cách viết này giúp bạn dễ dàng phân biệt đâu là class, đâu là hàm hoặc biến thông thường. Đặc biệt trong lúc Ctrl F việc lộn cú pháp là MyClass hay my_class hay myClass sẽ không còn là vấn đề. 3. Hằng số: ==**UPPER_CASE**== Với các hằng số (giá trị không thay đổi), bạn nên viết toàn bộ bằng chữ in hoa và dùng dấu gạch dưới để phân cách từ.

# Đặt tên hằng số
PI = 3.14
MAX_VALUE = 100
MIN_VALUE = 5
def check_exam_result(score):
    if score >= MIN_VALUE:
        return "Pass"
    return "Fail"

Sử dụng UPPER_CASE giúp hằng số nổi bật và dễ phân biệt với các biến có thể thay đổi trong logic chương trình.

2.2. Căn lề và khoảng trắng

Trong Python, cách bạn căn lề và dùng khoảng trắng không chỉ ảnh hưởng đến khả năng đọc hiểu của code, mà đôi khi còn ảnh hưởng đến cách code chạy. Đây là lý do tại sao PEP-8 rất nghiêm ngặt về các quy tắc này. 1. Thụt lề – indent đúng cách Python dùng thụt lề để xác định khối lệnh (block of code), không có =={}== như các ngôn ngữ khác. Mỗi cấp thụt lề phải sử dụng 4 khoảng trắng. Tuyệt đối không dùng tab!

# Đúng
def greet():
    print("Hello")
# Sai: số khoảng trắng là 2
def greet():
 print("Hello")

Lưu ý: À nghe kì không, tại sao lại không dùng tab? Chẳng phải chúng ta vẫn hay tab để indent đó sao? Thực ra, vấn đề không nằm ở phím bạn bấm, mà ở cách IDE xử lý nó. Các công cụ như Google Colab, VS Code, hay Jupyter Notebook thường được cấu hình sẵn để mỗi lần bạn nhấn phím Tab thì nó sẽ tự động chèn 4 dấu cách (space) – đúng theo chuẩn PEP-8. Vì vậy, bạn vẫn có thể “Tab như thường” mà không sai chuẩn. image 4 35.png Tuy nhiên, không phải IDE nào cũng như vậy. Một số công cụ cũ hơn, hoặc editor không cấu hình đúng, sẽ thực sự chèn ký tự ==\t (tab thật) thay vì 4 space. Điều này có thể gây ra lỗi khi chuyển code sang môi trường khác (ví dụ: indentation error, code bị lệch dòng).== 2. Khoảng trắng trong biểu thức Khoảng trắng giúp các phép toán dễ đọc hơn. Thêm một dấu cách trước và sau các toán tử, ví dụ: =====, ==+==, ==-==, ==*==, ======, ==!===...

# Đúng
total = price + tax
# Sai: dính chặt các toán tử
total=price+tax

3. Tránh khoảng trắng thừa Không đặt khoảng trắng thừa sau dấu phẩy hoặc trước dấu đóng ngoặc. Điều này tưởng là nhỏ nhưng lại làm code trông rất thiếu chuyên nghiệp.

# Đúng
values = [1, 2, 3]
# Sai
values = [1,2 , 3 ]

4. Không thêm khoảng trắng trong ngoặc Đây là lỗi phổ biến: để khoảng trắng sau dấu ==( hoặc trước dấu )==.

# Đúng
my_list = list([1, 2, 3])
# Sai
my_list = list( [1, 2, 3] )

image 5 33.png

2.3. Giới hạn độ “dài dòng” code

Độ dài dòng code không phải là code bị “dài dòng” mà là độ dài tối đa của mỗi dòng code 😉. Vì code bị kéo ngang đến hết cả màn hình sẽ rất khó đọc. Quy tắc PEP-8:

  • Mã nguồn (code logic): Không quá 79 ký tự/dòng

  • Docstring / Comment: Không quá 72 ký tự/dòng

  • Thực tế hiện đại: Một số IDE (như VS Code, PyCharm) cho phép dùng 88–100 ký tự, nhưng cũng tùy theo từng người.

  • Mục tiêu: Giúp code hiển thị gọn gàng trên mọi loại màn hình, kể cả khi mở song song nhiều file hoặc khi đọc diff trên Git Cách ngắt dòng đúng chuẩn Khi buộc phải viết một biểu thức dài, hãy ngắt dòng sao cho vẫn rõ ràng về mặt ngữ nghĩa, thay vì cố nhét mọi thứ vào một dòng.

  • Dùng dấu gạch chéo ==\== để ngắt dòng trực tiếp (ít khuyến khích vì dễ lỗi):

    total = a + b + \
            c + d
  • Ưu tiên dùng ngoặc đơn/vuông/nhọn () [] {} để Python tự hiểu là dòng chưa kết thúc:

    result = sum([
        price_apple,
        price_orange,
        price_banana,
    ])
  • Ngắt dòng tại dấu phẩy, hoặc trước các toán tử nhị phân (==+==, ==and==, ==or==, , ======...):

    if (user.is_authenticated and
        user.role == "admin" and
        user.is_active):
        access_granted = True
  • Thụt lề dòng tiếp theo thêm 4 space so với dòng đầu:

    final_total = (
        product_price * quantity
        + shipping_fee
        - discount
    )

image 6 30.png

2.4. Tổ chức Import chuẩn

Việc sắp xếp các dòng ==import tưởng như nhỏ nhặt, nhưng thực ra lại ảnh hưởng rất lớn đến tính nhất quán và khả năng mở rộng của dự án. PEP-8 khuyến nghị chia các dòng import thành 3 nhóm rõ ràng, và luôn theo thứ tự: chuẩn → bên thứ ba → nội bộ==. 1. Thư viện chuẩn (Standard Library) Đây là các module có sẵn trong Python, không cần cài đặt gì thêm như ==os==, ==sys==, ==math==, ==datetime==

import os
import sys
import math

Nên gom nhóm thư viện chuẩn vào đầu tiên và giữ nguyên thứ tự alphabet nếu có thể. 2. Thư viện bên thứ ba (Third-party packages) Tiếp theo là các thư viện được cài thêm qua ==pip==, như ==requests==, ==numpy==, ==pandas==, ==matplotlib==

import numpy as np
import pandas as pd
import requests

3. Module nội bộ (Local application imports) Cuối cùng là các module do bạn tự viết, nằm trong chính dự án của bạn.

from myproject.utils import parse_data
from myproject.models import User

2.5. Dòng trắng và Cấu trúc Hàm:

Dòng trắng trong hàm

  • Dùng 2 dòng trắng để phân tách giữa các class hoặc hàm độc lập.
  • Với các hàm bên trong class, chỉ cần 1 dòng trắng giữa các phương thức. Điều này giúp mã nguồn thoáng, có nhịp nghỉ rõ ràng, dễ định hình cấu trúc khi đọc từ trên xuống.
import os
import numpy as np
import pandas as pd
\#Space
\#Space
class Payment:
    variables = None
\#Space
    def __init__(self, amount, date):
        self.amount = amount
        self.date = date
\#Space
    def __repr__(self):
        return f"Payment(amount={self.amount}, date={self.date})"
\#Space
    def calculate_total(self):
        return self.amount

Cấu trúc class PEP-8 khuyến nghị thứ tự bên trong một class như sau:

  1. =="""Docstring mô tả class"""==
  2. Biến class (nếu có)
  3. ==__init__() – constructor==
  4. Các phương thức công khai (public method)
  5. Các phương thức riêng tư (private method: ==__method==)
  6. Các ==@staticmethod hoặc @classmethod== Việc giữ đúng thứ tự giúp class dễ định vị chức năng hơn, đặc biệt với file dài.
class Product:
    """
    Lớp Product đại diện cho một sản phẩm trong hệ thống bán hàng.
    Attributes:
        name (str): Tên sản phẩm.
        price (float): Giá sản phẩm.
        stock (int): Số lượng trong kho.
    """
    TAX_RATE = 0.08  # Hằng số class
    def __init__(self, name: str, price: float, stock: int = 0):
        """
        Khởi tạo một đối tượng Product.
        Args:
            name (str): Tên sản phẩm.
            price (float): Giá bán.
            stock (int): Số lượng tồn kho (mặc định là 0).
        """
        self.name = name
        self.price = price
        self.stock = stock
    def __repr__(self):
        return f"Product(name={self.name}, price={self.price}, stock={self.stock})"
    def apply_discount(self, rate: float) -> None:
        """
        Áp dụng chiết khấu cho sản phẩm.
        Args:
            rate (float): Tỷ lệ chiết khấu (0–1).
        """
        self.price *= (1 - rate)
    def _check_stock(self) -> bool:
        """Hàm riêng kiểm tra tồn kho."""
        return self.stock > 0
    @staticmethod
    def currency_format(amount: float) -> str:
        """Trả về định dạng chuỗi tiền tệ."""
        return f"${amount:,.2f}"

Cấu trúc hàm gọn gàng Bên trong mỗi hàm, nên giữ một thứ tự logic như sau:

  1. Docstring mô tả đầu vào, đầu ra, chức năng
  2. Các bước kiểm tra đầu vào (validation)
  3. Code xử lý chính
  4. Kết thúc bằng ==return== Mỗi hàm chỉ nên làm đúng một việc (Single Responsibility). Nếu dài hơn 20–30 dòng, hãy cân nhắc chia nhỏ.
def calculate_total_price(price: float, quantity: int, tax_rate: float = 0.1) -> float:
    """
    Tính tổng giá tiền sau thuế.
    Args:
        price (float): Giá của một sản phẩm.
        quantity (int): Số lượng mua.
        tax_rate (float): Tỷ lệ thuế áp dụng (mặc định là 10%).
    Returns:
        float: Tổng tiền cần thanh toán.
    """
    # Validate đầu vào
    if price < 0 or quantity <= 0:
        raise ValueError("Giá và số lượng phải là số dương.")
    # Logic chính
    subtotal = price * quantity
    tax = subtotal * tax_rate
    total = subtotal + tax
    return total

2.6. Tài liệu hóa (Documentation)

Viết code tốt không chỉ là làm cho máy hiểu, mà còn là làm cho người khác hiểu được logic và mục đích.

  • Giảm thời gian đọc hiểu

  • Dễ bảo trì

  • Tăng hiệu quả teamwork

  • Tự động sinh tài liệu với công cụ như Sphinx image 7 25.png 1. Docstring là gì? Là đoạn văn bản nằm ngay sau định nghĩa hàm/lớp/module, nằm giữa 3 dấu ngoặc kép =="""..."""==, dùng để mô tả mục đích, tham số và giá trị trả về. Các định dạng phổ biến:

  • Google style - phổ biến, dễ đọc:

    def greet(name: str) -> str:
        """
        Tạo lời chào cá nhân hóa.
     
        Args:
            name (str): Tên người nhận.
     
        Returns:
            str: Chuỗi lời chào.
        """
        return f"Hello, {name}!"
  • NumPy style - chi tiết, phù hợp xử lý số liệu lớn.

  • reStructuredText - dùng khi bạn muốn tạo tài liệu web bằng Sphinx. 2. Type hint và annotations Python hỗ trợ khai báo kiểu dữ liệu với cú pháp ==: type -> return_type==, tuân theo PEP 484. Điều này giúp:

  • IDE đưa gợi ý tốt hơn

  • Bắt lỗi sớm khi truyền sai kiểu

  • Code dễ đọc, rõ ràng về contract

def calculate_total(price: float, quantity: int) -> float:
    return price * quantity

Dùng thư viện typing đển import thêm các kiểu dữ liệu nâng cao hơn.

from typing import List, Dict, Optional, Union
def analyze_data(
    values: List[float],
    config: Optional[Dict[str, Union[int, str]]] = None
) -> Dict[str, float]:
    ...

Dùng mypy, pyright hoặc bật gợi ý trong VS Code để kiểm tra kiểu dữ liệu khi code. 3. Tài liệu hóa module & project Khi làm dự án thực tế, tài liệu không chỉ nằm ở từng hàm, mà còn phải bao gồm cấp độ module và toàn bộ project:

  • ==""" Module docstring """==: Viết ở đầu file ==.py – mô tả nội dung và các hàm chính.==
  • ==README.md==: Giải thích cách cài đặt, cách chạy, cấu trúc thư mục.
  • ==CONTRIBUTING.md==: Hướng dẫn cách tham gia đóng góp, tiêu chuẩn code.
  • ==Sphinx==: Tự sinh tài liệu API từ docstring.
  • ==Wiki==: Tài liệu chi tiết cho các tính năng phức tạp, business logic, flow dữ liệu.

2.7. Công cụ kiểm tra Pep-8 và code cơ bản

Thực tế thì các quy tắc dài dòng trên bạn không cần thiết phải nhớ vì đã có các công cụ hỗ trợ bản kiểm tra rồi, bạn chỉ cần hiểu tại sao bạn cần viết những dòng code sạch thôi. Dưới đây là 4 công cụ phổ biến giúp bạn kiểm tra code đã clean chưa: 1. Flake8 – Kiểm tra style theo PEP-8 Kết hợp nhiều công cụ như ==pyflakes==, ==pycodestyle==, ==mccabe đ==ể phát hiện lỗi:

  • Cú pháp sai
  • Style không đúng chuẩn PEP-8
  • Code quá phức tạp Cài đặt:
pip install flake8

Sử dụng:

flake8 your_file.py

2. Black – Format code tự động Tự động căn chỉnh, xuống dòng, căn lề, xoá khoảng trắng dư… Được xem là chuẩn không cần chỉnh trong các dự án lớn. Cài đặt:

pip install black

Sử dụng:

black your_file.py

3. Pylint – Phân tích sâu hơn Ngoài kiểm tra cú pháp, Pylint còn:

  • Chấm điểm chất lượng code của bạn
  • Gợi ý cách đặt tên
  • Phát hiện code dư thừa, dead code
  • Hỗ trợ viết plugin tùy chỉnh Cài đặt:
pip install pylint

Sử dụng:

pylint your_file.py

4. Mypy – Kiểm tra type annotations Giúp bạn xác minh kiểu dữ liệu tĩnh (type hint) khi viết code Python. Phát hiện lỗi trước khi runtime, đặc biệt hữu ích trong các hệ thống lớn. Cài đặt:

pip install mypy

Sử dụng:

mypy your_file.py

Mẹo:

  • ==Tích hợp CI/CD==: Các công cụ này có thể tích hợp vào pipeline GitLab, GitHub Actions để tự động kiểm tra khi push/merge.
  • ==Pre-commit hook==: Dùng với ==pre-commit để kiểm tra code trước khi commit.==
  • ==IDE Support==: Cả VSCode và PyCharm đều hỗ trợ tích hợp trực tiếp, hiển thị lỗi realtime. Ví dụ về Flake8:
def greeting(name):
    print("Hello, " + name)
greeting("Alice")
 greeting("Bob")

Mặc dù đoạn code trên có thể chạy, nhưng nếu bạn chạy ==flake8 greeting.py==, bạn sẽ gặp lỗi:

greeting.py:5:1: E999 IndentationError: unexpected indent

Dòng số 5 (==greeting("Bob")==) bị thụt lề sai.

2.8. Cấu trúc tiêu chuẩn cho Dự án Python

Một dự án Python tổ chức tốt sẽ dễ bảo trì, dễ mở rộng, và dễ để nhiều người tham gia. Dưới đây là cấu trúc thư mục phổ biến được cộng đồng sử dụng: Cấu trúc gợi ý:

CopyEdit
project_name/
├── README.md               ← Giới thiệu project
├── requirements.txt        ← Danh sách thư viện cần cài
├── setup.py                ← Dùng để cài đặt project như package
├── src/                    ← Thư mục chứa mã nguồn chính
│   └── __init__.py
├── tests/                  ← Unit test, integration test (pytest, unittest)
├── docs/                   ← Hướng dẫn, tài liệu API
├── resources/              ← Dữ liệu tĩnh, file cấu hình, template HTML,...
└── .gitignore              ← File cấu hình bỏ qua khi commit

Ví dụ template: https://github.com/khoanta-ai/python_project_template Cấu Trúc Cho Dự Án Data Science (Theo Cookiecutter) Đối với dự án phân tích dữ liệu hoặc machine learning, bạn nên dùng cấu trúc như Cookiecutter Data Science – được nhiều công ty và cộng đồng áp dụng. Cấu trúc chuẩn Cookiecutter:

project/
├── data/                   ← Dữ liệu (raw, processed, interim)
├── notebooks/              ← Jupyter notebooks (EDA, training, demo)
├── models/                 ← Mô hình đã huấn luyện
├── src/                    ← Code xử lý: tải data, build features, train
│   ├── data/
│   ├── features/
│   ├── models/
│   └── visualization/
├── references/             ← Tài liệu phụ: manuals, schema
├── reports/                ← Báo cáo, biểu đồ
│   └── figures/
├── docs/                   ← Hướng dẫn, mô tả kỹ thuật
├── requirements.txt
├── setup.py
├── tox.ini                 ← Cấu hình chạy test
└── README.md

Tham khảo: https://cookiecutter-data-science.drivendata.org/ image 8 25.png

3. Viết Pythonic code

3.1. The Zen of Python (Triết lý Python)

Python không chỉ là ngôn ngữ, nó là một triết lý, một tư duy lập trình.
Và bạn có thể tìm thấy triết lý đó trong một câu lệnh kỳ lạ:

import this

Câu lệnh này sẽ hiện ra The Zen of Python – 19 nguyên tắc thiết kế cốt lõi của Python được Tim Peters viết trong PEP 20: image 84.png Ref: https://peps.python.org/pep-0020/ Zen of Python khuyến khích bạn đơn giản hóa mọi thứ, rõ ràng, tối giản, trực quan, và tránh sự mơ hồ. Thay vì viết code chỉ để chạy được, bạn học cách viết code để người khác (và chính bạn sau 1 tuần sau) có thể hiểu được ngay. Đây là một số ví dụ Python cho các nguyên tắc trong “The Zen of Python”: 1. Explicit is better than implicit – Rõ ràng tốt hơn ẩn ý Ẩn ý quá mức:

def get_data():
    return x if x else y  
    # x và y đến từ đâu?
    # dùng khi nào?

Rõ ràng, dễ hiểu:

def get_data(primary, fallback):
    if primary:
        return primary
    return fallback

2. Beautiful is better than ugly – Cái đẹp tốt hơn cái xấu Xấu xí, khó đọc, không đồng nhất:

def c(x):return[x*2for x in x if x>0]

Dễ nhìn hơn, dễ đọc hơn, rõ ràng hơn

def clean_double_positive(numbers):
    return [x * 2 for x in numbers if x > 0]

3. Simple is better than complex – Đơn giản tốt hơn phức tạp Code tỏ ra nguy hiểm:

def multiply(a, b): return eval(str(a) + '*' + str(b))

Dễ nhìn hơn, dễ đọc hơn, rõ ràng hơn

def multiply(a, b):
    return a * b

4. Flat is better than nested – Phẳng tốt hơn lồng nhau Lồng nhau như bánh chưng 3 lớp:

for user in users:
    if user.is_active:
        if user.age >= 18:
            send_email(user)

Viết phẳng, rõ logic hơn:

for user in users:
    if not user.is_active or user.age < 18:
        continue
    send_email(user)

Chúng ta đã thấy Zen of Python không chỉ dạy ta viết code hay và đẹp hơn, nó còn dạy ta suy nghĩ rõ ràng, thiết kế tinh gọn, và chọn cái đơn giản thay vì phức tạp không cần thiết. Và nếu nhìn rộng hơn, bạn sẽ thấy:

  • Rõ ràng bao giờ cũng tốt hơn mập mờ, dù trong code hay trong giao tiếp
  • Cái đẹp, dù là trong giao diện, câu lệnh hay một hành động tử tế sẽ luôn có sức mạnh thuyết phục
  • Và nếu phải chọn, phức tạp hợp lý vẫn còn hơn là sự rối rắm thiếu kiểm soát Vì vậy, triết lý của Python không chỉ hữu ích khi bạn viết phần mềm, mà còn có thể là kim chỉ nam khi bạn học tập, làm việc và sống một cách mạch lạc, sáng suốt hơn. image 1 44.png

3.2. Pythonic Code

Pythonic Code nghĩa là tận dụng tối đa tính năng và đặc điểm riêng của Python để viết code. Code Pythonic dễ đọc, dễ hiểu, ngắn gọn như đọc tiếng Anh, đồng thời tuân thủ các quy ước và triết lý của Python.

1. Indexes & Slices

Trong Python, mọi ==list==, ==tuple==, hoặc ==string đều là sequences và bạn có thể tiếp cận từng phần tử trong đó bằng indexing hoặc slicing. Đây là cách mà Python cho phép bạn làm việc với dữ liệu rất nhanh gọn:== Index cơ bản: lấy 1 phần tử

numbers = [1, 2, 3, 4, 5]
first = numbers[0]     # 1
last  = numbers[-1]    # 5
  • index âm để đếm ngược từ cuối danh sách. Slice cơ bản: lấy nhiều phần tử
first_three = numbers[:3]    # [1, 2, 3]
last_three  = numbers[-3:]   # [3, 4, 5]
middle      = numbers[1:4]   # [2, 3, 4]
  • Cú pháp chung: ==list[start:stop:step] (lấy từ start đến trước stop==và bước nhảy là ==step==)
  • Nếu bỏ ==start==, mặc định là 0. Nếu bỏ ==stop==, mặc định đến cuối danh sách. image 2 42.png Slice nâng cao: step và đảo chiều
data = [10, 20, 30, 40, 50, 60]
even = data[::2]       # [10, 30, 50]
odd  = data[1::2]      # [20, 40, 60]
reverse = data[::-1]   # [60, 50, 40, 30, 20, 10]
  • ==[::-1] là một trick phổ biến để đảo ngược thứ tự phần tử.== Một số trick Pythonic hay dùng:
  • Sao chép shallow:
original = [1, 2, 3]
copy_list = original[:]    # Tạo bản sao (shallow) nhanh chóng
  • Thay thế một đoạn:
letters = list("abcdef")
letters[1:3] = ["X", "Y"]    # ['a', 'X', 'Y', 'd', 'e', 'f']

→ Slice có thể dùng để thay thế hàng loạt phần tử trong list một cách linh hoạt.

  • Xoá phần tử bằng slice:
numbers = [1, 2, 3, 4, 5]
numbers[1:3] = []            # [1, 4, 5]

→ Khi gán một slice bằng ==[]==, Python sẽ xoá toàn bộ đoạn đó. image 3 40.png

2. List, Dict, Set Comprehension

List, Dict, Set Comprehensions là cú pháp đặc trưng trong Python giúp bạn tạo danh sách (list), từ điển (dict) và tập hợp (set) một cách ngắn gọn, dễ đọc, Pythonic. Không Pythonic:

numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
    squares.append(n**2)
  • Cách viết này giống như nhiều ngôn ngữ khác: rõ ràng nhưng dài dòng.
  • Mất 3 dòng để biểu diễn một thao tác đơn giản. List Comprehension:
squares = [n**2 for n in numbers]
  • Cùng kết quả nhưng ngắn gọn hơn, dễ đọc hơn giống Tiếng Anh.
  • Đây là dạng phổ biến của list comprehension – giúp biến đổi danh sách dễ dàng. Dict Comprehension
names = ['Alice', 'Bob', 'Charlie']
lengths = {name: len(name) for name in names}
-> {'Alice': 5, 'Bob': 3, 'Charlie': 7}
  • Tạo một dictionary ánh xạ tên ➝ độ dài tên.
  • Sử dụng cú pháp: =={key_expr: value_expr for item in iterable}== Set Comprehension
numbers = [1, 2, 2, 3, 4, 4]
unique_squares = {n**2 for n in numbers}
  • Kết quả là một tập hợp (set) chứa gi==á trị không trùng lặp==: =={1, 4, 9, 16}==.

  • Cú pháp gần giống list comprehension nhưng dùng =={} thay vì []==.

    Tác dụng:

    • Tự động loại bỏ các giá trị trùng.
    • Thường dùng trong xử lý dữ liệu để lấy unique items. image 4 34.png

3. Context Manager

Context Manager là một cơ chế giúp quản lý và giải phóng tài nguyên một cách tự động, điển hình như đóng file, ngắt kết nối database, giải phóng lock, benchmark thời gian, tạm thay đổi cấu hình, v.v. Điều này đảm bảo tài nguyên luôn được giải phóng đúng cách, tránh memory leak, giúp code ngắn gọn và an toàn hơn khi xử lý ngoại lệ. Không Pythonic:

file = open("example.txt", "r")
content = file.read()
file.close()  # Phải nhớ đóng file
  • Nếu quên file.close(), bạn có thể gây ra memory leak hoặc lỗi tài nguyên chưa được giải phóng. Pythonic: Dùng with để tự động quản lý tài nguyên
with open("example.txt", "r") as file:
    content = file.read()
  • Câu lệnh with sẽ tự động đóng file khi khối lệnh kết thúc, kể cả khi xảy ra lỗi. Decorator @contextmanager Chúng ta có thể sử dụng decorator @contextmanager từ module contextlib để dễ dàng tạo context manager chỉ với một hàm generator đơn giản, thay vì phải định nghĩa class với phương thức ==__enter__() __exit__()== Dùng ==**__enter__()** **__exit__()**==
class FileReader:
    def __init__(self, filename):
        self.filename = filename
        self.file = None
    def __enter__(self):
        print(">>> Opening file...")
        self.file = open(self.filename, 'r')
        return self.file  # Giá trị này sẽ được gán cho biến sau `as`
    def __exit__(self, exc_type, exc_value, traceback):
        print(">>> Closing file...")
        if self.file:
            self.file.close()
        # Nếu không muốn chặn exception, return False hoặc None
        # Nếu muốn suppress exception (ẩn đi), return True
        return False
# Cách sử dụng
with FileReader("file.txt") as f:
    content = f.read()
    print(content)

Dùng decorator @contextmanager

from contextlib import contextmanager \#import module contexlib
@contextmanager
def file_reader(filename):
    f = open(filename, "r")  # setup: mở file
    try:
        yield f              # cho phép khối with sử dụng file này
    finally:
        f.close()           # cleanup: luôn đóng file sau khi with xong
# Cách sử dụng
with file_reader("file.txt") as file:
    print(file.read())
  • Mọi thứ trước ==yield là phần khởi tạo tài nguyên (giống __enter__==)
  • Mọi thứ sau ==yield là phần giải phóng tài nguyên (giống __exit__==)
  • ==yield f trả đối tượng file ra ngoài để sử dụng trong khối with== image 5 32.png

4. So Sánh và Điều Kiện

Trường hợp 1: So sánh với None Sai Pythonic

x = None
if x == None:  # SAI!
    print("x is None")

Đúng Pythonic

x = None
if x is None:
    print("x is None")
elif x is not None:
    print("x is not None")

Dù về mặt giá trị ==x == None có thể đúng, nhưng về mặt bản chất thì đây là so sánh equality, trong khi None là một singleton object (tức là chỉ có duy nhất một bản thể None trong Python runtime). Hơn nữa dùng == có thể dẫn đến bug nếu bạn đang xử lý một object có phương thức __eq__ được override không chuẩn.== Trường hợp 2: So sánh Boolean Trong Python, biến Boolean (==True==, ==False==) tự thân đã mang giá trị logic nên không cần so sánh với ==== True hay != False==. Sai Pythonic

ready = True
if (ready == True):  # Quá rườm rà
    print("Ready")
valid = False
if (valid != False):  # Dài dòng
    print("Valid")

Đúng Pythonic

ready = True
if ready:  # Tận dụng truthiness
    print("Ready")
valid = False
if not valid:  # Rõ ràng, gọn gàng
    print("Invalid")

Trường hợp 3: Kiểm tra chuỗi, list hoặc dict rỗng Trong Python một chuỗi, list hoặc dict rỗng được đánh giá là ==False và không rỗng là True==. Hãy tận dụng truthiness để giúp code ngắn gọn và dễ đọc hơn. Sai Pythonic

if len(name) > 0:
    print(name)
if len(items) == 0:
    print("Empty")

Đúng Pythonic

if name:
    print(name)
if not items:
    print("Empty list")

Trường hợp 4: Kiểm tra trong collection Sử dụng ==in là cách viết chuẩn Pythonic để kiểm tra sự tồn tại trong collection.== Không cần vòng lặp thủ công và biến tạm sẽ giúp giảm rủi ro bug và tăng hiệu suất đọc code. Sai Pythonic

active_users = ["alice", "bob", "charlie"]
user = "bob"
found = False
for u in active_users:
    if u == user:
        found = True
        break
if found:
    print("User is active")

Đúng Pythonic

active_users = ["alice", "bob", "charlie"]
user = "bob"
if user in active_users:
    print("User is active")

Trường hợp 5: Chaining comparison Chúng ta có thể kết chuỗi các phép so sánh giống như cách viết toán học 0 ≤ 𝑥 ≤ 100 để giúp trực quan hơn Sai Pythonic

if value >= 0 and value <= 100:
    print("Valid percentage")

Đúng Pythonic

if 0 <= value <= 100:
    print("Valid percentage")

image 6 29.png

5. Properties và dấu Underscore_ (dunder)

Khi viết class trong Python, việc kiểm soát truy cập và tổ chức các thuộc tính/methods không chỉ là vấn đề cú pháp, nó ảnh hưởng trực tiếp đến khả năng bảo trì, mở rộng và tính an toàn của chương trình tuân theo các quy tắc OOP. ==**@property**====: Gọi method như thuộc tính Trong lập trình hướng đối tượng, @property cho phép sử dụng method như thuộc tính, giúp code gọn, dễ kiểm soát truy cập.== Ví dụ:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    @property
    def area(self):
        return self.width * self.height

Khi sử dụng:

rect = Rectangle(3, 4)
print(rect.area)  # 12
  • Bạn có thể truy cập method ==area() như biến (====.area==). Điều này giúp thêm logic kiểm tra khi cần mà không đổi cách dùng. Hơn nữa nó tăng tính đóng gói và bảo trì tốt hơn. Underscore_: Trong Python, tên biến/hàm có dấu ==_ mang ý nghĩa rõ ràng về phạm vi sử dụng và ý định thiết kế. **_var**== – Single Underscore (Biến nội bộ)

Quy ước rằng biến là “nội bộ” hoặc dùng riêng trong class/module. Vẫn có thể truy cập được từ bên ngoài, nhưng khuyến cáo là không nên.

class MyClass:
    def __init__(self):
        self._internal_value = 42

==**__var**== – Double Underscore (Name Mangling)

Biến sẽ được đổi tên ngầm thành _ClassName__var, tránh xung đột khi kế thừa lớp.

class Secure:
    def __init__(self):
        self.__token = "secret"

Bạn không thể gọi ==obj.__token==, nhưng có thể gọi ==obj._Secure__token (không khuyến khích). **__var__**== – Dunder Methods (Magic Methods)

Dành riêng cho những hàm đặc biệt do Python định nghĩa như init, str, len. Không nên tự tạo kiểu tên này nếu không định override hành vi mặc định. Ví dụ: Định nghĩa lại phép so sánh == bằng cách định nghĩa lại (override) __eq__ Ta có:

k1 = khoa("x", "Khoa x")
k2 = khoa("y", "Khoa y")
an = sv("01", "An", "k1")
an2 = sv("01", "An", "k2")

Khi ta chưa định nghĩa lại __eq__:

print(an==an2) -> False

Định nghĩa lại __eq__ chỉ so sánh dựa trên __makh:

def __eq__ (self, other):
		return (self.__makh.__eq__(other.__makh)
print(an==an2) -> True

==**_**== – Underscore đơn lẻ

Dùng cho biến tạm, biến không quan trọng hoặc kết quả gần nhất trong interpreter.

for _ in range(3):
    print("Hello")  # _ không có ý nghĩa ngoài vòng lặp

image 7 24.png

4. Nguyên lý chung để viết code tốt

Nguyên lý chung để viết code tốt hơn, bền vững và dễ bảo trì đều dựa trên các nền tảng cốt lõi như:

  • DRY (Don’t Repeat Yourself): Tránh lặp lại code, mỗi chức năng, phép toán nên được định nghĩa một lần duy nhất trong hệ thống.
  • YAGNI (You Aren’t Gonna Need It): Không thêm tính năng nếu bạn thật sự không cần đến nó
  • KISS (Keep It Simple, Stupid): Luôn ưu tiên các giải pháp đơn giản, tránh những thiết kế phức tạp khó hiểu
  • Defensive Programming: lập trình phòng thủ, đề phòng các input không mong muốn từ người dùng và đảm bảo các lỗi tiềm ẩn có thể xảy ra.
  • Separation of Concerns: phân chia trách nhiệm rõ ràng giữa các thành phần, các module trong hệ thống. Tiếp theo, hãy đi sâu vào và phân tích từng nguyên lý:

4.1. DRY (Don’t Repeat Yourself)

image 86.png Nguyên tắc DRY được giới thiệu bởi Andy Hunt và Dave Thomas trong cuốn The Pragmatic Programmer. Tư tưởng cốt lõi là:

“Tránh lặp lại code, mỗi phần kiến thức trong hệ thống phải có một biểu diễn duy nhất, rõ ràng và có thẩm quyền” Vì sao cần tránh lặp lại? Hãy tưởng tượng bạn có 3 hàm giống nhau, chỉ khác 1–2 dòng. Khi logic thay đổi, bạn sẽ phải sửa cả 3 nơi. Việc bỏ sót hoặc sửa lệch nhau rất dễ gây lỗi. Lúc đó, bạn không còn control được code, và bảo trì trở thành ác mộng. Ví dụ minh họa: Vi phạm DRY

def print_morning_greeting(name):
    print(f"Good morning, {name}!")
    print("I hope you have a wonderful day.")
    print("Don't forget to drink water and take breaks.")
    print("------------------------------------------------")
def print_evening_greeting(name):
    print(f"Good evening, {name}!")
    print("I hope you had a wonderful day.")
    print("Don't forget to drink water and take breaks.")
    print("------------------------------------------------")
print_morning_greeting("Alice")
print_evening_greeting("Bob")

Tuân thủ DRY

def print_greeting(name, time_of_day):
    print(f"Good {time_of_day}, {name}!")
    print(f"I hope you {'have' if time_of_day == 'morning' else 'had'} a wonderful day.")
    print("Don't forget to drink water and take breaks.")
    print("------------------------------------------------")
print_greeting("Alice", "morning")
print_greeting("Bob", "evening")

Lợi ích khi áp dụng DRY

  • Giảm rủi ro khi thay đổi: chỉ cần sửa một nơi.
  • Tăng tính tái sử dụng: dễ gom thành thư viện dùng lại.
  • Code gọn gàng: rõ ràng hơn cho người đọc và đồng đội. Cách áp dụng
  • Tách các đoạn lặp lại thành hàm riêng (function), class, hoặc module.
  • Nếu có đoạn giống đến 70% và sẽ thay đổi độc lập, vẫn nên chia nhỏ để không tạo dependency không cần thiết.

4.2. YAGNI (You Aren’t Gonna Need It)

image 1 46.png Trong quá trình phát triển phần mềm, một sai lầm phổ biến mà nhiều lập trình viên thường mắc phải là cố gắng dự đoán trước tương lai, viết thêm hàng loạt tính năng hoặc lớp hỗ trợ mà… chưa ai yêu cầu. Đây là lúc chúng ta cần đến nguyên tắc YAGNI. Nguyên tắc YAGNI, được giới thiệu bởi Ron Jeffries – một trong ba nhà sáng lập Agile. Ý tưởng rất rõ ràng:

❝ Đừng thêm bất kỳ chức năng nào vào hệ thống trừ khi bạn thực sự cần nó. ❞ Việc thêm vào quá nhiều logic “lo xa” không chỉ gây tốn thời gian, mà còn khiến code trở nên phức tạp, khó kiểm soát, tăng technical debt và giảm tốc độ release. Quan trọng hơn, phần lớn những tính năng để dành đó sẽ không bao giờ được dùng đến.

Technical debt:

“Nợ kỹ thuật”: nghe có vẻ nguy hiểm, nhưng thực ra… khá giống chuyện vay tiền để làm nhà cho kịp Tết.

Khi viết code nhanh để chạy cho kịp deadline, bạn có thể bỏ qua kiểm tra lỗi, code cứng, viết tắt logic, hoặc không viết test. Những điều này giúp dự án chạy ngay lúc đó, nhưng lại giống như vay nợ bạn sẽ phải trả giá ở tương lai: code khó sửa, dễ lỗi, khó mở rộng.

Technical Debt = Tối ưu thời gian hiện tại = Tăng chi phí bảo trì sau này Ví dụ minh họa: Vi phạm YAGNI:

class SuperCalculator:
    def __init__(self):
        self.history = []
        self.memory = 0
        self.scientific_mode = False
        self.conversion_rates = {
            "USD_to_EUR": 0.85,
            "USD_to_GBP": 0.73,
            "GBP_to_USD": 1.37,
        }
    def add(self, a, b):
        result = a + b
        self.history.append(f"{a} + {b} = {result}")
        return result
    def subtract(self, a, b):
        result = a - b
        self.history.append(f"{a} - {b} = {result}")
        return result
    def toggle_scientific_mode(self):
        self.scientific_mode = not self.scientific_mode
        return self.scientific_mode

Tuân thủ YAGNI:

class SimpleCalculator:
    def add(self, a, b):
        return a + b
    def subtract(self, a, b):
        return a - b
# Usage
calc = SimpleCalculator()
print(calc.add(5, 3))  # 8

Phiên bản đơn giản chỉ làm đúng điều được yêu cầu là cộng và trừ. Các tính năng mở rộng như lưu lịch sử, chuyển đổi tiền tệ hay chế độ khoa học có thể được thêm vào sau khi thực sự phát sinh nhu cầu. Lợi ích khi áp dụng YAGNI:

  • Tiết kiệm thời gian và công sức phát triển.
  • Giảm technical debt. Giúp tránh những phần code chết không dùng tới, gây rối.
  • Giữ cho hệ thống đơn giản, rõ ràng và dễ bảo trì. Code nhỏ gọn dễ test, dễ hiểu, dễ refactor hơn về sau. Khi nào nên áp dụng: Nếu bạn đang nghĩ đến việc “viết thêm cho chắc” nhưng chưa có yêu cầu rõ ràng thì hãy dừng lại. YAGNI khuyên bạn hãy chỉ viết những gì cần thiết ở hiện tại, không viết cho một tương lai chưa có yêu cầu cụ thể.

4.3. KISS (Keep It Simple, Stupid)

image 2 44.png Trong phát triển phần mềm, phức tạp không phải là thông minh, đó là lý do nguyên tắc KISS ra đời.

❝ Giải pháp đơn giản luôn là lựa chọn tối ưu khi không có lý do chính đáng để làm phức tạp hóa vấn đề. ❞ Thực tế, code đơn giản không chỉ dễ đọc hơn mà còn giảm bug, dễ bảo trì và dễ cộng tác nhóm. Ngược lại, những đoạn mã “ngầu” nhưng khó hiểu có thể trở thành rào cản lớn khi bàn giao hoặc mở rộng hệ thống. Ví dụ minh họa: Vi phạm KISS:

def is_even_complex(number):
    binary = bin(number)[2:]    # Convert to binary
    last_digit = binary[-1]     # Check last digit
    return int(last_digit) == 0 # Determine parity

Tuân thủ KISS:

def is_even_simple(number):
    return number % 2 == 0

Lợi ích khi áp dụng KISS

  • Dễ đọc – Dễ debug – Dễ bảo trì – Dễ mở rộng
  • Giảm bug tiềm ẩn do logic phức tạp không cần thiết
  • Tăng hiệu suất làm việc nhóm: ai cũng đọc hiểu và góp phần vào codebase Khi nào nên áp dụng? Mọi lúc mọi nơi, trừ khi có lý do xác đáng để làm khác. Python vốn sinh ra với triết lý: ❝ Simple is better than complex ❞ (Zen of Python) → Hãy viết code đúng tinh thần đó: ngắn gọn, rõ ràng, hiệu quả.

4.4. Defensive Programming (Lập trình phòng thủ)

image 3 42.png Lập trình phòng thủ là kỹ thuật viết code với tư duy:

❝ Mọi thứ có thể sai đều sẽ sai – nên hãy chuẩn bị trước. ❞ Tư duy này không bi quan, mà thực tế, bởi vì trong thế giới thật, input có thể đến từ bất kỳ đâu: người dùng, API, hệ thống bên thứ ba, thậm chí từ đồng đội bạn viết sai. Do đó, bạn không thể giả định rằng mọi thứ luôn đúng. Hãy lập trình với giả định là sẽ có lỗi xảy ra. Ví dụ minh họa: Thiếu kiểm tra Input:

def divide(a, b):
    return a / b
print(divide(5, 0))  
# Gây ZeroDivisionError

Có lập trình phòng thủ:

def divide(a, b):
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("Tham số phải là số.")
    if b == 0:
        raise ValueError("Không thể chia cho 0.")
    return a / b

Nguyên tắc cơ bản

  • Luôn kiểm tra đầu vào (type, range, null, empty…).
  • Không bao giờ tin tưởng input bên ngoài: người dùng, file, request từ API.
  • Fail sớm nếu có điều kiện bất thường. Lợi ích của lập trình phòng thủ
  • Giúp hệ thống bền vững hơn, ít bị vỡ khi dữ liệu bất thường.
  • Giảm technical debt vì dễ trace lỗi hơn.
  • Tăng khả năng mở rộng, vì mỗi hàm đều tự kiểm soát rủi ro của chính nó.
  • Tối ưu trải nghiệm người dùng không bị gián đoạn bởi lỗi không rõ lý do.

Error handling (Cách xử lý lỗi)

Nên bắt lỗi cụ thể thay vì chung chung (Exception). Không bao giờ nuốt lỗi mà không xử lý hoặc ghi log. Một lỗi bị nuốt đi trong im lặng có thể khiến bạn mất hàng giờ debug mà chẳng biết chuyện gì đang xảy ra. Ví dụ minh họa: Bắt lỗi quá chung chung

try:
    with open("file.txt") as f:
        data = f.read()
        parsed = json.loads(data)
        result = 10 / parsed["value"]
except Exception:
    print("đã xảy ra lỗi")  
    # Lỗi bị bỏ qua hoàn toàn!

Có xử lý lỗi

try:
    with open("file.txt") as f:
        data = f.read()
        parsed = json.loads(data)
        result = 10 / parsed["value"]
except FileNotFoundError:
    logger.error("Không tìm thấy file.")
    raise ConfigError("File cấu hình không tồn tại")
except json.JSONDecodeError:
    logger.error("File không đúng định dạng JSON.")
except KeyError:
    logger.error("Thiếu trường 'value' trong dữ liệu.")
except ZeroDivisionError:
    logger.error("Giá trị 'value' không thể bằng 0.")

Lưu ý khi xử lý lỗi:

  • Luôn bắt lỗi cụ thể thay vì ==except Exception==.
  • Không được để ==pass mà không log hoặc xử lý gì cả.==
  • Sử dụng ==logging thay vì print để theo dõi lỗi chuyên nghiệp.==
  • Tạo custom exception nếu lỗi thuộc domain riêng của ứng dụng.
  • Raise lại lỗi khi cần để không nuốt mất stack trace. Mẹo bắt lỗi trong Python: ==1. Dùng **repr(E)** để in rõ loại lỗi và thông điệp==
except Exception as e:
    print(repr(e))  # Ví dụ: ZeroDivisionError('division by zero')import traceback
try:
    1 / 0
except Exception as e:
    traceback.print_exc()  # Hiển thị lỗi như log khi crash

==2. In cả **type(E)** để biết rõ loại lỗi==

except Exception as e:
    print(f"Lỗi: {e} ({type(e).__name__})")

image 4 36.png

Sử dụng Logging và Print hợp lý

Việc debug và ghi nhật ký là hai nhu cầu quan trọng nhưng mục đích khác nhau.

  • ==print() thường được dùng trong khi phát triển để xem nhanh giá trị biến.==
  • ==logging phù hợp cho môi trường sản phẩm nhờ vào sự linh hoạt và chi tiết.== So sánh Print vs Logging | | | |---|---| |Print|Logging| |Đơn giản, dễ sử dụng|Cấu hình linh hoạt| |Khó kiểm soát đầu ra|Nhiều cấp độ log (DEBUG, INFO, WARNING, ERROR…)| |Không phân loại mức độ nghiêm trọng|Dễ dàng bật/tắt theo cấu hình| |Khó tắt khi triển khai|Tự động thêm thời gian, file, dòng code| |Không lưu lại thông tin|Điều hướng log ra file, email, remote…| Ví dụ sử dụng Logging a. Logging cơ bản:
import logging
logging.basicConfig(level=logging.WARNING,
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                    filename='app.log')
logging.debug("Chi tiết cho debug")
logging.info("Thông tin thường")
logging.warning("Cảnh báo: có vấn đề")
logging.error("Lỗi nghiêm trọng")
logging.critical("NGUY HIỂM: app sắp crash")

b. Ghi log exception:

try:
    1 / 0
except Exception as e:
    logging.error("Có lỗi xảy ra: %s", e)

c. Tạm tắt log:

logging.disable(logging.CRITICAL)  # Tạm tắt tất cả
...
logging.disable(logging.NOTSET)    # Bật lại logging

Lời khuyên khi sử dụng:

  • Dùng ==print() khi debug nhanh trong notebook hoặc scripts ngắn.==
  • Dùng ==logging trong các dự án backend, script batch, pipeline dài, microservice…==
  • Luôn ghi lại exception bằng logging thay vì ==pass==. Sử dụng ==logger.error("msg", exc_info=True) hoặc %s với biến exception.==

4.5. Separation of Concerns (Phân chia trách nhiệm)

Một nguyên lý quan trọng giúp hệ thống dễ mở rộng và bảo trì là

Mỗi phần code chỉ nên đảm nhiệm một vai trò cụ thể. Nguyên lý này gọi là Separation of Concerns (SoC) tách các mối quan tâm thành từng phần riêng biệt. Ví dụ minh họa Không phân chia trách nhiệm rõ ràng

def process_user_data(user):
    # Xác thực dữ liệu
    if not user.email or '@' not in user.email:
        return False
    # Lưu vào database
    db.connect()
    db.execute("INSERT INTO users VALUES (?)", (user.to_dict(),))
    db.commit()
    # Gửi email
    smtp = SMTP('smtp.example.com')
    smtp.login('user', 'password')
    smtp.send_mail(
        to=user.email,
        subject='Chào mừng bạn!',
        body='Cảm ơn đã đăng ký...'
    )
    # Tạo báo cáo
    report = {"user": user.id, "time": time.now()}
    with open("report.json", "w") as f:
        json.dump(report, f)

Chúng ta có thể thấy hàm ==process_user_data() làm quá nhiều việc:== Kiểm tra dữ liệu người dùng, thao tác với database, gửi email và ghi log ra file. Tách nhiệm vụ rõ ràng hơn

def process_user_data(user):
    if validate_user(user):
        save_user(user)
        notify_user(user)
        log_activity(user)
def validate_user(user):
    return bool(user.email and '@' in user.email)
def save_user(user):
    repository = UserRepository()
    repository.add(user)
def notify_user(user):
    email_service = EmailService()
    email_service.send_welcome(user.email)
def log_activity(user):
    reporter = ActivityReporter()
    reporter.create_registration_report(user.id)

Ở đây đã tách mỗi trách nhiệm thành một hàm:

  • ==validate_user() chỉ kiểm tra dữ liệu.==
  • ==save_user() chịu trách nhiệm lưu vào database.==
  • ==notify_user() lo việc gửi email.==
  • ==log_activity() đảm nhiệm việc ghi nhận log.== Tại sao phải tách trách nhiệm các phần ra?
  • Giúp code dễ đọc, dễ hiểu và dễ debug.
  • Tăng khả năng tái sử dụng: mỗi hàm chỉ làm một việc nên có thể dùng lại linh hoạt.
  • Giảm rủi ro bug lan rộng: thay đổi logic ở một nơi không ảnh hưởng toàn hệ thống.
  • Dễ kiểm thử (test) vì từng hàm/module nhỏ có thể kiểm tra độc lập. image 5 34.png DRY và SoC đôi khi chúng xung đột với nhau. Nguyên lý DRY khuyên ta không nên lặp lại logic. Ngược lại với DRY, SoC khuyên rằng mỗi đoạn code/chức năng nên giải quyết một mối quan tâm duy nhất. Một ví dụ thực tế trong công ty:
  • Có 2 bảng dữ liệu trông giống nhau: ==ServiceLine CareType==.
  • Trông thì giống (cùng cột, cùng liên kết), nhưng chúng mang 2 khái niệm hoàn toàn khác:
    • ==ServiceLine==: khái niệm về marketing.
    • ==CareType==: khái niệm về nghiệp vụ y tế. Việc DRY hóa chỉ vì giống về mặt cấu trúc là sai. Điều này có thể gây ra vướng víu khi hệ thống phát triển.
  • Nếu chúng đại diện cho cùng một khái niệm → Hãy DRY.
  • Nếu khác khái niệm dù giống bề ngoài → Hãy tách biệt (SoC).

5. SOLID Principles

SOLID là tập hợp 5 nguyên tắc thiết kế hướng đối tượng, được xem như cốt lõi cho việc xây dựng phần mềm dễ mở rộng, dễ bảo trì và ít lỗi. Những nguyên tắc này đặc biệt hữu ích khi bạn làm việc trong hệ thống lớn, hoặc trong các team nhiều người.

  • S – Single Responsibility Principle (SRP):

    Mỗi class chỉ nên có một lý do để thay đổi. Tức là mỗi class nên chỉ làm một việc duy nhất, và làm nó thật tốt.

  • O – Open/Closed Principle (OCP):

    Code nên mở cho việc mở rộng (thêm tính năng mới), nhưng đóng với việc sửa đổi. Điều này giúp giảm nguy cơ phá vỡ hệ thống khi thay đổi.

  • L – Liskov Substitution Principle (LSP):

    Class con nên có thể thay thế class cha mà không ảnh hưởng tới tính đúng đắn của chương trình. Tức là “kế thừa đúng nghĩa”.

  • I – Interface Segregation Principle (ISP):

    Nên chia nhỏ các interface. Một class không nên bị ép phải implement các phương thức mà nó không dùng đến.

  • D – Dependency Inversion Principle (DIP):

    Lệ thuộc vào abstraction thay vì implementation cụ thể. Giảm sự phụ thuộc cứng nhắc giữa các module, tăng tính linh hoạt.

Giải thích thêm:

Interface là gì?

→ Interface là một tập hợp các phương thức trừu tượng (chỉ khai báo, không định nghĩa), yêu cầu các class triển khai (implement) phải tuân thủ và cài đặt đầy đủ.

Nó giống như bạn làm giao kèo với một class, rằng: “Nếu mày ký hợp đồng này, mày phải viết hết mấy cái hàm tao yêu cầu.”

5.1. Single Responsibility Principle (SRP)

Single Responsibility Principle (SRP) phát biểu rằng: Mỗi module, class hoặc function nên chỉ có một lý do duy nhất để thay đổi. Đây là nguyên tắc đầu tiên trong bộ SOLID và đóng vai trò nền tảng giúp code dễ hiểu, dễ kiểm thử và dễ bảo trì. Vì sao SRP quan trọng?

  • Một lớp – Một nhiệm vụ

    Khi một class chỉ chịu trách nhiệm duy nhất (ví dụ: lưu dữ liệu, gửi email, xử lý đầu vào…), thì logic sẽ rõ ràng và dễ theo dõi hơn. Việc thay đổi không gây ảnh hưởng dây chuyền đến những phần không liên quan.

  • Phân tách rõ ràng

    Thay vì gộp mọi thứ vào một class lớn như ==FileProcessor xử lý đọc, kiểm tra, ghi, phân tích… ta nên tách ra thành FileReader==, ==FileValidator==, ==FileAnalyzer==, ==FileWriter==... mỗi class làm một việc.

  • ==Dễ bảo trì và dễ kiểm thử==

    Đơn vị càng nhỏ càng dễ test. Khi viết test cho ==GradeCalculator bạn không phải quan tâm đến DatabaseSaver hay StudentReportPrinter==. Điều này làm tăng độ tin cậy của phần mềm.

image 82.png Ví dụ minh họa SRP Vi phạm SRP

class Student:
    def __init__(self, name, studet_id):
        self.name = name
        self.student_id = student_id
        self.grades = []
    def add_grade(self, course, grade):
        self.grades.append({"course": course, "grade": grade})
    def calculate_gpa(self):
        total = sum(item["grade"] for item in self.grades)
        return total / len(self.grades)
    def save_to_database(self):
        print(f"Saving student {self.name} to database...")
    def print_report(self):
        print(f"Student: {self.name}")
        print(f"ID: {self.student_id}")
        for item in self.grades:
            print(f"{item['course']}: {item['grade']}")
        print(f"GPA: {self.calculate_gpa():.2f}")

Class ==Student đang làm quá nhiều việc: lưu dữ liệu, tính GPA, in báo cáo. Điều này vi phạm nguyên tắc SRP nghiêm trọng.==

Tuân thủ SRP

class StudentData:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
        self.grades = []
    def add_grade(self, course, grade):
        self.grades.append({"course": course, "grade": grade})
class GradeCalculator:
    @staticmethod
    def calculate_gpa(student_data):
        total = sum(item["grade"] for item in student_data.grades)
        return total / len(student_data.grades)
class StudentRepository:
    @staticmethod
    def save_to_database(student_data):
        print(f"Saving student {student_data.name} to database...")
class StudentReportPrinter:
    @staticmethod
    def print_report(student_data):
        print(f"Student: {student_data.name}")
        print(f"ID: {student_data.student_id}")
        for item in student_data.grades:
            print(f"{item['course']}: {item['grade']}")
        gpa = GradeCalculator.calculate_gpa(student_data)
        print(f"GPA: {gpa:.2f}")

Giờ đây, mỗi class chỉ làm đúng phần việc của mình. image 1 42.png

5.2. Open/Close Principle (OCP)

Nguyên lý Open/Closed được phát biểu bởi Bertrand Meyer:

“Modules nên được mở để mở rộng, nhưng đóng để sửa đổi”. Điều này có nghĩa là khi hệ thống cần thêm tính năng mới, bạn nên thực hiện bằng cách thêm code mới, chứ không phải sửa code cũ đang hoạt động ổn định.

  • Mở Cho Việc Mở Rộng

    Code cần được thiết kế sao cho có thể dễ dàng thêm chức năng mới mà không làm thay đổi code hiện có.

    Ví dụ: khi thêm một phương thức thanh toán mới (ví dụ: MoMo), ta chỉ cần tạo một class mới mà không phải sửa class PaymentProcessor.

  • Đóng Cho Việc Sửa Đổi

    Sau khi một module được kiểm thử kỹ lưỡng, việc sửa đổi nội dung bên trong sẽ tiềm ẩn nhiều rủi ro. Thay vào đó, hãy mở rộng bằng cách sử dụng kế thừa, composition hoặc các pattern như Strategy, Plugin.

  • Sử Dụng Abstraction

    Trong Python, bạn có thể dùng ==abc.ABC @abstractmethod để định nghĩa interface rõ ràng. Điều này giúp code dễ mở rộng và tuân thủ đúng thiết kế, mà không cần sửa code đang tồn tại.==

    Ví dụ: tạo một ABC ==PaymentProcessor và kế thừa các class như CreditCardProcessor==, ==PayPalProcessor==.

  • Mở Rộng Bằng Kế Thừa hoặc Strategy

    Thay vì thêm điều kiện ==if-else xử lý từng loại hình riêng lẻ, hãy dùng subclass hoặc strategy để thêm hành vi mới.==

    Điều này giúp code ==dễ mở rộng==, ==ít bug hơn==, và dễ tích hợp các hệ thống plugin, config-driven hoặc microservice sau này.

image 2 40.png Ví dụ về Open/Closed Principle Vi phạm OCP:

class PaymentProcessor:
    def process_payment(self, payment_type, amount):
        if payment_type == "credit_card":
            print(f"Xử lý thanh toán thẻ tín dụng: {amount}₫")
        elif payment_type == "paypal":
            print(f"Xử lý thanh toán PayPal: {amount}₫")
        elif payment_type == "momo":
            print(f"Xử lý thanh toán MoMo: {amount}₫")
        else:
            print("Phương thức thanh toán không được hỗ trợ.")
  • Nếu muốn thêm loại thanh toán mới (VD: ZaloPay), phải sửa đổi method ==process_payment==.
  • Code dễ phát sinh lỗi, khó mở rộng, khó test độc lập từng phương thức. Tuân thủ OCP:
from abc import ABC, abstractmethod
# Interface - abstract class cho các phương thức thanh toán
class PaymentProcessor(ABC):
    @abstractmethod
    def process(self, amount):
        pass
# Triển khai từng loại thanh toán riêng biệt
class CreditCardProcessor(PaymentProcessor):
    def process(self, amount):
        print(f"Xử lý thanh toán thẻ tín dụng: {amount}₫")
class PayPalProcessor(PaymentProcessor):
    def process(self, amount):
        print(f"Xử lý thanh toán PayPal: {amount}₫")
# Mở rộng: thêm phương thức thanh toán MoMo mà không sửa code cũ
class MoMoProcessor(PaymentProcessor):
    def process(self, amount):
        print(f"Xử lý thanh toán MoMo: {amount}₫")
# Hàm sử dụng chung
def checkout(processor: PaymentProcessor, amount):
    processor.process(amount)
# Sử dụng
checkout(CreditCardProcessor(), 100000)
checkout(PayPalProcessor(), 200000)
checkout(MoMoProcessor(), 300000)

Ta nên tách mỗi phương thức ra một class riêng kế thừa ==PaymentProcessor==, để mở rộng mà không động vào code cũ.

5.3. Liskov Substitution Principle

Nguyên lý Liskov Substitution nhấn mạnh rằng: “Lớp con phải có thể thay thế được lớp cha mà không làm thay đổi tính đúng đắn của chương trình”. Đây là nền tảng để viết code an toàn, nhất quán và dễ mở rộng, đặc biệt trong môi trường sử dụng kế thừa hoặc interface.

  • Thay Thế Được

    Đối tượng của lớp con phải hoạt động đúng khi được sử dụng thay cho lớp cha. Hàm client_code nhận ==Parent vẫn hoạt động trơn tru nếu truyền vào Child==.

    → Ví dụ: nếu ==Bird có phương thức fly()==, thì mọi subclass của Bird cũng phải có khả năng gọi ==fly() đúng nghĩa.==

  • Hành Vi Nhất Quán

    Lớp con không được làm thay đổi hành vi đã định nghĩa ở lớp cha. Nếu lớp cha không ném ra lỗi thì lớp con cũng không được tự ý ném exception.

    → Vi phạm điều này khiến hệ thống hoạt động không thể đoán trước.

  • Tránh Vi Phạm

    Lớp con không được:

    • Thêm ==điều kiện đầu vào khắt khe hơn (====precondition==)
    • ==Giảm ràng buộc đầu ra (====postcondition==)
    • Thay đổi logic khiến client dùng class cha bị lỗi khi chuyển sang class con image 3 38.png Ví Dụ Cổ Điển: Rectangle vs Square
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def set_width(self, width):
        self.width = width
    def set_height(self, height):
        self.height = height
    def area(self):
        return self.width * self.height
class Square(Rectangle):
    def set_width(self, width):
        self.width = self.height = width
    def set_height(self, height):
        self.width = self.height = height

Khi dùng ==Square thay cho Rectangle==, việc gọi ==set_width() hay set_height() sẽ thay đổi cả hai chiều, phá vỡ logic area = width * height==, từ đó vi phạm LSP. ==Giải pháp: Không nên cho Square kế thừa Rectangle==. Hãy tách chúng thành hai thực thể độc lập hoặc dùng ==Shape base class trừu tượng hơn.== Ví dụ 2: Vi phạm LSP

class Bird:
    def fly(self):
        print("Bird can fly")
class Penguin(Bird):
    def fly(self):
        raise Exception("Penguin không thể bay!")

Hàm ==let_bird_fly(bird) sẽ hoạt động với Bird==, nhưng nếu truyền ==Penguin==, chương trình sẽ phát sinh lỗi – do hành vi của lớp con không đúng như kỳ vọng của lớp cha. Điều này khiến hệ thống dễ sập, khó bảo trì và không thể kiểm thử tốt. Tuân thủ LSP

class FlyingCreature:
    def fly(self):
        print("Flying creature can fly")
class Bird(FlyingCreature):
    pass
class Penguin:
    def swim(self):
        print("Penguin can swim")

Lúc này, ==Bird chỉ kế thừa FlyingCreature – đúng với khả năng bay. Penguin không kế thừa class bay nữa mà tách riêng hành vi của mình. Hàm let_fly() giờ đây chỉ dùng cho đối tượng thật sự có khả năng bay, đảm bảo đúng hành vi mong đợi.==

5.4. Interface Segregation Principle

Nguyên tắc phân tách interface khuyến khích chia nhỏ các interface thành các phần chuyên biệt, thay vì nhồi nhét quá nhiều phương thức vào một interface. Điều này giúp client chỉ cần quan tâm đến các chức năng thực sự cần dùng, tránh sự phụ thuộc không cần thiết và tăng khả năng tái sử dụng linh hoạt hơn. Ví dụ minh họa Vi phạm ISP – Fat Interface

class AllDeviceActions:
    def print(self): pass
    def scan(self): pass
    def fax(self): pass
    def copy(self): pass
class BasicPrinter(AllDeviceActions):
    def print(self): print("Printing...")
    def scan(self): raise NotImplementedError("Not supported")
    def fax(self): raise NotImplementedError("Not supported")

==BasicPrinter buộc phải implement các method không liên quan như scan==, ==fax==, mặc dù không cần thiết – điều này vi phạm ISP.

Tuân thủ ISP – Tách nhỏ interface

class Printer:
    def print(self): pass
class Scanner:
    def scan(self): pass
class Fax:
    def fax(self): pass
class BasicPrinter(Printer):
    def print(self): print("Printing...")
class MultiFunctionDevice(Printer, Scanner, Fax):
    def print(self): print("Printing...")
    def scan(self): print("Scanning...")
    def fax(self): print("Faxing...")

Trong ví dụ này, mỗi interface chỉ đảm nhận một nhiệm vụ chuyên biệt. ==BasicPrinter không cần quan tâm đến scan hay fax==, trong khi ==MultiFunctionDevice có thể kết hợp các chức năng tùy theo nhu cầu.== image 4 33.png Trong Python Bạn có thể dùng ABC module (abstractmethod) hoặc typing.Protocol (Python 3.8+) để định nghĩa interface rõ ràng. Duck typing cho phép các lớp chỉ triển khai những phương thức cần thiết mà không cần kế thừa chính thức. Lợi Ích Việc tuân theo ISP giúp code linh hoạt hơn với khả năng swap implementation. Dễ unit test với mock objects. Giảm coupling và side-effects khi thay đổi code, đặc biệt trong hệ thống lớn có nhiều module phụ thuộc. image 5 31.png

5.5. Dependency Inversion Principle

Nguyên lý DIP nhấn mạnh rằng:

“Modules cấp cao không nên phụ thuộc vào modules cấp thấp. Cả hai nên phụ thuộc vào abstraction.” Điều này giúp hệ thống dễ mở rộng, dễ test, giảm coupling và tăng khả năng thay thế module mà không ảnh hưởng đến logic nghiệp vụ. Mục tiêu của DIP Trong một hệ thống phần mềm:

  • High-level module (ví dụ: business logic, use case) cần tập trung vào quy tắc nghiệp vụ cốt lõi.
  • Low-level module (ví dụ: ORM, database, external API) là nơi chứa chi tiết triển khai. Thay vì để module cấp cao gọi trực tiếp database hoặc API, ta nên trích xuất abstraction (interface, protocol), để cả hai cùng phụ thuộc vào abstraction đó. Điều này cho phép hoán đổi chi tiết cài đặt mà không làm ảnh hưởng đến logic tổng thể. Ví dụ minh họa: Vi phạm DIP
class MySQLDatabase:
    def save(self, data):
        print("Lưu vào MySQL")
class UserService:
    def __init__(self):
        # Gắn chặt với MySQL
        self.database = MySQLDatabase()
    def create_user(self, user_data):
        self.database.save(user_data)
  • ==Vấn đề: UserService phụ thuộc trực tiếp vào MySQLDatabase==.
  • ==Hậu quả: Nếu muốn chuyển sang MongoDB hoặc mock DB để test, bạn phải sửa lại code của UserService==. Tuân thủ DIP với abstraction
from abc import ABC, abstractmethod
class Database(ABC):
    @abstractmethod
    def save(self, data): pass
class MySQLDatabase(Database):
    def save(self, data):
        print("Lưu vào MySQL")
class MongoDatabase(Database):
    def save(self, data):
        print("Lưu vào MongoDB")
class UserService:
    def __init__(self, database: Database):
        self.database = database  # Phụ thuộc vào abstraction
    def create_user(self, user_data):
        self.database.save(user_data)
  • ==Ưu điểm: UserService không còn gắn chặt vào chi tiết kỹ thuật.==
  • ==Linh hoạt: Có thể truyền bất kỳ implementation nào tuân theo interface Database==.
  • ==Testable: Dễ dàng thay thế bằng mock database trong unit test==

6. Các Design Patterns Phổ Biến

Design Patterns là những giải pháp thiết kế đã được kiểm chứng, giúp giải quyết các vấn đề lặp đi lặp lại trong thiết kế phần mềm. Dù bạn dùng Python, Java, hay bất kỳ ngôn ngữ nào, hiểu và áp dụng đúng design patterns sẽ giúp hệ thống của bạn linh hoạt, dễ bảo trì và mở rộng. Chúng ta có thể chia design patterns thành ba nhóm chính:

  1. Creational Patterns – Mẫu khởi tạo Tập trung vào việc tạo đối tượng một cách linh hoạt, giúp tách biệt logic khởi tạo khỏi logic sử dụng.
  2. Structural Patterns – Mẫu cấu trúc Giúp xác định mối quan hệ giữa các lớp hoặc đối tượng, từ đó cấu trúc hệ thống linh hoạt hơn.
  3. Behavioral Patterns – Mẫu hành vi Tập trung vào cách các đối tượng giao tiếp, cộng tác với nhau một cách hiệu quả và có tổ chức.

6.1. Creational Patterns: Factory & Singleton

Creational Patterns giúp bạn tách biệt quy trình khởi tạo đối tượng khỏi phần sử dụng, từ đó làm cho hệ thống linh hoạt hơn và dễ mở rộng hơn về sau. Trong số này, hai mẫu phổ biến nhất là: Factory Pattern và Singleton Pattern.

Factory Pattern

Factory Pattern cho phép tạo đối tượng mà client không cần biết lớp cụ thể nào được khởi tạo. Điều này giúp:

  • Giấu đi logic phức tạp bên trong
  • Cung cấp interface chung cho các đối tượng liên quan
  • Dễ dàng mở rộng (thêm loại mới) mà không cần sửa mã gốc (client code) Ví dụ:
class ButtonFactory:
    def create_button(self, button_type):
        if button_type == "round":
            return RoundButton()
        elif button_type == "square":
            return SquareButton()
        elif button_type == "toggle":
            return ToggleButton()
# Sử dụng
factory = ButtonFactory()
button = factory.create_button("round")
button.render()  # Không cần biết round button được triển khai thế nào

Singleton Pattern

Singleton Pattern đảm bảo chỉ có một instance duy nhất của class tồn tại trong toàn bộ chương trình. Nó đặc biệt hữu ích cho:

  • Kết nối database
  • Logging
  • Quản lý config dùng chung Ưu điểm:
  • Kiểm soát tài nguyên truy cập toàn cục
  • Giảm thiểu chi phí khởi tạo
  • Tránh xung đột khi dùng nhiều nơi trong hệ thống Ví dụ:
class DatabaseConnection:
    _instance = None
    _is_initialized = False
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    def __init__(self):
        if not self._is_initialized:
            self.host = "localhost"
            self.connect_db()
            self._is_initialized = True
    def connect_db(self):
        print("Connecting to database...")
# Dù gọi nhiều lần, chỉ một kết nối được tạo
conn1 = DatabaseConnection()
conn2 = DatabaseConnection()
assert conn1 is conn2  \#True

💡 Khi nào dùng?

  • Dùng Factory khi bạn cần tách logic tạo object ra khỏi logic sử dụng – nhất là khi object có nhiều kiểu con khác nhau.
  • Dùng Singleton khi bạn cần chỉ một thể hiện duy nhất tồn tại toàn bộ chương trình – như kết nối DB hoặc config loader.

6.2. Structural Patterns: Adapter & Decorator

Structural Patterns giúp bạn xây dựng hệ thống linh hoạt thông qua tổ chức mối quan hệ giữa các đối tượng. Hai mẫu tiêu biểu là Adapter Pattern và Decorator Pattern, đặc biệt hữu ích trong việc mở rộng hoặc tích hợp mà không làm thay đổi code gốc.

Adapter Pattern – Kết nối interface không tương thích

Adapter hoạt động như một trung gian phiên dịch, giúp 2 interface khác nhau có thể giao tiếp với nhau mà không cần chỉnh sửa mã nguồn gốc. Điều này rất phù hợp khi:

  • Tích hợp thư viện bên thứ ba không theo chuẩn của bạn
  • Cần xử lý sự khác biệt giữa các API
  • Không thể (hoặc không được phép) sửa code gốc
  • Cần đồng bộ hóa interface trong hệ thống lớn Ví dụ:
# Thư viện bên thứ 3
class ThirdPartyPrinter:
    def send(self, message):
        print(f"[ThirdParty] {message}")
# Adapter
class PrinterAdapter:
    def __init__(self, third_party_printer):
        self.printer = third_party_printer
    def print_message(self, message):
        self.printer.send(message)
# Sử dụng
printer = PrinterAdapter(ThirdPartyPrinter())
printer.print_message("Hello")  # Chuẩn hóa interface theo hệ thống

Decorator Pattern – Mở rộng hành vi mà không sửa code gốc

Decorator cho phép bạn thêm chức năng cho một đối tượng hiện có mà không cần thay đổi lớp ban đầu. Thay vì kế thừa, decorator sử dụng wrapping – bao bọc đối tượng ban đầu và mở rộng hành vi của nó. Khi nào nên dùng:

  • Muốn mở rộng chức năng linh hoạt
  • Có nhiều tổ hợp tính năng cần kết hợp
  • Tuân thủ nguyên tắc Open/Closed: mở rộng dễ, không cần chỉnh sửa lớp gốc Ví dụ:
class BaseNotifier:
    def send(self, message):
        print(f"Gửi: {message}")
# Decorator
class EmailDecorator:
    def __init__(self, notifier):
        self.notifier = notifier
    def send(self, message):
        self.notifier.send(message)
        print(f"[Email] {message}")
class SMSDecorator:
    def __init__(self, notifier):
        self.notifier = notifier
    def send(self, message):
        self.notifier.send(message)
        print(f"[SMS] {message}")
# Sử dụng kết hợp decorator
notifier = SMSDecorator(EmailDecorator(BaseNotifier()))
notifier.send("Chào bạn!")  # Gửi qua Base → Email → SMS

6.3. Behavioral Patterns: Command & Template Method

Behavioral Patterns tập trung vào cách các đối tượng tương tác và chia sẻ trách nhiệm hành vi:

Command Pattern

Command Pattern cho phép bạn đóng gói một yêu cầu (hành vi) thành một object, qua đó:

  • Tách biệt người gọi (Invoker)người thực thi (Receiver) ⇒ tăng tính mô-đun
  • Dễ lưu trữ lịch sử hành động để thực hiện undo/redo, log, queue hoặc replay
  • Tuân thủ Open/Closed: dễ mở rộng các hành động mới mà không sửa mã cũ
  • Ứng dụng phổ biến: GUI (nút bấm, shortcut), giao dịch ngân hàng, hàng đợi xử lý Ví dụ:
# Command interface
class Command:
    def execute(self):
        pass
# Concrete commands
class TurnOnLight(Command):
    def execute(self):
        print("💡 Light turned ON")
class TurnOffLight(Command):
    def execute(self):
        print("💤 Light turned OFF")
# Invoker
class RemoteControl:
    def __init__(self):
        self.history = []
    def submit(self, command):
        command.execute()
        self.history.append(command)
# Usage
remote = RemoteControl()
remote.submit(TurnOnLight())
remote.submit(TurnOffLight())

Template Method Pattern

Template Method Pattern cung cấp một thuật toán bộ khung trong lớp cha, với các bước cụ thể được tùy biến trong lớp con. Nhờ đó:

  • Tái sử dụng logic chung
  • Hạn chế lỗi do override cấu trúc lớn
  • Giảm duplication, tuân thủ nguyên lý DRY
  • Hữu ích khi cần kiểm soát flow logic chuẩn nhưng linh hoạt về chi tiết từng bước Khi nào nên dùng?
  • Có quy trình gồm nhiều bước (ETL, phân tích dữ liệu, xử lý request…)
  • Mỗi bước cần tùy chỉnh tùy loại đối tượng, nhưng flow tổng thể thì không thay đổi Ví dụ minh họa:
class DataProcessor:
    def process(self):
        self.read_data()
        self.transform_data()
        self.save_data()
    def read_data(self):
        raise NotImplementedError
    def transform_data(self):
        raise NotImplementedError
    def save_data(self):
        print("💾 Data saved.")
class CSVProcessor(DataProcessor):
    def read_data(self):
        print("📄 Reading CSV file...")
    def transform_data(self):
        print("🔁 Cleaning data...")
# Usage
job = CSVProcessor()
job.process()

6.4. Cách dùng Design Patterns hiệu quả

Design Patterns là công cụ mạnh mẽ nhưng không phải viên đạn bạc. Lạm dụng hoặc dùng sai ngữ cảnh dễ khiến code trở nên phức tạp, khó bảo trì. Hãy nhớ: “Design Pattern tốt là pattern không được nhận ra.” Dưới đây là hướng dẫn thực chiến: khi nào nên áp dụng, khi nào cần tránh, và khi nào phải cân nhắc trade-off. ==Khi nào nên áp dụng==

Tình huốngDesign Pattern
Hệ thống plugin, module mở rộng==Factory Pattern==
Quản lý kết nối DB dùng chung==Singleton Pattern==
Tích hợp thư viện không tương thích (VD: chuyển JSON → XML)==Adapter Pattern==
Muốn trích xuất giao diện giao tiếp với DB để dễ test==Dependency Inversion==
==Khi nào không nên áp dụng==
------
Trường hợpLý do
Dự án nhỏ, script đơn giản (VD: pandas script)Không cần abstraction, chỉ làm code nặng nề
==Singleton==: dùng trong unit testGây khó khăn khi mock & kiểm soát trạng thái
==Decorator==: dùng quá nhiềuTạo ra wrapper hell – khó đọc, khó debug
==Khi Nào Phải Cân Nhắc Trade-offs==
------
PatternTrade-off
==Dependency Inversion==Rất tốt cho testing/API, nhưng cần thiết kế nhiều interface
==Template Method==Code pipeline dễ tái sử dụng, nhưng người mới khó theo dõi flow
==Command Pattern==Hỗ trợ undo/redo tốt, nhưng cần nhiều lớp + object liên quan
==Tránh Lạm Dụng==
------
Sai lầm thường gặpHạn chế
Dùng ==Factory Method k==hi chỉ có 2 loại đối tượngOverkill – hard code đôi khi đơn giản hơn
==Singleton không x==ử lý đồng bộ (race condition)Nguy cơ bug ngầm trong ứng dụng đa luồng
Dùng ==Command Pattern thay vì lam==bda đơn giảnNếu chỉ cần gọi hàm ẩn danh, lambda đủ rồi
==Gợi ý cuối: Pattern là công cụ – không phải mục tiêu.==
  • Đừng pattern hóa mọi thứ.
  • Ưu tiên clarity trước abstraction.
  • Chỉ dùng khi nó giải quyết đúng vấn đề hiện tại hoặc trong tương lai gần.