paint-brush
Thử nghiệm một kiến trúc sạch trong một ứng dụng giao diện người dùng - Điều đó có hợp lý không?từ tác giả@playerony
10,773 lượt đọc
10,773 lượt đọc

Thử nghiệm một kiến trúc sạch trong một ứng dụng giao diện người dùng - Điều đó có hợp lý không?

từ tác giả Paweł Wojtasiński21m2023/05/01
Read on Terminal Reader

dài quá đọc không nổi

Các nhà phát triển giao diện người dùng phải đối mặt với thách thức tạo ra các kiến trúc có thể mở rộng, có thể bảo trì. Nhiều ý tưởng kiến trúc được đề xuất có thể chưa bao giờ được triển khai trong môi trường sản xuất thực tế. Bài viết này nhằm mục đích cung cấp cho các nhà phát triển giao diện người dùng các công cụ họ cần để điều hướng thế giới phát triển trang web không ngừng phát triển.
featured image - Thử nghiệm một kiến trúc sạch trong một ứng dụng giao diện người dùng - Điều đó có hợp lý không?
Paweł Wojtasiński HackerNoon profile picture

Khi bối cảnh kỹ thuật số phát triển, sự phức tạp của các trang web hiện đại cũng vậy. Với nhu cầu ngày càng tăng về trải nghiệm người dùng tốt hơn và các tính năng nâng cao, các nhà phát triển giao diện người dùng phải đối mặt với thách thức tạo ra các kiến trúc có thể mở rộng, có thể bảo trì và hiệu quả.


Trong số rất nhiều bài báo và tài nguyên có sẵn về kiến trúc giao diện người dùng, một số lượng đáng kể tập trung vào Kiến trúc sạch và sự thích ứng của nó. Trên thực tế, hơn 50% trong số gần 70 bài báo được khảo sát thảo luận về Kiến trúc sạch trong bối cảnh phát triển front-end.


Mặc dù có rất nhiều thông tin, nhưng vẫn tồn tại một vấn đề nhức nhối: nhiều ý tưởng kiến trúc được đề xuất có thể chưa bao giờ được triển khai trong môi trường sản xuất thực tế. Điều này làm dấy lên nghi ngờ về tính hiệu quả và khả năng ứng dụng của chúng trong các tình huống thực tế.


Bị thúc đẩy bởi mối quan tâm này, tôi bắt đầu hành trình kéo dài sáu tháng để triển khai Kiến trúc sạch trên giao diện người dùng, cho phép tôi đối mặt với thực tế của những ý tưởng này và tách lúa mì ra khỏi vỏ trấu.


Trong bài viết này, tôi sẽ chia sẻ kinh nghiệm và hiểu biết của mình từ hành trình này, cung cấp hướng dẫn toàn diện về cách triển khai thành công Kiến trúc sạch trên giao diện người dùng.


Bằng cách làm sáng tỏ những thách thức, phương pháp hay nhất và giải pháp trong thế giới thực, bài viết này nhằm mục đích cung cấp cho các nhà phát triển giao diện người dùng những công cụ họ cần để điều hướng thế giới phát triển trang web không ngừng phát triển.

khung

Trong hệ sinh thái kỹ thuật số đang phát triển nhanh chóng ngày nay, các nhà phát triển tha hồ lựa chọn khi nói đến các khung giao diện người dùng. Sự phong phú của các tùy chọn này giải quyết nhiều vấn đề và đơn giản hóa quá trình phát triển.


Tuy nhiên, nó cũng dẫn đến những cuộc tranh luận bất tận giữa các nhà phát triển, mỗi người đều cho rằng framework ưa thích của họ vượt trội hơn những framework khác. Sự thật là, trong thế giới phát triển nhanh chóng của chúng ta, các thư viện JavaScript mới xuất hiện hàng ngày và các khung được giới thiệu gần như hàng tháng.


Để duy trì tính linh hoạt và khả năng thích ứng trong một môi trường năng động như vậy, chúng ta cần một kiến trúc vượt qua các khuôn khổ và công nghệ cụ thể.


Điều này đặc biệt quan trọng đối với các công ty sản phẩm hoặc hợp đồng dài hạn liên quan đến bảo trì, nơi phải đáp ứng các xu hướng thay đổi và tiến bộ công nghệ.


Không phụ thuộc vào các chi tiết, chẳng hạn như khung, cho phép chúng tôi tập trung vào sản phẩm mà chúng tôi đang làm việc và chuẩn bị cho những thay đổi có thể phát sinh trong vòng đời của nó.


Đừng sợ; bài viết này nhằm mục đích cung cấp một câu trả lời cho tình trạng khó xử này.

Hợp tác nhóm Fullstack

Trong nhiệm vụ triển khai Kiến trúc sạch trên giao diện người dùng, tôi đã hợp tác chặt chẽ với một số nhà phát triển fullstack và phụ trợ để đảm bảo rằng kiến trúc sẽ dễ hiểu và có thể bảo trì, ngay cả đối với những người có ít kinh nghiệm về giao diện người dùng.


Vì vậy, một trong những yêu cầu chính đối với kiến trúc của chúng tôi là khả năng tiếp cận của nó đối với các nhà phát triển phụ trợ, những người có thể không thông thạo về các vấn đề phức tạp của giao diện người dùng, cũng như các nhà phát triển fullstack có thể không có chuyên môn sâu rộng về giao diện người dùng.


Bằng cách thúc đẩy sự hợp tác liền mạch giữa các nhóm frontend và backend, kiến trúc nhằm thu hẹp khoảng cách và tạo ra trải nghiệm phát triển thống nhất.

Cơ sở lý thuyết

Thật không may, để xây dựng một số nội dung tuyệt vời, chúng ta cần có một số bí quyết cơ bản. Sự hiểu biết rõ ràng về các nguyên tắc cơ bản sẽ không chỉ tạo thuận lợi cho quá trình triển khai mà còn đảm bảo rằng kiến trúc tuân thủ các thông lệ tốt nhất trong phát triển phần mềm.


Trong phần này, chúng tôi sẽ giới thiệu ba khái niệm chính tạo thành nền tảng cho phương pháp tiếp cận kiến trúc của chúng tôi: nguyên tắc SOLID , Kiến trúc sạch (thực sự xuất phát từ nguyên tắc SOLID) và Thiết kế nguyên tử . Nếu bạn cảm thấy mạnh mẽ về những lĩnh vực này, bạn có thể bỏ qua phần này.

Nguyên tắc RẮN

SOLID là từ viết tắt đại diện cho năm nguyên tắc thiết kế hướng dẫn các nhà phát triển tạo phần mềm có thể mở rộng, có thể bảo trì và mô-đun:


  • Nguyên tắc Trách nhiệm Đơn lẻ (SRP) : Nguyên tắc này nói rằng một lớp chỉ nên có một lý do để thay đổi, nghĩa là nó phải có một trách nhiệm duy nhất. Bằng cách tuân thủ SRP, các nhà phát triển có thể tạo mã tập trung hơn, dễ bảo trì và có thể kiểm tra hơn.


  • Nguyên tắc Mở/Đóng (OCP) : Theo OCP, các thực thể phần mềm nên mở để mở rộng nhưng đóng để sửa đổi. Điều này có nghĩa là các nhà phát triển có thể thêm chức năng mới mà không cần thay đổi mã hiện có, giảm nguy cơ phát sinh lỗi.


  • Nguyên tắc thay thế Liskov (LSP) : LSP khẳng định rằng các đối tượng của lớp dẫn xuất sẽ có thể thay thế các đối tượng của lớp cơ sở mà không ảnh hưởng đến tính đúng đắn của chương trình. Nguyên tắc này thúc đẩy việc sử dụng hợp lý tính kế thừa và tính đa hình.


  • Nguyên tắc Phân tách Giao diện (ISP) : ISP nhấn mạnh rằng khách hàng không nên bị buộc phải phụ thuộc vào các giao diện mà họ không sử dụng. Bằng cách tạo các giao diện nhỏ hơn, tập trung hơn, các nhà phát triển có thể đảm bảo khả năng bảo trì và tổ chức mã tốt hơn.


  • Nguyên tắc đảo ngược phụ thuộc (DIP) : DIP khuyến khích các nhà phát triển phụ thuộc vào sự trừu tượng hơn là triển khai cụ thể. Nguyên tắc này thúc đẩy cơ sở mã theo mô-đun, có thể kiểm tra và linh hoạt hơn.


Nếu bạn muốn khám phá chủ đề này sâu hơn, điều mà tôi thực sự khuyến khích bạn làm, thì không vấn đề gì. Tuy nhiên, hiện tại, những gì tôi trình bày đã đủ để đi xa hơn.


Và SOLID cung cấp cho chúng ta điều gì trong bài viết này?

Kiến trúc sạch

Robert C. Martin, dựa trên các nguyên tắc SOLID và kinh nghiệm sâu rộng của ông trong việc phát triển các ứng dụng khác nhau, đã đề xuất khái niệm về Kiến trúc sạch. Khi thảo luận về khái niệm này, sơ đồ dưới đây thường được tham chiếu để thể hiện trực quan cấu trúc của nó:



Vì vậy, Clean Architecture không phải là một khái niệm mới; nó đã được sử dụng rộng rãi trong các mô hình lập trình khác nhau, bao gồm lập trình chức năng và phát triển phụ trợ.


Các thư viện như Lodash và nhiều khung phụ trợ đã áp dụng phương pháp tiếp cận kiến trúc này, bắt nguồn từ các nguyên tắc RẮN.


Clean Architecture nhấn mạnh việc tách biệt các mối quan tâm và tạo ra các lớp độc lập, có thể kiểm tra trong một ứng dụng, với mục tiêu chính là làm cho hệ thống dễ hiểu, dễ bảo trì và sửa đổi.


Kiến trúc được tổ chức thành các vòng tròn hoặc lớp đồng tâm; mỗi bên đều có ranh giới, sự phụ thuộc và trách nhiệm rõ ràng:


  • Thực thể : Đây là các đối tượng và quy tắc kinh doanh cốt lõi trong ứng dụng. Các thực thể thường là các đối tượng đơn giản đại diện cho các khái niệm thiết yếu hoặc cấu trúc dữ liệu trong miền, chẳng hạn như người dùng, sản phẩm hoặc đơn đặt hàng.


  • Ca sử dụng : Còn được gọi là Người tương tác, Ca sử dụng xác định các quy tắc kinh doanh dành riêng cho ứng dụng và sắp xếp sự tương tác giữa Thực thể và hệ thống bên ngoài. Các Ca sử dụng chịu trách nhiệm triển khai chức năng cốt lõi của ứng dụng và không thể biết được các lớp bên ngoài.


  • Bộ điều hợp giao diện : Các thành phần này đóng vai trò là cầu nối giữa lớp bên trong và lớp bên ngoài, chuyển đổi dữ liệu giữa trường hợp sử dụng và các định dạng hệ thống bên ngoài. Bộ điều hợp giao diện bao gồm kho lưu trữ, người trình bày và bộ điều khiển, cho phép ứng dụng tương tác với cơ sở dữ liệu, API bên ngoài và khung giao diện người dùng.


  • Khung và Trình điều khiển : Lớp ngoài cùng này bao gồm các hệ thống bên ngoài, chẳng hạn như cơ sở dữ liệu, khung giao diện người dùng và thư viện của bên thứ ba. Khung và Trình điều khiển chịu trách nhiệm cung cấp cơ sở hạ tầng cần thiết để chạy ứng dụng và triển khai các giao diện được xác định trong các lớp bên trong.


Kiến trúc sạch thúc đẩy luồng phụ thuộc từ các lớp bên ngoài vào các lớp bên trong, đảm bảo rằng logic nghiệp vụ cốt lõi vẫn độc lập với các công nghệ hoặc khung cụ thể được sử dụng.


Điều này dẫn đến một cơ sở mã linh hoạt, có thể bảo trì và có thể kiểm tra, có thể dễ dàng thích ứng với các yêu cầu hoặc ngăn xếp công nghệ đang thay đổi.

thiết kế nguyên tử

Atomic Design là một phương pháp tổ chức các thành phần giao diện người dùng bằng cách chia nhỏ các giao diện thành các phần tử cơ bản nhất của chúng và sau đó lắp ráp lại chúng thành các cấu trúc phức tạp hơn. Brad Frost lần đầu tiên giới thiệu khái niệm này vào năm 2008 trong một bài báo có tiêu đề "Phương pháp thiết kế nguyên tử".


Đây là một hình ảnh thể hiện khái niệm Thiết kế nguyên tử:



Nó bao gồm năm cấp độ riêng biệt:


  • Nguyên tử : Đơn vị nhỏ nhất, không thể chia cắt của giao diện, chẳng hạn như nút, đầu vào và nhãn.


  • Phân tử : Nhóm nguyên tử hoạt động cùng nhau, tạo thành các thành phần giao diện người dùng phức tạp hơn như biểu mẫu hoặc thanh điều hướng.


  • Sinh vật : Sự kết hợp của các phân tử và nguyên tử tạo ra các phần riêng biệt của giao diện, chẳng hạn như đầu trang hoặc chân trang.


  • Bản mẫu : Thể hiện bố cục và cấu trúc của một trang, cung cấp khung cho việc sắp xếp các sinh vật, phân tử và nguyên tử.


  • Trang : Phiên bản mẫu chứa đầy nội dung thực tế, hiển thị giao diện cuối cùng.


Bằng cách sử dụng Thiết kế nguyên tử, các nhà phát triển có thể gặt hái một số lợi ích, chẳng hạn như tính mô đun, khả năng sử dụng lại và cấu trúc rõ ràng cho các thành phần giao diện người dùng, bởi vì nó yêu cầu chúng ta tuân theo phương pháp Hệ thống thiết kế, nhưng đây không phải là chủ đề của bài viết này, vì vậy hãy tiếp tục.

Nghiên cứu điển hình: NotionLingo

Để phát triển quan điểm đầy đủ thông tin về Kiến trúc sạch cho phát triển giao diện người dùng, tôi bắt đầu hành trình tạo một ứng dụng. Trong khoảng thời gian sáu tháng, tôi đã thu được những hiểu biết và kinh nghiệm quý báu khi làm việc cho dự án này.


Do đó, các ví dụ được cung cấp trong suốt bài viết này rút ra từ kinh nghiệm thực tế của tôi với ứng dụng. Để duy trì tính minh bạch, tất cả các ví dụ đều được lấy từ mã có thể truy cập công khai.


Bạn có thể khám phá kết quả cuối cùng bằng cách truy cập kho lưu trữ tại https://github.com/Levofron/NotionLingo .

Triển khai kiến trúc sạch

Như đã đề cập trước đó, có rất nhiều triển khai của Clean Architecture có sẵn trực tuyến. Tuy nhiên, một vài yếu tố phổ biến có thể được xác định qua các triển khai này:


  • Lớp miền : Cốt lõi của ứng dụng của chúng tôi, bao gồm các mô hình, trường hợp sử dụng và hoạt động liên quan đến kinh doanh.


  • Lớp API : Chịu trách nhiệm tương tác với các API của trình duyệt.


  • Lớp kho lưu trữ : Đóng vai trò là cầu nối giữa miền và lớp API, cung cấp không gian để ánh xạ các loại API với các loại miền của chúng tôi.


  • Lớp giao diện người dùng : Chứa các thành phần của chúng tôi, tạo thành giao diện người dùng.


Bằng cách hiểu những điểm chung này, chúng ta có thể đánh giá cao cấu trúc cơ bản của Kiến trúc sạch và điều chỉnh nó cho phù hợp với nhu cầu cụ thể của chúng ta.

Lãnh địa

Phần cốt lõi của ứng dụng của chúng tôi chứa:


  • Các trường hợp sử dụng : Các trường hợp sử dụng mô tả các quy tắc kinh doanh cho các hoạt động khác nhau, chẳng hạn như lưu, cập nhật và tìm nạp dữ liệu. Ví dụ: một trường hợp sử dụng có thể liên quan đến việc tìm nạp danh sách các từ từ Notion hoặc tăng chuỗi hàng ngày của người dùng đối với các từ đã học.


    Về cơ bản, các trường hợp sử dụng xử lý các tác vụ và quy trình của ứng dụng từ góc độ nghiệp vụ, đảm bảo rằng hệ thống hoạt động phù hợp với các mục tiêu mong muốn.


  • Mô hình : Mô hình đại diện cho các thực thể kinh doanh trong ứng dụng. Chúng có thể được xác định bằng giao diện TypeScript, đảm bảo rằng chúng phù hợp với nhu cầu và yêu cầu kinh doanh.


    Ví dụ: nếu trường hợp sử dụng liên quan đến việc tìm nạp danh sách các từ từ Notion, thì bạn sẽ cần một mô hình để mô tả chính xác cấu trúc dữ liệu cho danh sách đó, tuân thủ các ràng buộc và quy tắc kinh doanh phù hợp.


  • Hoạt động : Đôi khi, việc xác định một số tác vụ nhất định dưới dạng các trường hợp sử dụng có thể không khả thi hoặc bạn có thể muốn tạo các chức năng có thể tái sử dụng có thể được sử dụng trên nhiều phần trong miền của mình. Chẳng hạn, nếu bạn cần viết một hàm để tìm kiếm một từ Notion theo tên, thì đây là nơi các thao tác đó sẽ nằm trong đó.


    Các hoạt động rất hữu ích để đóng gói logic dành riêng cho miền có thể được chia sẻ và sử dụng trong các ngữ cảnh khác nhau trong ứng dụng.


  • Giao diện kho lưu trữ : Các trường hợp sử dụng yêu cầu phương tiện để truy cập dữ liệu. Theo Nguyên tắc đảo ngược phụ thuộc, lớp miền không được phụ thuộc vào bất kỳ lớp nào khác (trong khi các lớp khác phụ thuộc vào nó); do đó, lớp này xác định các giao diện cho các kho lưu trữ.


    Điều quan trọng cần lưu ý là nó chỉ định giao diện chứ không phải chi tiết triển khai. Bản thân các kho lưu trữ sử dụng Mẫu kho lưu trữ không liên quan đến nguồn dữ liệu thực tế và nhấn mạnh logic để tìm nạp hoặc gửi dữ liệu đến và từ các nguồn đó.


    Điều quan trọng cần đề cập là một kho lưu trữ duy nhất có thể triển khai nhiều API và một Trường hợp sử dụng duy nhất có thể sử dụng nhiều kho lưu trữ.

API

Lớp này chịu trách nhiệm truy cập dữ liệu và có thể giao tiếp với nhiều nguồn khác nhau khi cần thiết. Xem xét rằng chúng tôi đang phát triển một ứng dụng giao diện người dùng, lớp này sẽ chủ yếu đóng vai trò là trình bao bọc cho API trình duyệt.


Điều này bao gồm các API cho REST, bộ nhớ cục bộ, IndexedDB, tổng hợp giọng nói, v.v.


Điều quan trọng cần lưu ý là nếu bạn muốn tạo các loại OpenAPI và ứng dụng khách HTTP, lớp API là nơi lý tưởng để đặt chúng. Trong lớp này, chúng ta có:


  • Bộ điều hợp API : Bộ điều hợp API là bộ điều hợp chuyên dụng dành cho API trình duyệt được sử dụng trong ứng dụng của chúng tôi. Thành phần này quản lý các cuộc gọi REST và giao tiếp với bộ nhớ của ứng dụng hoặc bất kỳ nguồn dữ liệu nào khác mà bạn muốn sử dụng.


    Bạn thậm chí có thể tạo và triển khai hệ thống lưu trữ đối tượng của riêng mình nếu muốn. Bằng cách có Bộ điều hợp API chuyên dụng, bạn có thể duy trì giao diện nhất quán để tương tác với nhiều nguồn dữ liệu khác nhau, giúp dễ dàng cập nhật hoặc thay đổi chúng khi cần.


  • Loại : Đây là nơi dành cho tất cả các loại liên quan đến API của bạn. Các loại này không liên quan trực tiếp đến miền nhưng đóng vai trò mô tả các phản hồi thô nhận được từ API. Trong lớp tiếp theo, các loại này sẽ rất cần thiết để lập bản đồ và xử lý thích hợp.

Kho

Lớp kho lưu trữ đóng một vai trò quan trọng trong kiến trúc của ứng dụng bằng cách quản lý việc tích hợp nhiều API, ánh xạ các loại API dành riêng cho các loại miền và kết hợp các hoạt động để chuyển đổi dữ liệu.


Ví dụ: nếu bạn muốn kết hợp API tổng hợp giọng nói với bộ nhớ cục bộ, thì đây là nơi lý tưởng để làm điều đó. Lớp này chứa:


  • Triển khai kho lưu trữ : Đây là các triển khai cụ thể của các giao diện được khai báo trong lớp miền. Chúng có khả năng làm việc với nhiều nguồn dữ liệu, đảm bảo tính linh hoạt và khả năng thích ứng trong ứng dụng.


  • Hoạt động : Chúng có thể được gọi là người lập bản đồ, người biến áp hoặc người trợ giúp. Trong bối cảnh này, hoạt động là một thuật ngữ phù hợp. Thư mục này chứa tất cả các chức năng chịu trách nhiệm ánh xạ các phản hồi API thô với các loại miền tương ứng của chúng, đảm bảo rằng dữ liệu được cấu trúc phù hợp để sử dụng trong ứng dụng.

bộ chuyển đổi


Lớp bộ điều hợp chịu trách nhiệm sắp xếp các tương tác giữa các lớp này và buộc chúng lại với nhau. Lớp này chỉ chứa các mô-đun chịu trách nhiệm về:


  • Dependency Injection : Lớp Bộ điều hợp quản lý các phụ thuộc giữa các lớp API, Kho lưu trữ và Miền. Bằng cách xử lý nội xạ phụ thuộc, lớp Bộ điều hợp đảm bảo phân tách rõ ràng các mối quan tâm và thúc đẩy khả năng sử dụng lại mã hiệu quả.


  • Tổ chức mô-đun : Lớp Bộ điều hợp tổ chức ứng dụng thành các mô-đun dựa trên các chức năng của chúng (ví dụ: lưu trữ cục bộ, REST, tổng hợp giọng nói, Supabase). Mỗi mô-đun đóng gói một chức năng cụ thể, cung cấp cấu trúc mô-đun rõ ràng cho ứng dụng.


  • Xây dựng các hành động : Lớp Bộ điều hợp xây dựng các hành động bằng cách kết hợp các trường hợp sử dụng từ lớp Miền với các kho lưu trữ thích hợp. Những hành động này đóng vai trò là điểm vào để ứng dụng tương tác với các lớp bên dưới.

Bài thuyết trình

Lớp trình bày chịu trách nhiệm hiển thị giao diện người dùng (UI) và xử lý các tương tác của người dùng với ứng dụng. Nó tận dụng các lớp bộ điều hợp, miền và chia sẻ để tạo giao diện người dùng có chức năng và tương tác.


Lớp trình bày sử dụng phương pháp Thiết kế nguyên tử để tổ chức các thành phần của nó, dẫn đến một ứng dụng có thể mở rộng và bảo trì được. Tuy nhiên, lớp này sẽ không phải là trọng tâm chính của bài viết này, vì nó không phải là chủ đề chính về triển khai Kiến trúc sạch.

chia sẻ

Một nơi được chỉ định là cần thiết cho tất cả các yếu tố chung, chẳng hạn như các tiện ích tập trung, cấu hình và logic được chia sẻ. Tuy nhiên, chúng ta sẽ không đi sâu vào lớp này trong bài viết này.


Điều đáng nói chỉ là cung cấp sự hiểu biết về cách các thành phần phổ biến được quản lý và chia sẻ trong toàn bộ ứng dụng.

Chiến lược thử nghiệm cho từng lớp

Bây giờ, trước khi đi sâu vào mã hóa, điều cần thiết là thảo luận về thử nghiệm. Đảm bảo độ tin cậy và tính chính xác của ứng dụng của bạn là rất quan trọng và điều quan trọng là triển khai chiến lược thử nghiệm mạnh mẽ cho từng lớp của kiến trúc.


  • Lớp miền : Thử nghiệm đơn vị là phương pháp chính để kiểm tra lớp miền. Tập trung vào thử nghiệm các mô hình miền, quy tắc xác thực và logic nghiệp vụ, đảm bảo chúng hoạt động chính xác trong các điều kiện khác nhau. Áp dụng phát triển dựa trên thử nghiệm (TDD) để thúc đẩy thiết kế các mô hình miền của bạn và xác nhận rằng logic kinh doanh của bạn hợp lý.


  • Lớp API : Kiểm tra lớp API bằng các bài kiểm tra tích hợp. Các thử nghiệm này nên tập trung vào việc đảm bảo rằng API tương tác chính xác với các dịch vụ bên ngoài và các phản hồi được định dạng đúng. Sử dụng các công cụ như khung kiểm tra tự động, chẳng hạn như Jest, để mô phỏng các lệnh gọi API và xác thực các phản hồi.


  • Lớp kho lưu trữ : Đối với lớp kho lưu trữ, bạn có thể sử dụng kết hợp các bài kiểm tra đơn vị và tích hợp. Kiểm tra đơn vị có thể được sử dụng để kiểm tra các phương pháp kho lưu trữ riêng lẻ, trong khi kiểm tra tích hợp nên tập trung vào việc xác minh rằng kho lưu trữ tương tác chính xác với API của chúng.


  • Lớp bộ điều hợp : Các bài kiểm tra đơn vị phù hợp để kiểm tra lớp bộ điều hợp. Các thử nghiệm này phải đảm bảo rằng các bộ điều hợp đưa các thành phần phụ thuộc vào chính xác và quản lý việc chuyển đổi dữ liệu giữa các lớp. Giả lập các phần phụ thuộc, chẳng hạn như lớp API hoặc kho lưu trữ, có thể giúp cách ly lớp bộ điều hợp trong quá trình thử nghiệm.


Bằng cách triển khai chiến lược thử nghiệm toàn diện cho từng lớp của kiến trúc, bạn có thể đảm bảo độ tin cậy, tính chính xác và khả năng bảo trì của ứng dụng đồng thời giảm khả năng xuất hiện lỗi trong quá trình phát triển.


Tuy nhiên, nếu bạn đang xây dựng một ứng dụng nhỏ, thì các thử nghiệm tích hợp trên lớp bộ điều hợp là đủ.

Hãy viết mã gì đó

Được rồi, bây giờ bạn đã hiểu rõ về Kiến trúc sạch và thậm chí có thể hình thành quan điểm của riêng mình về nó, hãy tìm hiểu sâu hơn một chút và khám phá một số mã thực tế.


Hãy nhớ rằng tôi sẽ chỉ trình bày một ví dụ đơn giản ở đây; tuy nhiên, nếu bạn quan tâm đến các ví dụ chi tiết hơn, vui lòng khám phá kho lưu trữ GitHub của tôi được đề cập ở đầu bài viết này.


Trong "cuộc sống thực", Kiến trúc sạch thực sự tỏa sáng trong các ứng dụng lớn, cấp doanh nghiệp, trong khi nó có thể quá mức cần thiết đối với các dự án nhỏ hơn. Như đã nói, chúng ta hãy đi thẳng vào vấn đề.


Sử dụng ứng dụng của tôi làm ví dụ, tôi sẽ trình bày cách thực hiện lệnh gọi API để tìm nạp các đề xuất từ điển cho một từ nhất định. Điểm cuối API cụ thể này truy xuất danh sách các ý nghĩa và ví dụ bằng cách quét hai trang web trên web.


Từ góc độ kinh doanh, điểm cuối này rất quan trọng đối với chế độ xem "Tìm từ", cho phép người dùng tìm kiếm một từ cụ thể. Sau khi người dùng tìm thấy từ và đăng nhập, họ có thể thêm thông tin được quét trên web vào Cơ sở dữ liệu Notion của họ.

Cấu trúc thư mục

Để bắt đầu, chúng ta phải thiết lập cấu trúc thư mục phản ánh chính xác các lớp mà chúng ta đã thảo luận trước đó. Cấu trúc sẽ giống như sau:


 client ├── adapter ├── api ├── domain ├── presentation ├── repository └── shared


Thư mục máy khách phục vụ mục đích tương tự như thư mục "src" trong nhiều dự án. Trong dự án Next.js cụ thể này, tôi đã áp dụng quy ước đặt tên thư mục giao diện người dùng là "máy khách" và thư mục phụ trợ là "máy chủ".


Cách tiếp cận này cho phép phân biệt rõ ràng giữa hai thành phần chính của ứng dụng.

thư mục con

Chọn cấu trúc thư mục phù hợp cho dự án của bạn thực sự là một quyết định quan trọng cần được đưa ra sớm trong quá trình phát triển. Các nhà phát triển khác nhau có sở thích và cách tiếp cận riêng khi sắp xếp tài nguyên.


Một số có thể nhóm các tài nguyên theo tên trang, một số khác có thể tuân theo các quy ước đặt tên thư mục con do OpenAPI tạo ra và vẫn còn những người khác có thể tin rằng ứng dụng của họ quá nhỏ để đảm bảo một trong hai giải pháp đó.


Điều quan trọng là chọn một cấu trúc phù hợp nhất với nhu cầu và quy mô cụ thể của dự án của bạn trong khi vẫn duy trì một tổ chức tài nguyên rõ ràng và có thể duy trì được.


Tôi thuộc nhóm thứ ba, vì vậy cấu trúc của tôi trông như thế này:


 client ├── adapter │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ └── supabase ├── api │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ └── supabase ├── domain │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ ├── supabase └── repository ├── local-storage ├── rest ├── speech-synthesis └── supabase


Tôi đã quyết định bỏ qua các lớp trình bày và chia sẻ trong bài viết này, vì tôi tin rằng những ai muốn tìm hiểu sâu hơn có thể tham khảo kho lưu trữ của tôi để biết thêm thông tin. Bây giờ, hãy tiếp tục với một số ví dụ mã để minh họa cách áp dụng Kiến trúc sạch trong ứng dụng giao diện người dùng.

Định nghĩa tên miền

Hãy xem xét các yêu cầu của chúng tôi. Với tư cách là người dùng, tôi muốn nhận được một danh sách các đề xuất, bao gồm cả ý nghĩa và ví dụ của chúng. Do đó, một gợi ý từ điển có thể được mô hình hóa như sau:


 interface DictionarySuggestion { example: string; meaning: string; }


Bây giờ chúng tôi đã mô tả một đề xuất từ điển duy nhất, điều quan trọng cần đề cập là đôi khi từ thu được thông qua tìm kiếm trên web khác hoặc được sửa so với từ mà người dùng đã nhập. Để phù hợp với điều này, chúng tôi sẽ sử dụng phiên bản đã sửa sau này trong ứng dụng của mình.


Do đó, chúng ta cần xác định một giao diện bao gồm danh sách gợi ý từ điển và cách sửa từ. Giao diện cuối cùng trông như thế này:


 export interface DictionarySuggestions { suggestions: DictionarySuggestion[]; word: string; }


Chúng tôi đang xuất giao diện này, đó là lý do tại sao từ khóa export được đưa vào.

Giao diện kho lưu trữ

Chúng tôi đã có mô hình của mình và bây giờ là lúc đưa nó vào sử dụng.


 import { DictionarySuggestions } from './rest.models'; export interface RestRepository { getDictionarySuggestions: (word: string) => Promise<DictionarySuggestions | null>; }


Tại thời điểm này, mọi thứ nên rõ ràng. Điều quan trọng cần lưu ý là chúng ta hoàn toàn không thảo luận về API ở đây! Bản thân cấu trúc của kho lưu trữ khá đơn giản: chỉ là một đối tượng với một số phương thức, trong đó mỗi phương thức trả về dữ liệu của một loại cụ thể một cách không đồng bộ.


Xin lưu ý rằng kho lưu trữ luôn trả về dữ liệu ở định dạng mô hình miền.

Trường hợp sử dụng

Bây giờ, hãy xác định quy tắc kinh doanh của chúng ta là một trường hợp sử dụng. Mã này trông như thế này:


 export type GetDictionarySuggestionsUseCaseUseCase = UseCaseWithSingleParamAndPromiseResult< string, DictionarySuggestions | null >; export const getDictionarySuggestionsUseCase = ( restRepository: RestRepository, ): GetDictionarySuggestionsUseCaseUseCase => ({ execute: (word) => restRepository.getDictionarySuggestions(word), });


Điều đầu tiên cần lưu ý là danh sách các loại phổ biến được sử dụng để xác định các trường hợp sử dụng. Để đạt được điều này, tôi đã tạo một tệp use-cases.types.ts trong thư mục miền:


 domain ├── local-storage ├── rest ├── speech-synthesis ├── supabase └── use-cases.types.ts


Điều này cho phép tôi dễ dàng chia sẻ các loại cho các trường hợp sử dụng giữa các thư mục con của mình. Định nghĩa của UseCaseWithSingleParamAndPromiseResult trông như thế này:


 export interface UseCaseWithSingleParamAndPromiseResult<TParam, TResult> { execute: (param: TParam) => Promise<TResult>; }


Cách tiếp cận này giúp duy trì tính nhất quán và khả năng sử dụng lại của các loại trường hợp sử dụng trên lớp miền.


Bạn có thể thắc mắc tại sao chúng ta cần hàm execute . Ở đây, chúng tôi có một nhà máy trả về trường hợp sử dụng thực tế.


Lựa chọn thiết kế này là do chúng tôi không muốn tham chiếu trực tiếp việc triển khai kho lưu trữ trong mã trường hợp sử dụng, chúng tôi cũng không muốn kho lưu trữ được sử dụng bởi một lần nhập. Cách tiếp cận này cho phép chúng ta dễ dàng áp dụng phép tiêm phụ thuộc sau này.


Bằng cách sử dụng mẫu xuất xưởng và chức năng execute , chúng tôi có thể tách biệt các chi tiết triển khai của kho lưu trữ khỏi mã trường hợp sử dụng, giúp cải thiện tính mô đun và khả năng bảo trì của ứng dụng.


Cách tiếp cận này tuân theo Nguyên tắc đảo ngược phụ thuộc, trong đó lớp miền không phụ thuộc vào bất kỳ lớp nào khác và nó cho phép tính linh hoạt cao hơn khi hoán đổi các triển khai kho lưu trữ khác nhau hoặc sửa đổi kiến trúc của ứng dụng.

Định nghĩa API

Trước tiên, hãy xác định giao diện của chúng tôi:


 export interface RestApi { getDictionarySuggestions: (word: string) => Promise<AxiosResponse<DictionarySuggestions>>; }


Như bạn có thể thấy, định nghĩa của chức năng này trong giao diện gần giống với định nghĩa trong kho lưu trữ. Vì loại miền đã mô tả phản hồi nên không cần phải tạo lại loại tương tự.


Điều quan trọng cần lưu ý là API của chúng tôi trả về dữ liệu thô, đó là lý do tại sao chúng tôi trả lại toàn bộ AxiosResponse<DictionarySuggestions> . Bằng cách đó, chúng tôi duy trì sự tách biệt rõ ràng giữa các lớp API và miền, cho phép linh hoạt hơn trong việc xử lý và chuyển đổi dữ liệu.


Việc triển khai API này trông như thế này:


 export const getRestApi = (axiosInstance: AxiosInstance): RestApi => ({ getDictionarySuggestions: async (word: string) => { const encodedCurrentDate = encodeURIComponent(word); const response = await axiosInstance.get( `${RestEndpoints.GET_DICTIONARY_SUGGESTIONS}?word=${encodedCurrentDate}`, ); return response; } });


Tại thời điểm này, mọi thứ trở nên thú vị hơn. Khía cạnh quan trọng đầu tiên cần thảo luận là việc đưa vào axiosInstance của chúng tôi. Điều này làm cho mã của chúng tôi rất linh hoạt và cho phép chúng tôi xây dựng các bài kiểm tra vững chắc một cách dễ dàng. Đây cũng là nơi chúng tôi xử lý mã hóa hoặc phân tích cú pháp các tham số truy vấn.


Tuy nhiên, bạn cũng có thể thực hiện các hành động khác tại đây, chẳng hạn như cắt bớt chuỗi đầu vào. Bằng cách thêm axiosInstance , chúng tôi duy trì sự tách biệt rõ ràng giữa các mối quan tâm và đảm bảo rằng việc triển khai API có thể thích ứng với các tình huống hoặc thay đổi khác nhau trong các dịch vụ bên ngoài.

Triển khai kho lưu trữ

Vì giao diện của chúng tôi đã được xác định bởi miền, tất cả những gì chúng tôi phải làm là triển khai kho lưu trữ của mình. Vì vậy, việc thực hiện cuối cùng trông như thế này:

 export const getRestRepository = (restApi: RestApi): RestRepository => ({ getDictionarySuggestions: async (word) => { const { data } = await restApi.getDictionarySuggestions(word); if (!data?.suggestions?.length) { return null; } return formatDictionarySuggestions(data); } });


Một khía cạnh quan trọng cần đề cập là liên quan đến API. getRestRepository của chúng tôi cho phép chúng tôi chuyển một restApi đã xác định trước đó. Điều này thuận lợi vì, như đã đề cập trước đó, nó cho phép thử nghiệm dễ dàng hơn. Chúng ta có thể kiểm tra ngắn gọn formatDictionarySuggestions :


 export const formatDictionarySuggestions = ({ suggestions, word, }: DictionarySuggestions): DictionarySuggestions => { const cleanedWord = cleanUpString(word); const cleanedSuggestions = suggestions.map((_suggestion) => { const cleanedMeaning = cleanUpString(_suggestion.meaning); const cleanedExample = cleanUpString(_suggestion.example); return { meaning: cleanedMeaning, example: cleanedExample, }; }); return { word: cleanedWord, suggestions: cleanedSuggestions, }; };


Thao tác này lấy mô hình DictionarySuggestions miền của chúng tôi làm đối số và thực hiện dọn dẹp chuỗi, nghĩa là xóa khoảng trắng, ngắt dòng, tab và viết hoa không cần thiết. Nó khá đơn giản, không có sự phức tạp tiềm ẩn nào.


Một điều quan trọng cần lưu ý là tại thời điểm này, bạn không cần phải lo lắng về việc triển khai API của mình. Xin nhắc lại, kho lưu trữ luôn trả về dữ liệu trong mô hình miền! Không thể khác được vì làm như vậy sẽ phá vỡ nguyên tắc nghịch đảo phụ thuộc.


Và hiện tại, lớp miền của chúng tôi không phụ thuộc vào bất kỳ thứ gì được xác định bên ngoài nó.

Bộ điều hợp - Hãy đặt tất cả những thứ này lại với nhau

Tại thời điểm này, mọi thứ nên được triển khai và sẵn sàng để tiêm phụ thuộc. Đây là triển khai cuối cùng của mô-đun còn lại:


 import { getRestRepository } from '@repository/rest/rest.repository'; import { getRestApi } from '@api/rest/rest.api'; import { getDictionarySuggestionsUseCase } from '@domain/rest/rest.use-cases'; import { axiosInstance } from '@shared/axios.instance'; const restApi = getRestApi(axiosInstance); const restRepository = getRestRepository(restApi); export const restModule = { getDictionarySuggestions: getDictionarySuggestionsUseCase(restRepository).execute, };


Đúng rồi! Chúng tôi đã trải qua quá trình triển khai các nguyên tắc Kiến trúc sạch mà không bị ràng buộc vào một khuôn khổ cụ thể nào. Cách tiếp cận này đảm bảo rằng mã của chúng tôi có thể thích ứng được, giúp dễ dàng chuyển đổi khung hoặc thư viện nếu cần.


Khi nói đến thử nghiệm, kiểm tra kho lưu trữ là một cách tuyệt vời để hiểu cách các thử nghiệm được triển khai và tổ chức trong kiến trúc này.


Với nền tảng vững chắc về Kiến trúc sạch, bạn có thể viết các bài kiểm tra toàn diện bao gồm nhiều tình huống khác nhau, giúp ứng dụng của bạn trở nên mạnh mẽ và đáng tin cậy hơn.


Như đã trình bày, tuân theo các nguyên tắc Kiến trúc sạch và tách biệt các mối quan tâm dẫn đến cấu trúc ứng dụng có thể bảo trì, có thể mở rộng và có thể kiểm tra.


Cách tiếp cận này cuối cùng giúp dễ dàng thêm các tính năng mới, cấu trúc lại mã và làm việc với một nhóm trong dự án, đảm bảo thành công lâu dài cho ứng dụng của bạn.

Bài thuyết trình

Trong ứng dụng ví dụ, React được sử dụng cho lớp trình bày. Trong thư mục bộ điều hợp, có một tệp bổ sung có tên hooks.ts xử lý tương tác với mô-đun còn lại. Nội dung của tập tin này như sau:


 import { restModule } from '@adapter/rest/rest.module'; import { useAxios } from '@shared/hooks'; export const useDictionarySuggestions = () => { const { data, error, isLoading, mutate } = useAxios(restModule.getDictionarySuggestions); return { dictionarySuggestions: data, getDictionarySuggestions: mutate, dictionarySuggestionsError: error, isDictionarySuggestionsLoading: isLoading, }; };


Việc triển khai này giúp làm việc với lớp trình bày cực kỳ dễ dàng. Bằng cách sử dụng hook useDictionarySuggestions , lớp trình bày không phải lo lắng về việc quản lý ánh xạ dữ liệu hoặc các trách nhiệm khác không liên quan đến chức năng chính của nó.


Sự tách biệt các mối quan tâm này giúp duy trì các nguyên tắc của Kiến trúc sạch, dẫn đến mã dễ quản lý và bảo trì hơn.

Cái gì tiếp theo?

Trước hết, tôi khuyến khích bạn đi sâu vào mã từ repo GitHub được cung cấp và khám phá cấu trúc của nó.


Bạn còn có thể làm gì khác nữa không? Bầu trời là giới hạn! Tất cả phụ thuộc vào nhu cầu thiết kế cụ thể của bạn. Chẳng hạn, bạn có thể xem xét việc triển khai lớp dữ liệu bằng cách kết hợp kho lưu trữ dữ liệu (Redux, MobX hoặc thậm chí là một thứ gì đó tùy chỉnh - không thành vấn đề).


Ngoài ra, bạn có thể thử nghiệm các phương thức giao tiếp khác nhau giữa các lớp, chẳng hạn như sử dụng RxJS để xử lý giao tiếp không đồng bộ với phần phụ trợ, có thể bao gồm bỏ phiếu, thông báo đẩy hoặc ổ cắm (về cơ bản, được chuẩn bị cho bất kỳ nguồn dữ liệu nào).


Về bản chất, hãy thoải mái khám phá và thử nghiệm theo ý muốn, miễn là bạn duy trì kiến trúc phân lớp và tuân thủ nguyên tắc phụ thuộc nghịch đảo. Luôn đảm bảo miền là cốt lõi trong thiết kế của bạn.


Bằng cách đó, bạn sẽ tạo ra một cấu trúc ứng dụng linh hoạt và có thể bảo trì, có thể thích ứng với các tình huống và yêu cầu khác nhau.

Bản tóm tắt

Trong bài viết này, chúng ta đã đi sâu vào khái niệm Clean Architecture trong ngữ cảnh của một ứng dụng học ngôn ngữ được xây dựng bằng React.


Chúng tôi nhấn mạnh tầm quan trọng của việc duy trì kiến trúc phân lớp và tuân thủ nguyên tắc phụ thuộc nghịch đảo, cũng như lợi ích của việc tách biệt các mối quan tâm.


Một lợi thế đáng kể của Kiến trúc sạch là khả năng cho phép bạn tập trung vào khía cạnh kỹ thuật của ứng dụng mà không bị ràng buộc với một khuôn khổ cụ thể. Tính linh hoạt này cho phép bạn điều chỉnh ứng dụng của mình theo các tình huống và yêu cầu khác nhau.


Tuy nhiên, có một số nhược điểm đối với phương pháp này. Trong một số trường hợp, việc tuân theo một mẫu kiến trúc nghiêm ngặt có thể dẫn đến tăng mã soạn sẵn hoặc thêm độ phức tạp trong cấu trúc dự án.


Ngoài ra, việc dựa ít hơn vào tài liệu có thể vừa có lợi vừa có hại - trong khi nó cho phép nhiều tự do và sáng tạo hơn, nó cũng có thể dẫn đến nhầm lẫn hoặc hiểu sai thông tin giữa các thành viên trong nhóm.


Bất chấp những thách thức tiềm ẩn này, việc triển khai Kiến trúc sạch có thể mang lại nhiều lợi ích, đặc biệt là trong ngữ cảnh của React, nơi không có mẫu kiến trúc được chấp nhận rộng rãi.


Điều cần thiết là xem xét kiến trúc của bạn khi bắt đầu một dự án hơn là giải quyết nó sau nhiều năm vật lộn.


Để khám phá một ví dụ thực tế về Kiến trúc sạch đang hoạt động, vui lòng xem kho lưu trữ của tôi tại https://github.com/Levofron/NotionLingo . Bạn cũng có thể kết nối với tôi trên phương tiện truyền thông xã hội thông qua các liên kết được cung cấp trên hồ sơ của tôi.


Wow, đây có lẽ là bài viết dài nhất mà tôi từng viết. Nó cảm thấy không thể tin được!