Django views: Miscellaneous thinking

Posted in Programming on March 23, 2017 by manhhomienbienthuy Comments
Django views: Miscellaneous thinking

Django là một framework rất nổi tiếng, mạnh mẽ với rất nhiều tính năng được viết trên ngôn ngữ Python. Có nhiều lý do khiến nó trở thanh framework phổ biến như vậy. Trong bài viết này, tôi sẽ trình bày một vài ý kiến cá nhân với phần Views của nó.

Trước khi chúng ta bắt đầu, bạn nên tìm hiểu qua một chút về framework này ở đây. Một chút hiểu biết nhất định về Python cũng giúp bạn hiểu rõ hơn vấn đề.

Django sử dụng mô hình MVT (model-view-template)

Django được nhiều tài liệu, đặc biệt là Django Book giới thiệu rằng nó sử dụng mô hình MVC (model-view-controller). Mô hình này vô cùng quen thuộc rồi, có lẽ tôi không cần nói gì thêm về nó. Một hình ảnh minh họa cho mô hình MVC như sau:

mvc

Thế nhưng khi làm việc với Django, nhất là khi tôi đã làm việc với Ruby on Rails rồi thì MVC của Django quả thực rất khác những gì tôi nghĩ. Và mô hình chính xác mà Django triển khai được gọi là MVT (model-view-template). Mô hình này như sau:

mvt

Phải chăng Django đã gọi sai tên, họ gọi controller là view và gọi view là template. Theo lý giải của Django thì mọi thứ thú vị hơn rất nhiều.

Django gọi mô hình mà họ sử dụng là MVT bởi vì bản thân framework chính là controller (mà phần này thì trong suốt với người dùng). Lý do rất đơn giản, bởi vì framework chính là thứ dùng để điều khiển toàn bộ framework, bao gồm cả model, template, view để xử lý truy vấn và trả kết quả cho người dùng.

Chúng ta biết rằng, MVC là một mô hình chung cho toàn bộ tất cả các phần mềm chứ không phải là mô hình riêng cho các ứng dụng Web. Việc mỗi người có một kiến giải khác nhau cũng như cách triển khai khác nhau cho mô hình này cũng là điều dễ hiểu. Qua đây, chúng ta có thêm cơ hội hiển thêm về mô hình MVC tưởng như rất quen thuộc này.

Theo cách triển khai của Django, view sẽ thể hiện dữ liệu trả về cho người dùng, nó không chỉ là cách hiển thị dữ liệu như thế nào, mà còn là những dữ liệu nào được hiển thị nữa.

Ngược lại, Ruby on Rails và nhiều framework khác giao nhiệm vụ cho controller sẽ lấy dữ liệu trả về cho người dùng, trong khi view chỉ còn đơn giản là hiển thị chúng.

Mô hình MVC đã được phát triển trong thời gian rất dài, và nó càng phát triển mạnh hơn nữa trong thời đại Internet, khi mà các ứng dụng Web ngày càng phổ biến. Internet chính là một nơi lý tưởng để triển khai mô hình client-server. Phần lớn các framework web đều được xây dựng từ mô hình MVC. Và tôi mạnh dạn dự đoán rằng, nếu bạn đang có ý định không sử dụng MVC cho ứng dụng Web của mình, khả năng cao nhất là bạn đã sai ngay từ ý tưởng.

Về mặt khái niệm, mô hình MVC rất đơn giản:

  • Model (M) là mô phỏng của dữ liệu. Nó không thực sự là dữ liệu, nhưng nó là một thể hiện của dữ liệu và là nơi để chúng ta thao tác với dữ liệu thật sự. Model cho phép chúng ta lưu dữ liệu vào DB và không cần hiểu những hoạt động sâu xa bên dưới. Hơn nữa, model cung cấp cho chúng ta cách thức thao tác với DB rất đơn giản, khiến cho một model có thể sử dụng với rất nhiều DB khác nhau.
  • View (V) là những gì người dùng nhìn thấy. Nó là sự thể hiện của dữ liệu đối với người dùng. Nói một cách văn hoa, nó là sự thể hiện của Model. Trong ứng dụng Web, nó chính là những gì người dùng nhìn thấy trên trình duyệt. Với ứng dụng khác, nó là những gì họ nhìn thấy trên giao diện của ứng dụng. Ngoài ra, View còn cung cấp cho chúng ta phương thức để thu thập dữ liệu từ người dùng.
  • Controller (C) dùng để điều khiển luồng thông tin dữ Model và View. Nó được sử dụng để cài đặt các login về việc lấy dữ liệu từ DB thông qua Model và chuyển sang View. Nó cũng là nơi xử lý những truy vấn từ người dùng thông qua View và thực hiện các logic khác: thay đổi View, cập nhật dữ liệu thông qua Model.

Bởi vì mô hình là một khái niệm, nó có thể được triển khai rất khác nhau giữa các framework khác nhau. Những "Guru" của framework đó mới là người biết chính xác họ đang triển khai như thế nào, và tính năng nào thuộc về Controller, tính năng nào thuộc về View.

Thực ra với những người lập trình ứng dụng như chúng ta, chúng ta chỉ sử dụng framework thì việc hiểu biết kỹ càng về cách triển khai của framework nhiều khi không quan trọng lắm. Miễn là chúng ta vẫn hiểu được framework đó, chúng ta sẽ có thể hoàn thành được công việc của mình. Tất nhiên là nếu có hiểu biết thì vẫn hơn, nhất là khi phỏng vấn ứng tuyển.

Quay trở lại với Django, nó hoàn toàn tuân theo mô hình MVC mặc dù cách triển khai của nó hơi "dị". Bởi vì "C" chính là bản thân framework, và phần lớn các lập trình viên chỉ làm việc với Model, Template và View, nên Django thường được hiểu là sử dụng mô hình MVT. Trong mô hình này:

  • Model (M) là lớp để truy cập dữ liệu. Đây là nơi chứa mọi thứ liên quan đến dữ liệu: cách thức truy cập DB, validate dữ liệu, các phương thức và hành vi của dữ liệu, mối quan hệ của dữ liệu.
  • Template (T) là lớp hiển thị. Đây là nơi chứa đựng những gì liên quan đến việc hiển thị dữ liệu cho người dùng: dữ liệu được hiển thị trên Web hay dạng thức nào khác.
  • View (V) là nơi chứa các logic. Lớp này chứa các logic để truy cập dữ liệu qua Model và truyền nó ra ngoài cho Template tương ứng. Nó có thể coi là một cầu nối giữ Model và Template.

Mọi sự khó hiểu cũng diễn ra ở đây khi View của Django đang đảm nhận vai trò tương tự Controller trong MVC khi mà nó có khá nhiều logic. Sự khó hiểu nhẹ này chỉ là thoáng qua khi chúng ta đã quen với những framework khác. Rồi mọi chuyện sẽ trở nên bình thường khi chúng ta lại quen với Django

View: class-based vs function-based, lợi và hại

Tôi biết đến Django khá muộn. Không rõ lịch sử thế nào, nhưng khi tôi biết đến nó thì nó đã có cả class-based View và function-based View rồi. Function-based View là thứ tôi biết đến trước, còn class-based View thì phải mãi sau này mới biết.

Khi mới biết đến function-based View, nó là một thứ rất đơn giản. Mọi thứ đều được thể hiện ở code. Tôi cần gì, tôi code thứ đó. Nhưng dần dần, khi bài toán trở nên phức tạp hơn, những tính năng tương tự nhau xuất hiện nhiều hơn, function-based View lại lộ ra khuyết điểm của nó. Nó không thể mở rộng cũng như dùng lại được. Mỗi View tôi phải code một hàm, mặc dù nhiều phần cũng khá tương đồng.

Có thể tôi không phải là một lập trình viên đủ giỏi để có thể tái sử dụng các hàm. Nhưng rồi, class-based View đã giúp tôi.

Class-based view

Như đã nói ở trên, class-based View được sinh ra để giúp lập trình viên dễ dàng mở rộng và tái sử dụng code. Lợi ích lớn nhất của class-based View là nó có thể kế thừa. Từ một View này, dễ dàng chúng ta xây dựng View khác chỉ với một vài dòng code.

Hãy xem xét một ví dụ như sau:

# urls.py
urlpatterns = [
    url(r'^user/(?P<pk>\d+)/$', UserDetailView.as_view(), name="user_view"),
]

# views.py
class UserDetailView(DetailView):
    model = User

    def get_context_data(self):
        context = super().get_context_data()
        context['posts'] = self.object.posts.all()
        return context

Vậy là chúng ta đã có thể lấy ra được dữ liệu và chuyển cho Template để hiển thị cho người dùng.

Như code ở trên, View của chúng ta đã sử dụng kế thừa. Thay vì code lại toàn bộ View, tôi chỉ cần kế thừa nó từ một View khác và thêm một vài chỉnh sửa là đủ.

Ngoài ra, Django đã xây dựng sẵn rất nhiều View class cho những tác vụ thông thường của một ứng dụng Web, ví dụ như thêm, sửa, xóa một đối tượng, list các đối tượng, phân trang, v.v...

Sử dụng những class View này sẽ giúp chúng ta tiết kiệm rất nhiều thời gian và công sức để xây dựng View cho ứng dụng.

Bạn có thể tham khảo thêm trang ccbv.co.uk, nó cung cấp thông tin rất đầy đủ về các class View của Django. Một điều nữa là trang này cũng sử dụng class-based View để xây dựng.

Tiếp tục với class-based View, nếu bạn để ý, bạn có thể thấy rằng, thực ra nó cũng là function khi được gọi. Khi chúng ta thêm vào URL, chúng ta sử dụng phương thức as_view(), nó sẽ trả về một function. Nội dung của hàm as_view như sau:

class View(object):
    ...

    @classonlymethod
    def as_view(cls, **initkwargs):
        """
        Main entry point for a request-response process.
        """
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())
        return view
    ...

Nội dung của file này khá dài, bạn có thể xem bản đầy đủ ở Github.

Hàm view được trả về bởi as_view sẽ là phần được sử dụng của mọi class-based View. Khi được gọi, công việc của nó là sử dụng dispatch để xử lý truy vấn từ người dùng và gọi đến các hàm xử lý tương ứng. Ví dụ khi truy vấn bằng GET thì sẽ gọi phương thức get, truy vấn POST sẽ gọi phương thức post, v.v...

Ưu điểm:

  • Dễ dàng mở rộng, tái sử dụng code (nhờ khả năng kế thừa tuyệt vời)
  • Có thể lập trình hướng đối tượng dễ dàng (View kế thừa nhiều class)
  • Xử lý truy vấn HTTP riêng ứng với từng phương thức GET, POST, v.v...
  • Django xây dựng sẵn nhiều class cho các tác vụ thông thường, thời gian code sẽ giảm đi đáng kể

Nhược điểm:

  • Rất khó để đọc code, vì việc kế thừa khiến các class View chỉ còn rất ít code.
  • Luồng dữ liệu không tường minh, bởi sử dụng các class kiểu kế thừa khiến nhiều xử lý không được thể hiện trên code.
  • Những class được xây dựng sẵn là trong suốt với người dùng, rất khó debug.
  • Các xử lý phức tạp cần ghi đè phương thức, hoặc sử dụng các decorators (yêu cầu import từ bên ngoài)

Function-based View

Ngược lại với class-based View, function-based View dễ hiểu hơn rất nhiều. Tất cả những gì chúng ta muốn, chúng ta cần code ra. Việc xử lý các truy vấn với phương thức khác nhau có thể thực hiện như sau:

# urls.py
urlpatterns = [
    url(r'^user/(?P<pk>\d+)/$', view.user_view, name="user_view"),
]

# views.py
def user_view(request):
    if request.method == 'POST':
        # Code cho POST request
    else:
        # Code cho GET request
    # Có thể sẽ cần thêm code cho các phương thức khác như PATCH, DELETE

Function-based View không cho bạn lựa chọn nào khác ngoài việc phải viết một hàm (có thể gọi hàm khác) để xử lý các truy vấn. Lúc đầu tôi rất thích việc này, nó hoàn toàn không bị gò bó. Mọi xử lý tôi cần đều theo đúng ý mình. Thế nhưng việc phải viết một hàm để hiển thị form, sau đó validate dữ liệu người dùng nhập vào rồi lưu dữ liệu đó vào DB quả thực là rất khổ. Việc sử dụng class-based View kèm với một class Form sẽ giúp chúng ta rất nhiều thao tác.

Thế nhưng, một số trường hợp như sau, function-based View lại là một lựa chọn dễ dàng hơn class-based View

  • Cần thao tác với nhiều model một lúc. Nhiều khi chúng ta cần thao tác với hai hoặc nhiều model hơn trong cùng 1 trang. Rõ ràng những class Django thiết kế không phải cho trường hợp này. Cố sử dụng class-based View với các loại mixin khiến chúng ta mất nhiều thời gian để tìm cách xử lý.
  • Cùng 1 View nhưng phải xử lý nhiều thao một lúc. Đây là một điều rất dễ gặp với các ứng dụng Web do cùng một trang cần thể hiện nhiều thông tin cùng với các nút bấm nhanh cho người dùng dễ thao tác.
  • Edit tại chỗ. Ở đây thường là sẽ phải sử dụng một số kỹ thuật như AJAX để gửi thông tin người dùng nhập vào. Nó không phải là một View với form và các input rõ ràng để submit theo cách thông thường. Đây là một trường hợp khó và các class View được Django xây dựng sẵn chưa tính đến.

Như vậy, function-based View cũng có những lợi ích nhất định và do đó, cả class-based View và function-based View đều đang tồn tại song song.

Ưu điểm:

  • Dễ dàng cài đặt, cần gì code nấy
  • Code dễ dọc, do mọi thứ đều thể hiện trong 1 function
  • Luồng dữ liệu tường minh
  • Sử dụng decorators rất đơn giản

Nhược điểm:

  • Khó mở rộng và tái sử dụng code
  • Tự phải biết tay xử lý riêng cho các phương thức HTTP khác nhau

Rõ ràng là, cả class-based View và function-based View đều có những điểm lợi và hại riêng. Chẳng có cái nào tốt hơn cái nào, chỉ có cái nào phù hợp với bài toán hơn mà thôi. Ví dụ, nếu bạn cần những thao tác "tiêu chuẩn" với một ứng dụng Web như index, view, edit thì class-based View chính là thứ bạn nên lựa chọn. Nhưng nếu bạn cần những thao tác xử lý phức tạp với rất nhiều loại dữ liệu khác nhau, function-based view mới là lựa chọn hợp lý.

Kết luận

Django là một framework tuyệt vời. Nó cho phép chúng ta sử dụng những công cụ có sẵn cũng như tự viết những gì mình muốn. Điều cuối cùng tôi muốn nói là, bạn hãy tìm hiểu về các class View mà Django đã xây dựng sẵn mà sử dụng chúng trong ứng dụng của bạn nếu có thể. Khi bạn đang làm một thao tác xử lý bình thường, hãy sử dụng class-based View, nếu bạn cảm thấy khó khăn khi không biết phải chọn class nào để kế thừa, đó là lúc bạn nên nghĩ nên function-based View.

I apologise for any typos. If you notice a problem, please let me know.

Thank you all for your attention.