Nội dung:

1. Giới thiệu.

2. Phân tích các yêu cầu.

3. TDD (Test first).

4. Services.

5. Provider.

6. Kết luận.


1. Giới thiệu:

Trong bài viết trước, chúng ta đã xây dựng một nền tảng vững chắc cho ứng dụng Blog của mình dựa trên các khái niệm về Clean Architecture. Chúng ta thiết lập các quy tắc và cấu trúc dữ liệu cho Article và Comment: các thành phần cơ bản đã được triển khai. Trong bài này, chúng ta sẽ thêm vào các thành phần Service.
Vậy "Service" là gì ? Tùy thuộc vào ngữ cảnh và người bạn đang nói chuyện, bạn có thể nhận được các định nghĩa và giải thích hoàn toàn khác nhau. Trong trường hợp này, chúng ta sẽ mạnh dạn gọi chúng là "cốt lõi của logic nghiệp vụ". Từ quan điểm kiến trúc, đó là vòng tròn tiếp theo trong sơ đồ phụ thuộc phía trên.
Từ góc độ kỹ thuật, nó có thể là bất cứ thứ gì: "class, function, cluster functions và object với các phương thức". Hãy tránh xa việc hướng sự chú ý của bạn vào "chi tiết" như: framework, store, UI....Service chỉ phục thuộc vào các Entity hoặc các Service khác.
Từ quan điểm khái niệm, mục tiêu của Service là thực hiện các hoạt động nghiệp vụ bắt buộc trên các Entity. Nếu bạn đã quen thuộc với cuốn "Clean Architecture" của Uncle Bob, bạn có thể nhận thấy một số sai lệch. Trong cuốn sách của mình, Bob Martin đã mô tả vòng tròn tiếp theo là "các use case". Tôi không cố gắng sửa đổi hay sáng tạo lại những quy tắc ấy mà chỉ đơn thuần tìm cách kết hợp các use case bên dưới các Service. Nếu bạn đã quen với mô hình Repository-Service cũng khá phổ thông thì có lẽ bạn sẽ thấy một vài nét tương đồng.

2. Phân tích các yêu cầu:

Photo by Isaac Smith on Unsplash
Hãy bắt đầu hành trình của chúng ta từ việc phân tích các yêu cầu. Blog là một ứng dụng, nhưng chúng ta sẽ không triển khai toàn bộ các tính năng đầy đủ. Giả sử nhóm phân tích nghiệp vụ của chúng ta tiếp cận vấn đề theo hướng gia tăng giá trị theo thời gian thì có lẽ sản phẩm này sẽ theo hướng MVP.
Chúng ta sẽ có những yêu cầu như sau:
* Là người dùng tôi muốn xem tất cả các bài viết trên trang chủ.
* Là người dùng tôi có thể điều hướng đến trang của một bài viết và xem những nội dung chi tiết bài bài viết đó.
* Là người dùng tôi muốn để lại comment trên mỗi bài viết tôi đọc qua.
(đây là đường dẫn cho bản demo)
Đây là những yêu cầu khá dài, nhưng hiện tại chúng ta không quan tâm đến phần UI. Chúng ta đang nghĩ về "data first". Chúng ta có thể chuyển đổi chúng thành các task về kĩ thuật như chỉ nhắm vào logic nghiệp vụ và dữ liệu:
* Triển khai cách để nhận dữ liệu về tất cả các bài biết.
* Triển khai cách để bấy được một bài viết cụ thể qua Id.
* Triển khai cách khởi tạo một Comment cho bài viết.

3. TDD (Test first):

Photo by Siora Photography on Unsplash
Mã nguồn có sẵn trong repo. Vui lòng chuyển sang banch "entities". Branch trong dự án nếu bạn đã bỏ qua bài viết trước đó. Nếu bạn đã hoàn thành phần "Entity" và mọi thức hoạt động như dự tính, chúng ta có thể bắt đầu tiếp tục cuộc hành trình.
Về cấu trúc thư mục cho "Service", chúng ta sẽ làm theo cùng một cách tiếp cận mà trước đó đã được sử dụng trong phần "Entity":
/src/services/articles
contains all Articles Services code, specifically:

/src/services/articles/articles.ts
actual Article Service

/src/services/articles/articles.types.ts
types for Article Service

/src/services/articles/articles.spec.ts
unit tests for Article Service

/src/services/articles/articles.mocks.ts
mocks for Article Service

/src/services/articles/index.ts
barrel file for Article Service

/src/services/index.ts
barrel file for all Services
Hãy tạo những file này, Chúng ta cũng cần cập nhật lại các file barrel:
File /src/services/articles/index.ts
File /src/services/index.ts
Thật tuyệt vời, đã đến lúc chúng ta viết thêm một số type và kiểm thử. Theo yêu cầu, có 3 tính năng: lấy toàn bộ bài viết, lấy ra một bài viết cụ thể và tạo comment cho một bài viết cụ thể. Hãy xác định các thành phần cho chúng:
File article.types.ts
Phương thức đầu tiên rất đơn giản: lấy và trả về tất cả các bài viết.

"getOneById" mong đợi một ID làm đối số nhận vào phương thức và trả về một bài viết cụ thể nếu nó tồn tại hoặc không sẽ trả về việc id đó không tồn tại. Và cuối cùng "createComment" sẽ yêu cầu dữ liệu đầu vào là dữ liệu của một comment và id bài viết mà comment đó được đính kèm.
Lưu ý rằng chúng ta có thể tạo một Service riêng cho Comment. Nhưng vì không có API được dựng trước (hoặc có dữ liệu mock trong file JSON) cho Comment nên chúng ta sẽ thấy nó không cần thiết phải làm như vậy. Nhưng ứng dụng thực tế thì nên tách biệt.
Bây giờ, vì Service là một dạng mã TS/JS thuần và chúng ta đã tạo ra trước đó cho các Entity như Article và Comment, nên hoàn toàn dễ dàng khi bắt tay vào viết các kiểm thử:
File ArticleService Spec
Hai kiểm thử cuối cùng đáng được chú ý. Nếu dữ liệu được cung cấp để tạo Comment không hợp lệ, thì chúng sẽ xảy ra lỗi. Service nên kiểm tra với Entity và xác thực dữ liệu đã được cung cấp, nếu không sẽ gặp lỗi. Chúng ta thấy điều này khá quan trọng cần nhấn mạnh: Xử lý lỗi thường là một phần quan trọng của ứng dụng. Các quy tắc xác thực có thể không được xác định hoặc không được kiểm soát bởi UI: React/Vue component, Redux/VueX store, Thunk/Sagas...Quy tắc xác thực chỉ có thể được quản lý bởi Entity, trong khi Service chỉ sử dụng và thực thi chúng.

4. Services:

Photo by Lefteris kallergis on Unsplash
Đã đén lúc viết mã cho Article rồi, Nhiệm vụ rõ ràng: tạo một class và 3 phương thức. Ví dụ: "getAll" - trả về dữ tất cả dữ liệu:
File article.ts
Nhưng dữ liệu là gì ? Nếu có một backend/API, thì "dữ liệu" sẽ là kết quả của một lời yêu cầu đối với API. Tuy nhiên, nếu bạn nhớ lại, chúng ta sử dụng một file cục bộ để đơn giản hóa mọi thứ về dữ liệu. Vì vậy, trong trường hợp này, dữ liệu là nội dung của file data.json. Chúng ta có thể nhúng vào file Article.ts để truy cập vào dữ liệu. Giả sử webpack được thiết lập để nhúng các file json, sẽ không có vấn đề kỹ thuật nào với điều đó có thể xảy ra. Nhưng sẽ có vấn đề về mặt kiến trúc.

Bạn thấy đấy, việc nhúng file này có nghĩa là Service phụ thuộc vào nó về mặt cấu trúc. Điều gì sẽ xảy đến nếu xảy ra thay đổi: thay vì "data.json", thì có thể thành "article.json". Thay đổi này không liên quan gì đến Service. Điều đó có nghĩa là chúng ta đã vì phạm vào nguyên tắc Single Responsiblity Principle:
"Gộp những thay đổi vì những lý do giống nhau. Và chia tách những thứ thay đổi vì những lý do khác nhau."
Thêm vào đó, hãy tưởng tượng có một vài Service sử dụng chung một dữ liệu. Có thể bạn sẽ muốn nạp dữ liệu một lần và chuyển giao nó cho tất cả các Service thay vì tiếp tục nhúng đi nhúng lại. Vậy hãy ghi nhớ điều đó, hãy tiến thêm một bước nữa trong hành tình kiến trúc của chúng ta và "nhúng - inject" dữ liệu vào Service:
Bây giờ, bất kì ai khởi tạo service, phải cung cấp một khởi tạo về dữ liệu. Trong những trường hợp phức tạp hơn, chúng ta có thể phải dùng đến Axios, RxJS hoặc bất kỳ công cụ cấp thấp nào khác để giúp lấy và vận hành khối dữ liệu.
Lưu ý: chúng ta muốn nhấn mạnh vào thật ngữ cấp thấp - low-level. Bạn không nên đưa các framework hoặc thư viện quá nặng và tích hợp nó hoặt động chặt chẽ với UI như: Redux-Saga hoặc Thunk. Làm như vậy, bạn kết hợp Service với UI/Store và điều này là một thứ mà chúng ta nên tránh. Một lần nữa: hãy giữ các service sạch sẽ và thuần túy nhất có thể.
Thật tuyệt, tất cả các mảnh ghếp đều đã có. Hãy cùng triển khai các phương thức còn lại:
Articles Service
Và giờ đã đến lúc dọn dẹp các phần kiểm thử. Chúng ta tạo một phiên bản của ArticleService và cung cấp một mock constructor:
Articles Service Spec
Chúng ta cũng tạo ra mock cho ArticleService:
Article Service Mock Interface
Article Service Mock

5. Provider:

Photo by Anunay Mahajan on Unsplash
Service của chúng ta có vẻ đã khá tuyệt rồi. Nhưng có một chút thiếu sót. Hãy tưởng tượng rằng chúng ta có một vài chỗ đáng lưu tâm trong mã nguồn của ArticleService. Để thay đổi điều đó, chúng ta phải:
* Nạp dữ liệu từ file data.json.
* Khởi tạo một đối tượng qua ArticleService và cung cấp dữ liệu cho chúng.
Đó không phải là một đường lối rõ ràng. Có thể dễ dàng tránh được việc tải dữ liệu và khởi tạo service nhiều lần nếu chúng ta có một service provider. Nó cũng là một mô hình khá phổ biến gây khó khăn cho việc khởi tạo nhiều service và hỗ trợ chúng với những dữ liệu nạp vào cần thiết. Sau đó, những trường hợp này có thể được đưa vào "consumers" như: actions, UI components, hoặc các service khá...
Ta sẽ đặt Provider trong folder gốc của Service vì Provider sẽ đáp ứng các hoạt động của các Service:
/src/services/provider.types.ts
/src/services/provider.spec.ts
/src/services/provider.mock.ts
/src/services/provider.ts
Cùng định nghĩa Provider nào:
file provider.types.ts
Ứng dụng của chúng ta chỉ chứa một Service, vì vậy Provider chỉ giữ tham chiếu đến ArticleService. Khi áp dụng vào thực tiễn, Provider sẽ có nhiều Service được đăng kí hơn.
Việc kiểm thử được thực hiện khá dễ dàng: chúng ta nên đảm bảo rằng ArticleService được khởi tạo:
Provider Spec
Bản thân Provider chỉ đơn giản là một hàm khởi tạo và trả về một tham chiếu đến tất cả các service:
Và cuối cùng, hãy khai báo một mock và cập nhật lại các type cũng như barrel file:
File /src/services/provider.types.ts
File /src/services/provider.mock.ts
File /src/services/index.ts
Và cuối cùng, mã nguồn của chúng ta sẽ được biên dịch mà không có bất kì lỗi lầm gì và tất cả các kiểm thử đều sẽ đều vượt qua với mức độ bao quát là 100%. Hướng dẫn hoàn chỉnh có thể được tìm thấy trong branch "services" trong repo này.

6. Kết luận:

Trong chương này, chúng ta đã học cách thao tác với các Entity và dữ liệu để đáp ứng cho các tính năng mà nhóm phân tích nghiệp vụ đưa ra. Chúng ta đã kiểm thử các Service và đảm bảo chúng thực hiện chính xác những gì được lập trình. Chúng ta cũng xây dựng một "cầu nối" gữa các service và bất kì thứ gì ở đầu bên kia cần sử dụng chúng (store, UI components) bằng cách sử dụng mô hình Provider. Chúng ta sẽ sử dụng và tương tác thêm với Provider trong những chương sau.
Cái hay là các Entity và Service đều hoàn toàn nằm độc lập với UI. Bạn có thể chọn React hoặc Vue, có thêm Redux/VueX hoặc không, và ứng dụng sẽ hoạt động trơn tru vì logic nghiệp vụ đều hoàn toàn tách biệt và được viết bằng các mã nguồn thuần của ngôn ngữ cũng như đã đóng gói bên trong Service và Entity.
Lần tới, chúng ta sẽ xây dựng Store và xem cách nó và Service tương tác với nhau.

Các phần khác của serial:

Part 4: VueX
Part 5: UI - Pages và Components