Con đường đến với Clean Architecture trong thế giới Frontend
Image for post
Photo by William Bout on Unsplash
Chúng ta yêu thích UI phải không ? Ý mình là, phát triển giao diện người dùng. Rốt cuộc, nhiều người trong chúng tam những người phải xử lý JS và các thư viện như React và Vue là những nhà phát triển giao diện người dùng. Giao diện người dùng cần đẹp, được tư duy tốt mang lại trải nghiệm người dùng tuyệt vời là điều khiến chúng ta tự hòa về công việc của mình.
Mình suy đoán rằng đây là một trong những lý do chính khiến nhiều nhà phát triển giao diện người dùng mắc vào bẫy của các thư viện tiêu chuẩn như React hoặc Vue. Rất dễ rơi vào quan niệm sai lầm về ưu thế của thư viện cho việc phát triển UI.
Nhiều năm trước, trước khi các thư viện component chiếm vị trí hàng đầu trong thế giới frontend, việc sử dụng thuộc tính "data-" để lưu trữ dữ liệu business quan trọng trong các phần tử DOM là một sai lầm phổ biến. Thật dễ dàng để đi theo con dốc trơn trượt ấy. Rốt cuộc, một thuộc tính nằm ngay trong phần tử và thật đơn giản để đặt id của sản phẩm hoặc thậm trí mật khẩu của người dùng chỉ bằng cách gọi $().data(). Đơn giản nhưng để lại sai lầm lớn.
Thuộc tính Dataset không bao giờ có nghĩa là lưu trữ dữ liệu business. Chúng là một phần của API DOM. Và DOM luôn là một phần quy mô của ứng dụng được gọi là "view" hoặc "presentation", "UI". Nó không liên quan gì đến logic nghiệp vụ của ứng dụng của bạn. Nó chỉ tồn tại để trình bày kết quả của logic đã được xử lý cho người dùng thấy và thu thập phản hồi từ họ.

Image for post
React picture - wikipedia.org
React và Vue không khác nhau là mấy. Chúng tồn tại để giúp bạn xây dựng giao diện người dùng (hoặc gây khó khăn tùy vào cách bạn dùng chúng). Không hơn không kém. Nhưng UI không quá quan trọng.
Đúng vậy, nó có thể đẹp về mặt thẩm mỹ; nó có thể bán được nhiều sản phẩm của bạn. Nhưng giao diện người dùng tồi có thể nhanh chóng giết chết toàn bộ công ty của bạn. Tất cả đều đúng khi nhìn từ quan điểm marketing, thẩm mỹ và bán hàng.
Nhưng từ quan điểm kiến trúc phần mềm, giao diện người dùng chỉ là thứ yếu. Mặt khác, dữ liệu và logic là điều cần thiết. Chúng xác định tính độc đáo và là phần có giá trị nhất của phần mềm.
Mình đoán, với tư cách là một chuyên gia, bạn muốn bảo vệ phần giá trị nhất trong chương trình của mình càng nhiều càng tốt. Có nhiều cách bạn có thể làm như vậy. Trước tiên, bạn có thể bảo vệ nó khỏi các lỗi của mình. Tất nhiên, một trong những cách tốt nhất để làm như vậy là thử nghiệm. Không có gì cho chúng ta hy vọng vào ngày mai tốt đẹp hơn 100% test coverage.
Thứ hai, bạn có thể bảo vệ trung tâm của ứng dụng khỏi các lỗi khác. Là một nhà phát triển chuyên nghiệp, bạn có thể nghĩ đến mười lần trước khi cho phép một thư viện v0.0.1 từ một bên thứ ba không xác định được nhúng vào Business rule của bạn. Sau cùng, nếu chúng ta muốn khỏe mạnh thì hãy tránh xa các đồ ăn vặt cũng như việc tránh nhúng các thư viện bên thứ ba vào hệ thống mà chúng ta không chắc chắn cũng như kiểm soát được nó.
Thứ ba, và mình sẽ giữ sự chú ý của bạn về vấn đề này trong một khoảng thời gian, chúng ta nên cố gắng bảo vệ lõi của ứng dụng có giá trị của mình khỏi những thay đổi không hợp lý. Bob Martin luôn nói sự thật đơn giản nhưng quan trọng này trong nhiều năm khi thổi vào trong những cuốn sách, bài báo, hội nghị và buổi gặp mặt. Tuy nhiên, chúng ta vẫn tiếp tục bỏ qua lời khuyên tuyệt vời của ông ấy. Không tin mình ? Hãy thử nhớ lại bao nhiêu lần bạn đã nhìn thấy thứ gì đó như thế này trong mã nguồn:

Hoặc

Mình đã thấy rất nhiều. Và nhiều người trong số chúng chính do mình viết ra. Và rất dễ rơi vào cái bẫy này vì chúng ta đã được thông báo rằng các component của chúng ta là smart. Tất nhiên, không phải tất cả chúng, cũng có các component ở dạng "dumb", "functional", "stateless", "presentational". Chúng khác biệt so với smart bởi chỉ đơn giản chúng là UI đảm bảo về mặt hiển thị và không hề chứa bất kì business logic nào bên trong.
Nhưng có một nhóm các smart component hay "stateful component". Để tách chúng ra khỏi các "UI component", chúng ta thậm chí còn gọi chúng là các "container". Có thể dễ dàng đi đến kết luận, "oh vậy có nghĩa là chúng ta nên đặt business logic vào trong các smart component này !". Nhưng bây giờ bạn có ít nhất 2 vấn đề.
Đầu tiên, business logic của bạn hiện được kết hợp với UI. Chúng ta có thể gọi các smart component là bất cứ thứ gì chúng ta muốn (thậm chí nó có thể là "containers" hoặc "high-order functions"), nhưng chúng vẫn là một phần của UI.
Đơn giản vì các thành phần này trực tiếp hiển thị DOM (ví dụ: React/Vue Component) hoặc được kết hợp chặt chẽ với Redux Containers.
Và việc kết hợp business logic với UI là một vi phạm nghiêm trọng nguyên tắc thiết kế SOLID. Mỗi khi bạn thay đổi các phần tử trong UI, bạn có nguy cơ tiềm ẩn những thay đổi trong business logic. Nói cách khác, logic của bạn bị phụ thuộc vào UI.
Bob Martin đã viết cuốn "Clean Code" để giải thích lý do tại sao đây lại là một vấn đề như vậy và mình thực sự khuyên bạn nên đọc nó nếu bạn chưa đọc. Nhưng tại thời điểm này, mình cho rằng tất cả chúng ta đều đã làm.
Vấn đề thứ hai, nằm ở business logic của ứng dụng là phần quan trọng và có giá trị nhất của phần mềm lại bị phụ thuộc vào các thư viện của bên thứ 3 để giải quyết các vấn đề. Thuật ngữ của Bob, ứng dụng của bạn đã "kết hôn" với thư viện/framework của bên thứ 3.
Đừng hiểu lầm rằng mình có tư tưởng chống lại các framework hay thư viện. Chúng có thể rất hữu ích theo nhiều cách: tạo ra một cấu trúc vững chắc, cung cấp một ngôn ngữ chung cho các nhà phát triển từ các nhóm khác nhau, giúp giới thiệu các thành viên mới trong nhóm và tất nhiên, giúp bạn thực hiện một công việc lặp lại tẻ nhạt. Nhưng mình đang bỏ phiếu cho việc sử dụng hợp lý một công cụ mạnh mẽ như thế vì nó đi kèm với cái giá ở phía sau.
Hãy giả sử rằng chúng ta đã làm bài tập về nhà của mình và không chọn dùng framework mới nhất, cực hot nhưng chưa được kiểm định bởi những người khác (trong vài năm sử dụng trong môi trường production). Chúng ta cũng đã cân nhắc kỹ và chọn giải pháp tốt nhất cho mình, không chỉ giải pháp phổ biến nhất, so sánh một cách cẩn thận những ưu - nhược điểm.
Mặc dù vậy, chúng ta đã quyết "dùng" framework này. Tại sao lại như vậy ? Hãy tưởng tượng, phiên bản mới được phát hành. Bạn biết đấy, cái mà chúng ta thực sự nên chuyển sang vì nó sửa hơn 25 lỗ hổng nghiêm trọng đến từ các thư viện trên npm mà framework này sử dụng. Hoặc thậm chí tệ hơn, nhóm / công ty / CTO / PM / Bạn quyết định chuyển từ framework này sang framework khác bởi vì...tốt, bạn biết...các lý do.
Và hiện tại, chúng ta có nguy cơ phải thay đổi business logic của ứng dụng mặc dù thực tế chúng chẳng có tí thay đổi nào. Chúng ta không nhận được bất kỳ bản ghi nhớ mới nào từ nhóm tiếp thị hoặc phân tích kinh doanh. Chúng ta tính toán các khoản discount giống như cách chúng ta đã làm trước đây. Chúng ta tìm nạp các API giống nhau. Không có gì đổi thay về phía logic, nhưng tại sao lại phải thay đổi. Như vậy, chúng ta lại vi phạm nguyên tắc đầu tiên trong SOLID là Single Responsibility Principle và phải trả giá đắt.
Nhiều người trong chúng ta đã vượt qua được giai đoạn này. Chúng ta nhận thấy UI phải được tách rời khỏi business logic. Có lẽ chúng ta thậm chí đã bắt đầu đánh giá cao các mẫu MV-* (một lần nữa?). Và chúng ta đã nghe rất nhiều về quản lý state tập trung và những lợi ích của centralized store đem lại và quyết định "ah, Giờ tôi biết mình phải làm gì!".
Chúng ta tìm hiểu về các actions và mutations/reducers. Chúng ta biết rằng store là một single source of truth. Và chúng ta quyết định chuyển các tính toán discount, các yêu cầu API của chúng ta sang các action và mutation. Chúng ta dũng cảm trả lời các câu hỏi phỏng vấn như "chúng tôi nên tìm nạp request componentDidLoad hoặc action ở đâu?" để lấy được dữ liệu mới nhất. Điều đó hoàn toàn hợp lý: Store không phải là UI, đúng không nào? Chúng ta đã làm công việc của mình với mối quan tâm tách biệt.
Vấn đề bắt đầu xuất hiện khi chúng ta nhận ra điều hiển nhiên nhưng bằng cách nào đó lại bị che đi trước sự thật hiển hiện hàng ngày. Store chỉ có một trách nhiệm duy nhất: lưu trữ dữ liệu. Nó không liên quan gì đến business logic. Nó không quan tầm bằng cách nào, từ đâu và tại sao bạn lấy dữ liệu mình cần. Nó chỉ chịu trách nhiệm lưu trữ và cung cấp dữ liệu cho UI. Nó chỉ là một trường hợp đặc biệt một dạng cơ sở dữ liệu tạm thời. Vì vậy nếu chúng ta dùng chung nó với mã nguồn chứa business logic thì một lần nữa chúng ta lại phải trả giá khi vi phạm nguyên tắc Separation of Converns principle.
Và vẫn còn một vấn đề khác. Chúng ta đã phải đối mặt trước đây. Chúng ta đã kết hợp logic của mình với Flux / Redux / VueX hoặc bất kì thư viện bên thứ 3 nào. Tất cả những vấn đề chúng ta đã thảo luận trước đó lại chìm vào bóng tối. Hãy dừng lại một giây và tự trả lời câu hỏi: "Nếu ngày mai ta phải thay đổi giải pháp Store từ Redux qua VueX thì business logic có thay đổi hay bị ảnh hưởng không?" Có bao nhiêu business logic giá trị sẽ bị ảnh hưởng và phải viết lại, nhưng lại không hề có yêu cầu mới về thay đổi business logic nào.
Image for post
Photo by Olav Ahrens Røtne on Unsplash
Vậy giải pháp là gì? Một lần nữa Bob Martin đã trả lời điều này từ rất lâu trước đây: hãy giữ cho business không phụ thuộc vào logic. Sử dụng ít hoặc không sử dụng thư viện của bên thứ 3 cho các mã nguồn quan trọng. Hãy chắc chắn rằng không có lý do nào khác để thay đổi ngoại trừ sự thay đổi trong các business rule. Nó chỉ đơn giản vậy thôi.
Và việc tránh sự phụ thuộc chính là cách tốt nhất để có được sự dễ dàng trong kiểm thử. Chúng ta muốn mã nguồn quan trọng của mình được kiểm thử nhiều nhất có thể, phải không nào ?