0
0
Lập trình
Admin Team
Admin Teamtechmely

Giải Quyết Vấn Đề Mô Hình Khổng Lồ Trong Django

Đăng vào 4 ngày trước

• 9 phút đọc

Vấn Đề Lớn Nhất: Mô Hình Khổng Lồ Trong Django

Trong mỗi dự án Django, có một vấn đề lớn mà mọi người đều gặp phải: tệp models.py khổng lồ. Ai cũng biết đến nó. Ban đầu, nó rất đơn giản, phản ánh một cách đẹp đẽ sơ đồ cơ sở dữ liệu của bạn. Nhưng giờ đây, nó đã trở thành một con quái vật dài 2.000 dòng, bị thổi phồng bởi các quản lý tùy chỉnh, các bộ trang trí @property ẩn chứa các truy vấn khổng lồ, và các phương thức save() dường như có nhiều tác dụng phụ hơn cả một sản phẩm quảng cáo trên truyền hình vào ban đêm.

Các view của bạn trở nên lộn xộn, logic kinh doanh bị phân tán, và việc thay đổi bất kỳ điều gì cũng giống như phẫu thuật bằng một con dao bơ. Trong lúc tuyệt vọng, một giải pháp có vẻ thanh lịch hiện ra: "selectors." Chỉ cần đưa tất cả các truy vấn rắc rối đó ra một tệp selectors.py riêng biệt! Cảm giác thật sạch sẽ. Cảm giác thật đúng đắn.

Nhưng tôi ở đây để nói với bạn rằng selectors là một cạm bẫy. Chúng là một giải pháp tạm thời mà cảm giác như là tiến bộ nhưng cuối cùng dẫn bạn đến một con đường đau khổ về kiến trúc. Có một cách tốt hơn, một cách tiếp cận có cấu trúc hơn sẽ cứu bạn khỏi những cơn đau đầu trong tương lai: các mẫu Repository và Service. Hãy cùng phân tích tại sao sự kết hợp này lại thắng lợi, và tại sao selectors không thể đứng vững trong dài hạn.


Cách Chúng Ta Đến Được Đây

Không ai bắt đầu với mục đích viết mã lộn xộn. Chúng ta thường đến đây bằng cách làm theo những lời khuyên tốt đẹp. Trong thế giới Django, câu thần chú cổ điển là "mô hình béo, view mỏng." Ý tưởng là giữ cho các view gọn nhẹ và đẩy tất cả logic liên quan đến dữ liệu vào mô hình tương ứng.

Đối với một dự án nhỏ, điều này hoạt động tuyệt vời! Một mô hình User với một phương thức như is_profile_complete() là hoàn toàn hợp lý. Nhưng điều gì sẽ xảy ra khi logic trở nên phức tạp hơn? Ngay lập tức, bạn có user.get_recent_orders(), user.calculate_lifetime_value(), và user.send_password_reset_email(). Mô hình của bạn không còn chỉ là một cấu trúc dữ liệu; nó trở thành một mạng lưới rối rắm của các quy tắc kinh doanh, truy vấn cơ sở dữ liệu, và các tương tác bên ngoài.

Đây là lúc các nhà phát triển, một cách hợp lý, tìm kiếm một cách để dọn dẹp. Tệp selectors.py ra đời. Bạn tạo ra các hàm như user_get_active_with_recent_orders()product_get_top_sellers(). Mô hình trở nên gọn gàng hơn, view gọi một hàm sạch—vấn đề được giải quyết, phải không?

Không hoàn toàn. Thành thật mà nói, đây giống như việc quét một đống bụi dưới thảm. Căn phòng trông sạch hơn một chút, nhưng bạn thực sự chưa xử lý vấn đề. Bạn chỉ mới chuyển nó đi. Sự tách biệt này là nông cạn, và như chúng ta sẽ thấy, nó tạo ra một loạt các vấn đề mới.


Mẫu Repository 101: Bảo Vệ Dữ Liệu Của Bạn

Vậy, giải pháp thay thế là gì? Hãy bắt đầu với mảnh ghép đầu tiên: Mẫu Repository.

Nói một cách đơn giản, một repository là một lớp trừu tượng ngồi giữa logic kinh doanh của ứng dụng và cơ sở dữ liệu của bạn. Công việc duy nhất của nó là xử lý truy cập dữ liệu. Hãy tưởng tượng nó như một đại lý chuyên dụng cho một mô hình cụ thể. Nếu bạn cần lấy người dùng, lưu một người dùng, hoặc xóa một người dùng, bạn sẽ nói chuyện với UserRepository. Bạn thiết kế nó để phục vụ các trường hợp sử dụng của bạn, không phải để phản chiếu mọi truy vấn có thể có.

Cấu trúc:

Copy
your_app/
  users/
    models.py
    repositories/
      __init__.py
      users.py
    services/
      __init__.py
      users.py

Dưới đây là cách mà UserRepository có thể trông như thế nào, kế thừa từ BaseRepository (như cái ở đây):

Copy
# your_app/users/repositories/users.py

from your_app.apps.common.repositories import BaseRepository 
from your_app.users.models import User
from typing import Optional


class UserRepository(BaseRepository):
    def get_by_id(self, user_id: int) -> Optional[User]:
        return self.filter(id=user_id).first()

    def get_by_email(self, email: str) -> Optional[User]:
        return self.filter(email=email).first()

Tại sao điều này quan trọng

  • Hợp đồng rõ ràng. Nó cung cấp một giao diện rõ ràng, cụ thể cho các thao tác dữ liệu. Không còn User.objects.filter(...) rải rác khắp 20 tệp khác nhau. Bạn tập trung logic truy vấn của mình ở một nơi.
  • Ý định truy vấn tập trung. Nếu một luồng cần prefetch hoặc annotations đặc biệt, bạn thêm một phương thức và giữ nó nhất quán.
  • Test seams. Bạn có thể truyền một repository giả trong các bài kiểm tra. Bạn có thể thêm caching hoặc logging ở một nơi mà không cần phải tìm kiếm qua các view.

Lớp dịch vụ, còn được gọi là bộ não

Nếu các repository là bảo vệ dữ liệu của bạn, thì các dịch vụ là bộ não của hoạt động. Các dịch vụ ngồi trên các repository để tổ chức các quy trình kinh doanh và thực thi các quy tắc. Đây là nơi mà các giao dịch nên bắt đầu và kết thúc. Nếu một quy tắc kinh doanh thay đổi—như "người dùng cao cấp được xử lý ưu tiên"—bạn muốn có một, và chỉ một, nơi để thực hiện thay đổi đó.

Phép so sánh tốt nhất là: Các Repository lấy nguyên liệu. Các dịch vụ theo công thức để nấu món ăn.

Một dịch vụ không biết làm thế nào nguyên liệu được lấy (đó là công việc của repository), và view không biết làm thế nào món ăn được nấu. View chỉ nói, "Tôi muốn đặt hàng 'Đăng Ký Người Dùng Mới'."

Copy
# users/services/users.py

from .repositories import UserRepository
# Giả sử một NotificationService tồn tại ở nơi khác
from notifications.services import NotificationService

class UserService:
    def __init__(self, user_repo: UserRepository, notification_service: NotificationService):
        self.user_repo = user_repo
        self.notification_service = notification_service

    def register_user(self, email: str, name: str) -> User:
        # Quy tắc kinh doanh: kiểm tra xem người dùng đã tồn tại chưa
        if self.user_repo.get_by_email(email):
            raise ValueError("Người dùng với email này đã tồn tại.")

        # Bước 1: Tạo người dùng thông qua repository
        user = self.user_repo.create(email=email, name=name)

        # Bước 2: Tổ chức một hành động khác
        self.notification_service.send_welcome_email(user.email)

        return user

Đây là một quy trình sạch sẽ, giao dịch và dễ theo dõi.


Rủi Ro Thực Sự Của Selectors: Một Dốc Trơn

Vậy nếu các dịch vụ là bộ não, tại sao không chỉ sử dụng selectors cho dữ liệu? Rủi ro không phải là selectors vốn đã xấu, mà là chúng là một dốc trơn.

Chúng thiếu một ranh giới kiến trúc mạnh mẽ, khiến việc thêm "chỉ một mảnh nhỏ" logic kinh doanh trở nên quá dễ dàng. Theo thời gian, những thỏa hiệp nhỏ này biến tệp truy vấn sạch sẽ của bạn thành một lớp logic kinh doanh thứ cấp rối rắm.

Mẫu Repository và Service ngăn chặn điều này bằng cách tạo ra các rào cản rõ ràng.

  • Repositories có một công việc: lấy dữ liệu.
  • Services có một công việc: thực thi các quy tắc kinh doanh.

Sự tách biệt rõ ràng này khiến con đường đúng trở nên dễ dàng và ngăn chặn sự suy thoái chậm của kiến trúc của bạn.


Các hành động của quản trị viên, tác vụ celery và những thứ khác

Giữ cùng một mẫu ở khắp mọi nơi. Các hành động quản trị viên gọi các dịch vụ. Các tác vụ celery gọi các dịch vụ. Các lệnh quản lý gọi các dịch vụ. Một luồng. Một nơi cho tính idempotency và retries. Nếu một tác vụ chạy hai lần, dịch vụ kiểm tra trạng thái và bỏ qua hoặc tiếp tục.


Câu hỏi thường gặp trong nhóm

  • Tôi có thể giữ một trợ giúp nhỏ trên mô hình không? Có, nếu nó chỉ chạm vào trạng thái của mô hình đó.
  • Tôi có luôn cần một dịch vụ không? Nếu một view chỉ đọc thẳng, repo đến serializer có thể là đủ. Khi bạn phân nhánh hoặc thay đổi trạng thái, hãy tìm đến một dịch vụ.
  • Tôi có cần một repo cho mỗi mô hình không? Bắt đầu từ nơi nó giúp ích nhất. Những tập hợp lớn, truy vấn nặng, đường dẫn nóng. Hãy để lớp phát triển theo nhu cầu.
  • Các selectors có thể sống bên cạnh repos không? Nếu chúng tồn tại, hãy làm cho chúng là những lớp mỏng bao quanh repos, không phải là một nguồn sự thật riêng biệt.

Tại sao điều này quan trọng với các nhóm, không chỉ trong mã

Các lớp sạch sẽ giúp việc giao tiếp dễ dàng hơn. Một PM nói, thanh toán không thành công khi thử lại. Bạn chỉ cần nhìn vào một nơi. Một người mới hỏi, tôi nên đặt logic này ở đâu. Bạn có một câu trả lời sẵn sàng. Một người xem xét một PR và biết rõ ràng các mẫu nào cần mong đợi. Hình dạng của hệ thống vẫn ổn định khi mọi người thay đổi.

Ngoài ra, các repository hoạt động như những nơi riêng biệt để dạy hiệu suất. Bạn có thể thêm những bình luận nhỏ, như tại sao bạn đã sử dụng select_for_update ở đây, hoặc tại sao một chỉ mục lại quan trọng. Các đồng nghiệp mới sẽ học hỏi bằng cách thấy cùng một mẫu trong nhiều phương thức. Điều đó nhẹ nhàng hơn nhiều so với việc gửi một tài liệu wiki dài mà không ai đọc hai lần.


Kết Luận: Chọn Sự Rõ Ràng Thay Vì Tiện Lợi

Hãy nhìn, selectors không được sinh ra từ ác ý. Chúng đến từ một nơi tốt đẹp: mong muốn dọn dẹp mã lộn xộn. Chúng mang lại một khoảnh khắc giải tỏa, một cảm giác tổ chức. Nhưng đó chỉ là một giải pháp tạm thời. Chúng là một cái nạng, không phải là một chiến lược kiến trúc lâu dài.

Các mẫu Repository và Service đòi hỏi một chút kỷ luật hơn ở giai đoạn đầu. Bạn phải suy nghĩ về các lớp của ứng dụng và thực thi những ranh giới đó. Hãy giữ cho các mô hình gọn gàng. Đặt truy cập dữ liệu trong các repository. Đặt các quy tắc trong các dịch vụ. Làm chủ giao dịch ở lớp dịch vụ. Hãy nhất quán.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào