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.
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.
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.
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.
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:
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?
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:
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.
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:
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.
Để 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
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:
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.
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ữ.
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.
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:
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ề:
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.
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.
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.
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à đủ.
Đượ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ọ.
Để 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.
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.
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.
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.
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.
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.
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ó.
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.
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.
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.
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
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!