logoMột hệ thống khó hiểu thì cũng khó thay đổi
Common Reuse Principle - CRP

Khi đề cập tới Object Oriented (OO), chúng ta thường hay nhắc đến SOLID - bộ 5 nguyên lý hướng tới thiết kế class trong lập trình hướng đối tượng. Cùng nhắc lại một chút về nguyên lý "I" trong đó.

The Interface Segregation Principle (ISP) - Nguyên lý phân tách giao diện.

Hiện nay, nhiều Java project thực hiện phân chia package theo dạng sau:

controller ---> service ---> entity

  • Package controller chứa các Controller class - đóng vai trò là Input Adapter cho ứng dụng.
  • Package service chứa các Service class - đóng vai trò Input Port khai báo chức năng của ứng dụng.
  • Package entity chứa các Entity class - đóng vai trò là các thực thể nghiệp vụ.

Các từ khóa "Input Adapter", "Input Port" là các từ khóa thuộc kiến trúc Hexagonal.

Theo cách viết này, tôi thấy thông thường lớp service sẽ được viết theo dạng: ServiceImpl implements Service.

Và trong mỗi Service interface, ta sẽ khai báo các method cho "dịch vụ" đó.

Ví dụ, đây là các method trong một OrderService:

Hình 1: Các method trong một OrderService interface.

Đây là cách ta gom tất cả các use case vào trong một giao diện. Chú ý rằng OrderServiceImpl - class thực thi giao diện OrderService, sẽ được viết theo Transaction Script pattern, khi đó, các thực thể như Order, Payment,... sẽ chỉ thuần túy là các data class. Nhưng tạm bỏ qua điều này ở đây. Cái mà ta quan tâm là OrderService interface.

Giả sử, OrderService được sử dụng cho hai use case khác nhau:

  • Use case 1: Quản lý thông tin đơn hàng. Mang tới cho khách hàng chức năng cập nhật, bổ sung, hoặc hủy đơn hàng khi có thể.
  • Use case 2: Giám sát trạng thái đơn hàng. Cung cấp các chức năng giám sát để khách hàng có thể biết được đơn hàng của mình hiện đang được xử lý ở khâu nào.

Rõ ràng, với use case 1, các method cần thiết là createOrder, getOrder, updateOrder, cancelOrder, getAllOrdersTrong khi đó, use case 2 cần các method getOrderStatus, updateOrderStatus.

Hai use case này được triển khai bởi các class Service Impl khác nhau, như trong hình 2 dưới đây:

Hình 2: Hai class thực thi OrderService cho hai use case khác nhau. Các method được bôi đỏ là các method mà service thực thi quan tâm.

Đây là hai nghiệp vụ khác nhau nhưng lại được gom vào một giao diện. Nếu chúng thay đổi với tốc độ khác nhau, nghĩa là vì lý do khác nhau ở những thời điểm khác nhau thì nghiệp vụ này sẽ ảnh hưởng tới nghiệp vụ kia. Ví dụ, thời gian đầu ta tập trung vào Use case 1 (quản lý đơn hàng), trong khi Use case 2 lại khá tĩnh. Như vậy những class sử dụng OrderService, cụ thể là OrderManagementService sẽ cần cập nhật code liên tục, thậm chí đôi khi còn bổ sung hay loại bỏ method khai báo từ OrderService. Khi đó, OrderStatusTrackingService đứng ngoài cuộc nhưng vẫn phải được chỉnh sửa code khi method được bổ sung hay loại bỏ khỏi OrderService.

Vấn đề này còn rõ hơn nếu ta xây dựng ứng dụng theo các module/component, khi đó 3 class trên được gom lại vào cùng một component nghiệp vụ order-service.jar và được sử dụng ở nhiều microservices khác nhau. Những microservices quan tâm tới quản lý thông tin order sẽ cần tái triển khai, là điều được mong đợi. Nhưng những microservices chỉ quan tâm tới trạng thái order cũng sẽ cần tái triển khai dù nó không muốn, đây là một điều phiền toái.

Do đó, ISP khuyên rằng ta nên tách OrderService ra thành hai nhóm use case riêng biệt để chúng có thể phát triển độc lập nhau. Như hình 3 dưới đây:

Hình 3: Phân tách OrderService thành hai interface nhỏ hơn, ở đó, mỗi interface phụ trách một use case riêng biệt.

Câu hỏi cho bạn: Liệu ta có thể áp dụng một interface chỉ có một method có được không? vì như vậy ta luôn đảm bảo được ISP.

Giờ cùng quay lại chủ đề chính của chúng ta: CRP.

The Common Reuse Principle (CRP)

Nguyên lý CRP phát biểu như sau:

Không ép các users của một thành phần phải phụ thuộc vào những thứ mà họ không cần.

Như vậy, CRP rất giống với ISP bên trên. Thực ra, nó chính là ISP được phát biểu cho thành phần.

Trong khi CCP khuyến khích ta tạo ra những thành phần có xu hướng to hơn về kích thước thì CRP lại có xu hướng làm cho những thành phần đó nhỏ đi, vì nó tập trung vào tính dùng lại. 

Tính dùng lại mang lại nhiều lợi ích lớn cho chúng ta - những người phát triển phần mềm, vì khi dùng lại một thành phần, ta không cần tốn nguồn lực vào việc "phát minh lại bánh xe", ta hoàn toàn tin tưởng vào chất lượng phần mềm đó vì nó còn được nhiều người khác dùng lại nữa.

Nguyên lý CRP cho ta biết hai điều sau:

1. Gom những thứ cần tái sử dụng cùng lại với nhau.

Các class trong cùng một thành phần thường có mối liên hệ mật thiết với nhau để thực hiện cùng một nhiệm vụ, do CCP quy định. Do đó, khi được tái sử dụng, chúng sẽ được tái sử dụng cùng với nhau. Điều này quyết định việc: ta cần tái tổ chức chúng để sao cho chúng được tối ưu khi tái sử dụng.

Trong ví dụ trên, giả sử ta muốn 2 use case được tái sử dụng, vậy ta cần đóng gói các class của các use case đó vào cùng với nhau.

2. Phân tách những thứ không cần tái sử dụng cùng nhau ra.

Nguyên lý CRP không chỉ cho ta biết việc gom những class cần được tái sử dụng cùng nhau lại với nhau mà nó cón cho biết nhiều hơn thế: Nó còn cho ta biết những class nào không nên cùng thuộc về một thành phần

Khi một thành phần sử dụng thành phần khác đồng nghĩa với một sự phụ thuộc được tạo ra giữa chúng. Có thể thành phần sử dụng (using component) chỉ sử dụng duy nhất một class bên trong thành phần được sử dụng (used component) - nhưng điều này không làm suy yếu sự phụ thuộc. Thành phần sử dụng vẫn có sự phụ thuộc lên thành phần được sử dụng.

Bởi vì có sự phụ thuộc như vậy, bất cứ khi nào thành phần được sử dụng thay đổi thì thành phần sử dụng sẽ có khả năng phải thay đổi tương ứng. Kể cả khi không có bất cứ sự thay đổi nào là cần thiết cho thành phần sử dụng thì nó vẫn có khả năng phải biên dịch lại, xác nhận lại, và triển khai lại. Điều này là đúng kể cả khi thành phần sử dụng không quan tâm về sự thay đổi được tạo ra bên trong thành phần được sử dụng.

Do vậy, khi phụ thuộc vào một thành phần, có nghĩa là ta muốn đảm bảo rằng ta phụ thuộc vào mọi class trong thành phần đó. Hay nói cách khác, ta muốn đảm bảo rằng các class mà ta đặt vào cùng một thành phần không thể bị phân tách - có nghĩa là sự phụ thuộc này không phải là phụ thuộc trên một số và không phụ thuộc trên số còn lại. Nếu không, ta sẽ phải triển khai lại nhiều thành phần hơn mức cần thiết, điều này làm phí phạm
nguồn lực của tổ chức. Do đó, nguyên lý CRP cho ta biết nhiều điều về việc những class nào không nên đi cùng nhau hơn là việc những class nào nên đi cùng nhau. CRP cho rằng các class không cùng ranh giới với nhau thì không nên cùng thuộc về cùng một thành phần.

Quay lại ví dụ OrderService

Trong ví dụ OrderService bên trên, ta cần phân tách thành phần này ra hai thành phần riêng biệt đáp ứng cho hai use case.

Việc này không chỉ đơn thuần là copy và paste các class từ thành phần ban đầu vào hai thành phần mới. Vì làm như vậy, ta sẽ vô tình nhân bản các class dùng chung ra hai nơi khác nhau. Sau này, có những cập nhật cần tạo trên các class đó thì ta sẽ phải thông báo cho hai team phụ trách hai thành phần cùng cập nhật một logic. Ta có hai cách làm phù hợp với hai điều kiện phát triển khác nhau:

Cách 1: Nhân bản - phù hợp với dự án gấp, mới khởi tạo

Ở giai đoạn này, thường thì ta sẽ ưu tiên việc phát triển nhanh, do đó, tính dùng lại chưa được quan tâm nhiều. Ta sẽ thiết kế dùng lại một cách "thô".

Như vậy, ta có bức tranh sau:

Hình 4: Phân tách thành phần để tối ưu dùng lại, có thể chưa tối ưu do còn duplicate một số xử lý khác.

Chú ý, với hình 4, tôi đã sắp xếp các thành phần có Chính sách cao hơn ở bên trên các thành phần có chính sách thấp hơn. Chúng ta sẽ thường xuyên làm như vậy như một quy tắc khi biểu diễn các module trong kiến trúc của chúng ta.

Khi nào sẽ tối ưu cho việc dùng lại hơn nữa? Khi dự án ở giai đoạn ổn định hơn, như dưới đây.

Cách 2: Phân tách tối ưu - phù hợp với dự án ở giai đoạn maintenance

Ở giai đoạn này, dự án đang ở giai đoạn ổn định hơn, ta biết được trong các thành phần có những gì đang bị trùng lặp. Khi đó, ta sẽ bóc tách các thứ đó ra một thành phần riêng biệt nữa mà các thành phần cũ cần phải phụ thuộc vào.

Giả sử, cả hai use case đều cần phải kiểm tra một số điều kiện trước khi cho phép order được cập nhật, và ta muốn logic validate này được dùng chung trong toàn bộ hệ thống. Ta cần tách nó ra một thành phần riêng. Như vậy, bức tranh hệ thống của chúng ta sẽ như sau:

Hình 5: Thành phần được phân tách tối ưu hơn nữa.

Kết luận

CRP là một trong 3 nguyên lý thành phần tập trung vào tính cohesion của phần mềm. CRP có mâu thuẫn với CCP vì trong khi CCP có khuynh hướng tạo ra những thành phần lớn hơn thì CRP lại có khuynh hướng phân tách chúng ra thành những thành phần nhỏ hơn.

Để tổng hợp cả CRP và ISP, ta có một phát biểu cô đọng sau:

Không phụ thuộc vào những thứ mà ta không cần.

 

Tham khảo:

Chính sách trong hệ thống phần mềm.

Common Closure Principle - CCP.

Reuse/Release Equivalence Principle - REP

Giằng co giữa REP-CCP-CRP.

Single Responsibility Principle - SRP.

Open Closed Principle - OCP.

Bình luận
Gửi bình luận
Bình luận