Clean Architecture in Frontend

Image_637602c6d646ef0001551773
  • The Pandioner

  • 18/11/2022

Một số lý thuyết cơ bản và có ví dụ qua open-source.


Tôi muốn chia sẻ một số thông tin mà tôi thấy hữu ích cho các ứng dụng giao diện người dùng có kiến trúc lớn. Những ý tưởng đã được chứng minh là đáng tin cậy trong thực tế. Đồng thời, tôi muốn đơn giản hóa trong cách giải thích của mình.

Tôi đã triển khai một ứng dụng To-Do List không phức tạp để hỗ trợ giải thích. Ứng dụng sẽ sử dụng các nguyên tắc thiết kế giống như tôi đã áp dụng trên mô hình lớn hơn. Tôi sẽ sử dụng ứng dụng này để đưa ra các ví dụ về các thành phần riêng lẻ. Bạn cũng có thể xem source code trên Github để tự mình kiểm tra bức tranh hoàn chỉnh.

Các ví dụ sử dụng Angular và các công cụ xung quanh nó. Các nguyên tắc chung có thể được áp dụng trong bất kỳ hệ sinh thái nào khác.

Hình ảnh được lấy tại đây

Clean Architecture


Tôi lấy cảm hứng từ cuốn sách của Bob Martin, Clean Architecture. Đó là một cuốn sách tuyệt vời với nhiều hiểu biết sâu sắc về kiến ​​trúc phần mềm nói chung. Một tổng quan tuyệt vời và có cấu trúc tốt về những thứ quan trọng trong quá trình thiết kế hệ thống. Tôi nhận thấy các ý tưởng của Clean Architecture cũng có thể áp dụng được trong quá trình phát triển giao diện người dùng.

Mô hình được lấy từ The Clean Code Blog.

Clean Architecture là một cách để tách biệt một ứng dụng khỏi các frameworks, UI và databases, đảm bảo rằng các thành phần riêng lẻ có thể kiểm tra được. Nó tận dụng SOLID principles và chỉ ra cách kết hợp chúng lại với nhau trên kiến trúc lớn hơn.

Trong bài viết này, tôi chỉ mô tả một cách triển khai Clean Architecture. Sử dụng Angular làm framework và làm nơi chứa dependency injection.

High-level frontend architecture

Khi tiếp cận một tính năng mới, tôi nghĩ sẽ về một entity cơ bản và các operations mà nó cần. Sơ đồ này cho thấy high-level architecture của một tính năng mới. Chúng ta hãy xem xét kỹ hơn từng lớp này.

Entity

Các application layers có một hệ thống phân cấp. Entities ở trên cùng và giao diện người dùng ở dưới cùng. Một layer không được phụ thuộc vào bất kỳ layer bên dưới nào khác. Ví dụ: entity không được biết gì về giao diện người dùng. Nghe có vẻ khó hiểu, entity có lẽ là phần quan trọng nhất của clean architecture. Đó là nơi tôi bắt đầu thiết kế các tính năng hoàn toàn mới. Phần này tôi bảo vệ khỏi những thay đổi nhiều nhất. Mặc dù nó không có trên sơ đồ, nhưng Entity ở giữa tất cả các lớp này.

Có vẻ đơn giản, phải không? Một entity có thể đơn giản như Typescript interface. Ý tưởng cốt lõi là chỉ bao gồm những thuộc tính mô tả kiểu của một tính năng mới. Bất kỳ trạng thái nào có thể bắt nguồn từ các thuộc tính này đều không thuộc về đây.

Một trong những lỗi điển hình là đưa vào thực thể thông tin bổ sung giúp hiển thị. Bất cứ khi nào bạn sửa đổi thực thể, bạn phải kiểm tra kỹ xem dữ liệu mới có thuộc về domain không. Thông tin này phải liên quan đến giao diện người dùng, data management framework hoặc API.

Data layer

Vai trò của lớp này là cung cấp toolchain cho entity. Bạn cần thao tác gì? Các điều kiện trước/sau khi hoạt động được thực hiện là gì? Tần suất adapter (API) được gọi là? Bạn có cần cập nhật optimistic? Vấn đề gì về sorting, filtering và pagination? Có lẽ, bạn cũng cần tìm kiếm? Và bạn có thể cần một số thao tác đặc biệt như done/undone cho to-do element.

Có nhiều khả năng nhưng đảm bảo không thiết kế quá kỹ ứng dụng của bạn. Doanh nghiệp phải yêu cầu một số tính năng nhất định trước khi bạn triển khai các hoạt động mới cho data layer. Nếu không, ứng dụng có thể trở nên quá phức tạp mà không có lý do chính đáng. Nói cách khác, tại sao lại triển khai một tính năng nếu không ai cần nó? Ít code hơn có nghĩa là bảo trì ít hơn và thực hiện các yêu cầu mới nhanh hơn.

Phần còn lại của ứng dụng phụ thuộc vào logic trong data layer. Nó quyết định xem giao diện người dùng có nhận được một đối tượng từ cache hoặc remote API hay không.

Bạn có thể triển khai data layer với bất kỳ thư viện hoặc mẫu nào mà bạn thấy phù hợp với ứng dụng của mình. Nó phụ thuộc vào mức độ phức tạp của ứng dụng theo yêu cầu business. Một số khả năng:

  • Lớp có trạng thái internal. Nó có thể sử dụng RxJs Subjects/Observables.
  • Bất kỳ thư viện lấy cảm hứng từ Redux. Trong trường hợp này, Facade sẽ kích hoạt các hành động thay vì gọi trực tiếp các phương thức của data layer.
  • Bất kỳ thư viện state-management nào khác.
  • Facade có thể gọi trực tiếp Adapter. Về cơ bản, nó bỏ qua data layer nếu bạn không cần bất kỳ logic cache nào.

Adapter

Nói đúng ra, Adapter cũng thuộc về data layer. Đó là một khái niệm mạnh mẽ để đảm bảo rằng ứng dụng được well-isolated với API và những thay đổi của nó. Data services phụ thuộc vào sự trừu tượng của adapter's mà  tôi hoàn toàn kiểm soát. Đây là một triển khai của dependency inversion principle: Tôi tạo một abstract class cho adapter và sau đó sử dụng nó trong các data services. Tôi cũng viết một adapter hoàn toàn bị ẩn khỏi phần còn lại của ứng dụng. Do đó, lớp dữ liệu đưa ra các yêu cầu kỹ thuật của nó đối với việc triển khai adapter. Mặc dù dữ liệu chảy từ việc triển khai adapter sang các data services, adapter vẫn phụ thuộc vào data layer chứ không phải ngược lại.

Bạn có thể thiết kế ứng dụng của mình theo cách mà toàn bộ tương tác API được tách biệt hoàn toàn khỏi logic ứng dụng của bạn. Một vài lợi ích yêu thích của tôi:

  • Nếu API thay đổi thì tất cả những gì tôi phải làm là điều chỉnh việc triển adapter.
  • Nếu API không khả dụng, tôi vẫn có thể triển khai ứng dụng của mình. Và sau khi có API, tôi vẫn chỉ phải điều chỉnh triển khai adapter .

Trong ứng dụng này, tôi đã triển khai persistence layer dựa trên localStorage. Sau này có thể dễ dàng thay thế bằng lệnh gọi API. Mô hình này đã giúp tôi tiết kiệm vô số thời gian trong quá trình luyện tập.

Facade

Trong ví dụ hôm nay, facade là một đối tượng hoạt động như một interface giữa UI và data layer. Bất cứ khi nào UI cần tải todos hoặc tạo một cái mới, nó sẽ gọi một trong các phương thức của facade và nhận kết quả dưới dạng có thể quan sát được.

Facade có thể là bất cứ thứ gì bên trong.

  • Trong các tình huống đơn giản, tôi gọi trực tiếp các phương thức của adapter nếu tôi không cần bất kỳ caching hoặc data management.
  • Trong các trường hợp khác, tôi có thể kích hoạt một hành động giống như redux, ví dụ: dispatch(loadTodos())và sau đó lắng nghe các hành động tiếp theo loadTodosSuccessloadTodosFailure.
  • Tôi cũng có thể chuyển cuộc gọi từ facade sang một service khác điều phối tương tác với adapter. Nó có thể là service tự viết dựa trên Chủ đề RxJS hoặc service của bên thứ ba như dịch vụ từ @ngrx/data (đừng nhầm lẫn với NgRx)!

Tôi phân bổ trách nhiệm cho các lớp khác nhau. Data service được cho là yêu cầu dữ liệu từ adaptẻr, lưu dữ liệu vào kho lưu trữ và sắp xếp các bản cập nhật lạc quan nếu cần. Data service xác định cách thay đổi trạng thái sau mỗi thao tác.

Mặt khác, Facade hiển thị API dữ liệu cho giao diện người dùng. Nó có thể yêu cầu danh sách việc cần làm hoặc tạo một danh sách mới và sau đó nhận phản hồi từ todos$quan sát thống nhất có thể ẩn tất cả độ phức tạp của phản hồi. Đồng thời, bạn có thể nhận thấy rằng tôi sử dụng subscribe()phương thức bên trong Facade và sau đó trả về một giá trị có thể quan sát được. Tôi quyết định như vậy vì sự tiện lợi của logic ứng dụng. Đôi khi các thành phần kích hoạt một thao tác và thành phần nhận kết quả là khác nhau. Nó cũng có vòng đời khác nhau. Trong ứng dụng công việc này, đôi khi một thành phần kích hoạt bị hủy ngay sau khi nó yêu cầu một số dữ liệu, vì vậy tôi cần đảm bảo rằng một thành phần khác sẽ nhận được kết quả và duy trì ít nhất một đăng ký hoạt động. Facade thuận tiện cảm thấy khoảng cách này bằng cách giới thiệu bắt buộcsubscribe()nội bộ. Ngoài ra, nó đảm bảo rằng data service cơ bản không có logic bổ sung chỉ liên quan đến người sử dụng dữ liệu.

UI

Tại sao, giao diện người dùng cũng có logic! Công việc của UI là gọi facade vào đúng thời điểm, ví dụ: khởi tạo một thành phần hoặc một số hành động cụ thể của người dùng. Ngoài ra, UI chịu trách nhiệm quản lý trạng thái của nó. Không phải tất cả trạng thái đều chuyển đến data layer. Lớp giao diện người dùng phải vận hành trạng thái dành riêng cho giao diện người dùng.

Có nhiều cách tiếp cận để xử lý trạng thái giao diện người dùng. Và một lần nữa, sự lựa chọn phụ thuộc vào yêu cầu business. Đôi khi có thể chấp nhận lưu trữ trạng thái đơn giản trong một thành phần. Trong các trường hợp khác, cần có một cách để trao đổi dữ liệu giữa các thành phần giao diện người dùng. Tôi sẽ không đề cập đến chủ đề này ngày hôm nay và nó có thể là một cuộc trò chuyện vào một ngày khác.

Putting everything together

Data layer bao gồm data service và kho lưu trữ. Data service điều phối các hoạt động và logic trong khi kho lưu trữ chịu trách nhiệm về caching trong bộ nhớ. Tôi sử dụng @ngneat/elf để triển khai kho lưu trữ. Mặc dù nó có thể là bất kỳ thư viện nào khác hoặc thậm chí là code hoàn toàn tùy chỉnh.

Data service tương tác với abstract adapter để tìm nạp dữ liệu. Để đơn giản, tôi đã loại bỏ hoàn toàn backend và sử dụng triển khai dựa trên local-storage. Hãy nhớ rằng, khi backend khả dụng, các điều chỉnh trong ứng dụng phần đầu của tôi có thể sẽ không đáng kể.

What's next?


Tôi cố ý chỉ dán một phần mã trong bài viết để minh họa các ý tưởng. Tôi khuyến khích bạn duyệt qua source code và tự mình xem mọi thứ.

Được dịch từ: https://blog.alexeykarpov.com/clean-architecture-in-frontend