logoMột hệ thống khó hiểu thì cũng khó thay đổi
Reuse/Release Equivalence Principle - REP

Ở phần bàn về nguyên lý CCP, chúng ta có một ví dụ đơn giản về hệ thống e-commerce, trong đó bao gồm một số thành phần chính như trong hình dưới đây:

Hình 1: Các thành phần chính làm nên hệ thống e-commerce.

Các thành phần này đã gom những thứ (những class) thay đổi vì cùng lý do và cùng thời điểm lại với nhau vào bên trong nó. Việc này là để tăng khả năng dùng lại, tăng khả năng cô lập sự thay đổi trong một phạm vi hữu hạn. Nhưng việc tạo ra những thành phần như vậy không thể hoạt động được nếu ta thiếu tên của thành phần, phiên bản của nó và những mô tả cho sự thay đổi tại một phiên bản nhất định. Thiếu những thông tin này khiến ta không phân biệt được các thành phần với nhau và ngay cả một thành phần với chính nó.

Định nghĩa.

Chính vì thế, Robert C. Martin đã đưa ra một nguyên lý chính thức cho điều này:

Đơn vị phát hành phải là đơn vị tái sử dụng, và tái sử dụng cần được quản lý qua các phiên bản phát hành.

Nguyên lý trên nhấn mạnh vào ý: Đơn vị tái sử dụng Reuse tương đương với đơn vị phát hành Release.

Hình 2: Reuse tương đương với Release.

Reuse là gì?

Reuse - tái sử dụng là khi một class hay một component hoàn chỉnh được sử dụng lại ở nhiều nơi. Việc này không đơn giản. Những thứ đi cùng nhau trong một đơn vị reuse đó phải đảm bảo tính cohesion cao, tức là chúng phải cùng hướng về một mục đích nghiệp vụ.

Code không nên được tái sử dụng bằng cách copy một class và paste nó vào các nơi khác. Nếu tác giả của class thực hiện cập nhật code, ta sẽ không thể nhận được các cập nhật đó một cách tự động. Khi đó ta sẽ phải xem xét chi tiết xem những gì đã thay đổi, sau đó thực hiện vào bản sao của ta. Như vậy, code của ta và của tác giả sẽ dần khác nhau.

Release là gì?

Trong bối cảnh này, tức việc sử dụng lại phần mềm, thì Release ám chỉ 3 thứ:

  1. Đóng gói (packaging): Mục đích của đóng gói là để tái sử dụng hiệu quả, các class cần được gom lại thành một đơn vị có thể phát hành được. Điều này có thể lạ nếu bạn làm việc trong một dự án xây dựng theo kiểu nguyên khối (monolith). Trong đó, bạn chỉ thường xuyên dùng lại các thư viện được đóng gói qua khai báo ở Maven, Gradle. Nhưng có bao giờ bạn nghĩ mình cần đóng gói các thư viện nghiệp vụ xử lý của mình không?
  2. Phiên bản (versioning): Phiên bản giúp người dùng theo dõi những thay đổi, cập nhật hoặc cải tiến trong các phiên bản phát hành của thành phần. Mỗi khi đơn vị tái sử dụng được thay đổi, một phiên bản mới được phát hành để người dùng có thể lựa chọn nâng cấp hoặc giữ lại phiên bản cũ.
  3. Mô tả (change logs): Change logs cung cấp một bản tóm tắt về những thay đổi, cập nhật, sửa lỗi trong phiên bản phát hành. Điều này giúp cho người dùng hiểu rõ hơn về những gì đã được thêm vào hoặc sửa chữa, từ đó họ có thể quyết định có nên nâng cấp lên phiên bản mới hay không. Change logs cũng rất hữu ích cho các nhà phát triển trong việc theo dõi lịch sử phát triển của component.

Equivalence là gì?

Tại sao Reuse lại tương đương với Release? Cụ thể là tại sao Reuse chính là Release và Release cũng chính là Reuse? Để làm rõ điều này, ta đi vào chính các khía cạnh vừa nói: Reuse, Packaging, và Versioning.

Một thành phần phần mềm chỉ thực sự được tái sử dụng hiệu quả khi nó được đóng gói và phát hành chính thức như một đơn vị độc lập. Bất kỳ một đơn vị nào được tạo ra để tái sử dụng cũng cần có quy trình phát hành riêng, được quản lý như một đơn vị độc lập để người dùng dễ dàng tích hợp và quản lý. Do đó, một đơn vị muốn được tái sử dụng thì nó phải luôn sẵn sàng cho việc đóng gói và phát hành.

Trong một máy tính cá nhân, bạn có CPU, RAM, Hard Disk,... Các thành phần này đều được coi như một module của máy tính. Và do là module nên chúng có khả năng bị thay thế bởi một module khác hoặc được mang đi để chạy ở một máy tính khác. Để làm được điều đó, nhà sản xuất phải gom các thành phần bên trong của Hard Disk lại trong một hình dáng, chìa ra một giao diện tích hợp, và đặt cho nó một cái tên cùng phiên bản tương ứng.

Việc đánh phiên bản khi phát hành cùng change logs giúp tăng khả năng tái sử dụng của thành phần vì qua những thông tin này, người dùng biết được liệu phiên bản mới này có còn tương thích với hệ thống của họ không. Ví dụ, Intel trong những năm gần đây phát hành các con CPU mới nhưng đòi hỏi socket khác nhau. Họ sẽ mô tả điều đó để cho người tiêu dùng biết rằng nếu sử dụng CPU mới này, họ sẽ cần mua một Mainboard mới chứ không dùng được Mainboard cũ nữa.

Như vậy, Reuse cũng chính là Release và Release cũng chính là Reuse.

Một đơn vị phần mềm không cần tái sử dụng, ta không cần đóng gói và phát hành nó.

Nguyên lý này là rất rõ ràng nhưng có một số điều bản thân nó không thể làm rõ được:

  • Những gì nên được gom lại trong cùng một thành phần để có thể phát hành cùng nhau được? Điều này được giải quyết bởi CCP.
  • Những gì nên được gom lại để tối ưu cho việc tái sử dụng? Điều này được giải quyết bởi CRP.

Tuy vậy, việc vi phạm REP lại rất dễ phát hiện và các người dùng của ta sẽ nhanh chóng báo cho ta biết.

Vi phạm REP.

Vi phạm REP thể hiện ở chính hai yếu tố tương đương nhau trong bản thân nó: Tái sử dụng và phát hành. Khi đó, các đơn vị trong thành phần không được đóng gói và quản lý đúng cách dẫn đến việc tái sử dụng gặp nhiều khó khăn.

Thiếu phiên bản. Khi phát hành một thành phần không có phiên bản rõ ràng, thể hiện ở việc không có mô tả rõ ràng ở change logs, người dùng sẽ không thể có đủ thông tin để xem phiên bản này có tương thích với ứng dụng của họ không. Do đó, họ sẽ báo lại cho ta.

Đóng gói thiếu hợp lý. Thành phần được ta gom lại những gì mà ta làm, bất kể chúng có liên hệ chặt chẽ với nhau hay không. Tức là ta đang vi phạm CCP. Người dùng sẽ thắc mắc rằng tại sao ta lại gom những thứ đó lại với nhau, liệu rằng phiên bản này có tương thích với ứng dụng của họ không vì trong này chứa quá nhiều thứ lộn xộn.

Ví dụ về một thư viện thanh toán bao gồm cả lớp xử lý thanh toán và lớp xử lý email thông báo.

Ở version 1.0, luồng xử lý cho người dùng có quyền lựa chọn chỉ cần xử lý thanh toán và gửi email thông báo là một lựa chọn.

Ở version 2.0, luồng xử lý bắt buộc gửi email thông báo sau khi xử lý thanh toán. Version này không có mô tả thay đổi này trong change logs.

Team B quản lý Payment Service đã sử dụng version 1.0 rất phù hợp. Khi Team C phát hành version 2.0 như trên, các tình huống sau sẽ có thể xảy ra:

  • Team B không thấy mô tả rõ ràng về thay đổi, họ thông báo lại với Team C.
  • Team B vội vàng tích hợp vào Payment Service và họ thấy rằng luồng xử lý bắt buộc phải gửi email dù muốn dù không. Họ báo lại với Team C.

Như vậy, khách hàng, hay người dùng của thành phần mà ta phát hành sẽ là những người thông báo lại vấn đề về sản phẩm của ta ngay nếu ta không thấy được các vấn đề đó.

Đứng ở ngã ba đường.

Cuộc sống rất nhiều khi đẩy ta đến các ngã ba lựa chọn. Ta cần phải đi đường nào đây? Và trong thiết kế phần mềm cũng tương tự, rất nhiều khi bạn phải đấu tranh xem đi theo cách nào sẽ tốt hơn về cái gì, xấu hơn về cái gì. Luôn là sự đánh đổi, nhưng sự đánh đổi sẽ mang lại lợi ích nếu quyết định đưa ra phù hợp hơn với tình huống gặp phải.

Với REP, người dùng sản phẩm, tức thành phần, của ta sẽ có những lúc đứng ở ngã ba như vậy. Khi ta thông báo một phiên bản mới, họ sẽ cẩn thận dò tìm, đọc kỹ từng mô tả của ta, thậm chí nếu có điều kiện thì trao đổi trực tiếp với ta để rõ ràng mọi thay đổi mà ta đã thực hiện trong phiên bản đó. Sau khi hiểu thấu, họ sẽ cần đưa ra lựa chọn: ở lại phiên bản cũ hay đi lên phiên bản mới này?.

Hãy cũng đi qua một ví dụ.

Giả sử ta có hai team: Team A phụ trách Component A, và Team B phụ trách Component B.

Trong Component A, sử dụng thư viện Component B do cần dùng chức năng trong đó.

Hình 3: Trục thời gian mô tả quá trình phát hành của hai thành phần và sự phụ thuộc của Component A vào Component B.

Trục thời gian mô tả 4 thời điểm từ t1 -> t3

Tại thời điểm t1, Component A (version 2.1) sử dụng Component B (version 1.0) và họ triển khai thành công.

Tại thời điểm t2, Component B được phát hành với version 2.0. Lúc này, Team A đọc change logs và thấy rằng những thay đổi đó họ không cần và họ quyết định không sử dụng phiên bản mới này.

Sau đó, Component B tiếp tục phát hành nhiều phiên bản khác nữa và Team A đều thấy các thay đổi đó hoặc là họ không cần hoặc là những thay đổi đó ảnh hưởng tới logic của họ, họ không cập nhật.

Tại thời điểm t3, Team A tiếp nhận yêu cầu mới cho Component A mà họ phụ trách, những thay đổi này lại chứa cả logic mà họ đang sử dụng ở Component B version 1.0 họ đang dùng. Họ cần làm gì? Sau đây là một số lựa chọn:

  1. Team A trao đổi với Team B về những yêu cầu này xem có phiên bản nào sau 1.0 đáp ứng được yêu cầu hay không. Hoặc Team A có thể chủ động rà soát từng phiên bản mà Team B phát hành cho Component B qua từng Change Logs. Nếu tìm được một phiên bản phù hợp, Team A có thể lựa chọn. Nhưng dù sao, điều này chỉ là giải pháp tình thế vì không có gì đảm bảo rằng tương lai sẽ không có những cập nhật tương tự.
  2. Team A yêu cầu Team B phát hành một phiên bản cập nhật. Đây là điều có thể khả dĩ nhất và đảm bảo được tương lai nhất. Hiện tại Component B có version 8.5. Họ có thể sẽ tiếp nhận yêu cầu mới từ Team A và phát hành phiên bản mới version 8.6.
  3. Team A fork từ Component B ra một phiên bản riêng và phát triển. Lúc này, Team A là owner của thành phần mới này, tạm đặt tên là Component BA, họ sẽ hoàn toàn chịu trách nhiệm cho nó. Cách này khiến cho logic giữa Component B và Component BA có thể có những chồng chéo lên nhau. Có lẽ, Team A và Team B cần ngồi lại bóc tách xem những gì dùng chung thì gom lại thành một component mới và hai component B và BA sẽ sử dụng lại. Như hình 4 dưới đây.
  4. Tìm giải pháp thay thế Component B hoặc thay đổi yêu cầu. Đây rõ ràng là cách cuối cùng.
Hình 4: Thay vì phát triển một Component BA hoàn toàn tách biệt với Component B, Team A và B cùng xác định những đơn vị dùng chung và gom chúng lại thành một thành phần dùng chung cho cả Component BA và Component B, gọi là Component BA-B Common.

Để kết phần này, tôi nhắc lại câu nói của Uncle Bob:

Ta đang sống trong thời kỳ của phần mềm tái sử dụng - là sự hoàn thiện cho lời hứa hẹn nguyên thủy nhất của lập trình hướng đối tượng.

Nguyên lý REP, CCP, và các nguyên lý thành phần khác giúp ta hiện thực hóa lời hứa hẹn này.

 

Tham khảo:

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

Common Closure Principle - CCP.

Common Reuse Principle - CRP.

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