Khái quát về các nguyên tắc thiết kế component

Component

Một component là một đơn vị phần mềm nhỏ nhất mà có thể deploy riêng biệt. Chẳng hạn một file jar, một file war, một gem, một DLL… Trong những ngôn ngữ biên dịch, chúng là một tập hợp các tập tin binary, trong những ngôn ngữ thông dịch, chúng là một tập hợp các tập tin mã nguồn.

Các component có mối liên kết với nhau tại runtime, tạo ra khả năng hoạt động cho hệ thống phần mềm. Chúng có thể được gói cùng nhau trong một file duy nhất, hay được deploy riêng biệt và được nạp một cách dynamic. Bất chấp những điều đó, một kiến trúc tốt phải giữ được khả năng deploy các component mà không bị phụ thuộc vào component khác, và kéo theo đó, là khả năng phát triển (develop) component này mà không phụ thuộc vào component kia.

Các nguyên tắc ngưng kết component

Vậy, mã nguồn nào sẽ nằm trong component nào?

Reuse/Release Equivalence Principle – Nguyên tắc Tái sử dụng Tương đương Phát hành

Mầm mống của việc tái sử dụng là mầm mống của việc phát hành.

Những tập tin có khả năng tái sử dụng sẽ ngưng kết với nhau thành component. Chúng phải được release cùng nhau, thông qua một bản phát hành được đánh phiên bản, (và gắn với một bộ tài liệu, nếu có). Chính bản phát hành sẽ đại diện cho cái gì đang được cung cấp để tái sử dụng, bằng cách đó cả tác giả lẫn người dùng của component đều đạt được tiếng nói chung.

Common Closure Principle – Nguyên tắc Co rút Tương đồng

Ngưng kết những tập tin mã nguồn sẽ thay đổi cùng nhau, bởi cùng một tác nhân – vào cùng một component; phân lập những tập tin mã nguồn mà sẽ thay đổi vào những lúc khác nhau, bởi những tác nhân khác nhau – vào những component khác nhau.

Nói cách khác, một component không được có nhiều nguyên nhân để dẫn đến thay đổi. Trên phạm vi một hệ thống phần mềm, khả năng bảo trì quan trọng hơn nhiều so với khả năng tái sử dụng. Nếu phần mềm buộc phải thay đổi, thay đổi đó chỉ nên phải diễn ra trong một component duy nhất thay vì phân tán ra khắp hệ thống. Vậy nên những gì sẽ cùng nhau thay đổi để phục vụ cho một nhu cầu thì nên ngưng kết lại với nhau. Điều đó sẽ giúp giảm thiểu nỗ lực và rủi ro khi phát triển và phát hành component.

Common Reuse Principle – Nguyên tắc Tái sử dụng Tương đồng

Không được bắt buộc người dùng của một component phải phụ thuộc vào những thứ họ không cần đến.

Mã nguồn hiếm khi được tái sử dụng một cách riêng rẽ, mà chúng phụ thuộc lẫn nhau. Những tập hợp mã nguồn càng phụ thuộc vào nhau nhiều thì càng có xu hướng được tái sử dụng cùng nhau, cùng một lúc, với cùng một mục đích. Common Reuse Principle phát biểu rằng chúng ta nên ngưng kết những mã nguồn như thế trong cùng một component.

Quan trọng hơn, nguyên tắc này giúp chúng ta nhận thức được những mã nguồn nào KHÔNG nên được kết tụ. Khi một component (client) sử dụng đến một component khác (server), một mối quan hệ phụ thuộc sẽ hình thành. Bất kỳ sự thay đổi nào ở server cũng có thể gây ảnh hưởng đến client. Vậy nên chúng ta sẽ muốn server chỉ chứa duy nhất những gì mà client cần sử dụng đến. Điều đó giúp bảo vệ client khỏi những ảnh hưởng không liên quan đến nó.

Nói cách khác, những mã nguồn không thường được tái sử dụng cùng nhau thì không nên kết tụ trong cùng một component. Nguyên tắc này rất giống với nguyên tắc phân tách interface: đừng để bị phụ thuộc vào những gì không dùng đến.

Bộ ba lực ngưng kết component

Có thể để ý thấy rằng mỗi nguyên tắc trong ba nguyên tắc ngưng kết component sẽ đối nghịch với hai nguyên tắc còn lại. Chẳng hạn REP và CCP làm component lớn hơn, nhưng CRP lại đòi hỏi component nhỏ đi. Những mâu thuẫn như thế chính là vấn đề mà công việc kiến trúc phần mềm phải giải quyết.

Sơ đồ dưới đây thể hiện lực tương tác giữa ba nguyên tắc. Mỗi cạnh của tam giác – mỗi đường nối – sẽ thể hiện cái giá phải trả khi nhà thiết kế bỏ qua nguyên tắc ở phía đối diện.

Các nhà thiết kế sẽ phải đặt lực căng giữa các cạnh ở mức độ phù hợp với tình huống hiện tại của hệ thống, nhưng đồng thời cũng để ngỏ khả năng thay đổi tỉ mức đó trong tương lai, khi dự án phát triển hệ thống phần mềm bước sang những giai đoạn mới. Lấy ví dụ, vào giai đoạn đầu của dự án, CCP nên được ưu tiên hơn nhiều so với REP, bởi lúc đó quá trình phát triển quan trọng hơn nhiều so với tái sử dụng. Sau đó, qua thời gian, sức chú ý của nhà thiết kế sẽ chuyển dần từ bên phải sang bên trái của biểu đồ.

Bắt cặp component

Mục này sẽ đưa ra một mô hình để đánh giá và ra quyết định cho mối quan hệ giữa các component. Các lực ảnh hưởng đến cấu trúc giữa các component bao gồm kỹ thuật, chính sách (nghiệp vụ) và nguy cơ biến động.

Acylic Dependencies Principle – Nguyên tắc Phụ thuộc Phi vòng

Nguyên tắc này giải quyết một vấn đề phổ biến trong quá trình phát triển phần mềm: các lỗi không lường trước gây ra do sự thay đổi hành vi bất ngờ tới từ các gói phụ thuộc. Nguyên tắc rất đơn giản:

Không được phép có phụ thuộc vòng xuất hiện trên sơ đồ phụ thuộc.

Với nguyên tắc REP, các component sẽ cần được đánh phiên bản khi phát hành. Khi kết hợp với nguyên tắc ADP ở đây, ngay khi có vấn đề tới từ component phụ thuộc, chúng ta luôn có thể lùi phiên bản component đang sử dụng lại và vấn đề tự động được giải quyết.

Hệ quả của phụ thuộc vòng

Nếu như có phụ thuộc vòng, việc lùi phiên bản component kéo theo việc lùi phiên bản của tính năng chính, điều này rõ ràng là không khả thi. Trên thực tế, việc này tương đương với ngưng kết các component lại với nhau và tạo ra một “búi” component khổng lồ, khiến cho việc phát triển và phát hành các component trở nên khó khăn hơn rất nhiều.

Khử phụ thuộc vòng

Chúng ta có thể khử phụ thuộc vòng giữa các component bằng cách áp dụng Nguyên tắc Đảo ngược Phụ thuộc để đảo chiều hướng phụ thuộc giữa các component, qua đó thực hiện được mục tiêu “khử vòng”.

Một cách khác là chèn vào vòng phụ thuộc một component mà cả hai đầu vòng đều phụ thuộc vào.

Sự chập chờn của cấu trúc component

Cách khử phụ thuộc vòng thứ hai cho chúng ta thấy một sự thật là cấu trúc hệ thống có thể thay đổi lập tức khi requirement của hệ thống có thay đổi. Thực tế là như vậy, cấu trúc sẽ biến đổi và mở rộng dần theo thời gian. Vậy nên cấu trúc component cần liên tục được theo dõi để phát hiện các phụ thuộc vòng. Một khi phụ thuộc vòng xuất hiện, nó cần được khử, bằng cách nào đó.

Thiết kế từ trên xuống

Vấn đề đằng sau ADP dẫn chúng ta tới một sự thật không thể chối bỏ đó là không thể nào mà lên cấu trúc hệ thống theo hướng từ trên xuống được. Cấu trúc không phải là thứ đầu tiên của hệ thống được thiết kế, thay vào đó nó tiến hóa theo sự phát triển và thay đổi của phần mềm.

Rất nhiều người sẽ không thể làm quen được với tổng kết này ngay lập tức. Trong tâm tưởng của phần lớn chúng ta, các cấu trúc phân rã của hệ thống (component) chẳng hạn sẽ đại diện cho một tính năng phân rã nào đó.

Thật không may, cấu trúc phụ thuộc giữa các component chẳng mấy chiều theo sự phân rã tính năng của hệ thống phần mềm. Thay vào đó chúng phụ thuộc nhiều hơn vào khả năng dựng và bảo trì của phần mềm. Đây chính là lý do tại sao chúng ta không thiết kế phần mềm ngay từ đầu — không có gì để dựng và bảo trì vào lúc đó cả. Nhưng theo thời gian, chúng ta sẽ muốn các thay đổi càng gây ra ít ảnh hưởng càng tốt, nên chúng ta sẽ bắt đầu để ý đến SRP và CCP.

Chúng ta cũng sẽ cố gắng cấu trúc để cô lập và bảo vệ các thành phần chứa chính sách cấp cao khỏi các thay đổi tới từ các component khác. Chẳng hạn chúng ta sẽ không muốn các thay đổi tới từ GUI gây ảnh hưởng tới các class chứa quy tắc nghiệp vụ.

Một khi hệ thống phần mềm đã lớn kha khá, chúng ta sẽ quan tâm tới khả năng tái sử dụng của các thành phần. Đây là lúc CRP tham gia vào bàn hoạch định. Và cuối cùng, bất cứ khi nào có các phụ thuộc vòng xuất hiện, chúng ta áp dụng ADP, đó sẽ là lúc cấu trúc của chúng ta xuất hiện chớp nháy, và mở rộng.

Nếu chúng ta cố gắng thiết kế, trong khi chưa có bất kỳ thiết kế class nào, chúng ta sẽ không có thông tin gì về các khối đóng kín, không biết về các khối tái sử dụng, chúng ta sẽ gần như gây ra các phụ thuộc vòng ở khắp nơi. Vậy nên mới có phát biểu rằng cấu trúc phụ thuộc của các component mở rộng và tiến hóa dần theo thiết kế logic của hệ thống.

Stable Dependencies Principle – Nguyên tắc Phụ thuộc Hướng vào Khối ổn định

Hướng sự phụ thuộc vào những component ổn định.

Trong hệ thống, bởi sự định hình bởi CCP, luôn có những component trông như được thiết kế để dễ bị thay đổi hơn những component khác – không được hướng phụ thuộc về phía chúng. Có như thế thì các component đó mới có khả năng dễ thay đổi một cách an toàn.

Tính ổn định

Tính ổn định ở đây được hiểu là “khả năng dễ dàng thay đổi”. Một component càng khó thay đổi thì được coi là có tính ổn định càng cao.

Có nhiều yếu tố ảnh hưởng đến tính ổn định của component, chẳng hạn kích thước, độ phức tạp, sự rõ ràng trong sáng của mã nguồn, và nhiều yếu tố khác… Nhưng trong số đó có một yếu tố đặc biệt quan trọng đó là số lượng phụ thuộc hướng đến nó. Càng nhiều phụ thuộc hướng đến một component, nỗ lực bỏ ra để thay đổi component đó càng lớn.

Trong trường hợp ngược lại, một component càng ít bị component khác phụ thuộc vào (và dĩ nhiên sẽ phần lớn hướng phụ thuộc vào component khác) thì càng ít ổn định và dễ thay đổi.

Chỉ số ổn định

Chúng ta có thể đo đếm mức ổn định của một component bằng một chỉ số gọi là chỉ số bất ổn định. Theo đó:

Chỉ số bất ổn định (Instable) = Số phụ thuộc hướng ra ngoài / ( Số phụ thuộc hướng ra ngoài + Số phụ thuộc hướng vào trong)

Giá trị của chỉ số bất ổn sẽ nằm trong khoảng từ 0 ~ 1. Giá trị 0 có nghĩa là component rất ổn định, và giá trị 1 có nghĩa là component rất dễ dàng thay đổi.

Không phải mọi component đều phải ổn định

Trong một hệ thống, nếu component nào cũng có độ ổn định cao thì không ổn, chúng ta muốn hệ thống phần mềm phải luôn sẵn sàng để nâng cấp và bảo trì, do đó phải tồn tại những component có độ ổn định thấp. Lý tưởng nhất, cấu trúc phụ thuộc của hệ thống trông sẽ như sau:

Abstract Component

Đôi khi trong hệ thống xuất hiện cấu trúc như sau, nó vi phạm nguyên tắc SDP ở đây. Component flexible được thiết kế để thay đổi thường xuyên, nhưng nó lại có mức ổn định quá cao, không những thế, các thay đổi trên nó còn gây ra sự thay đổi trên những component ổn định – vốn rất khó thay đổi.

Một trong những phương thức phổ biến để cải tiến thiết kế này đó là áp dụng DIP. Bằng cách tạo ra một component API chứa những interface mà stable phụ thuộc đến, sau đó khiến cho flexible implement những interface đó. API sẽ rất ổn định, và các mối phụ thuộc khác đều chỉ hướng vào nó.

Điều đặc biệt ở đây là component API sẽ chỉ chứa interface. Trông lạ lẫm nhưng thật ra điều này rất phổ biến. Java Server API là một ví dụ. Những API component hoàn toàn trừu tượng, nên chúng rất ổn định, và là những mục tiêu rất tốt để hướng phụ thuộc vào chúng.

Stable Abstractions Principle – Nguyên tắc Trừu tượng Ổn định

Một component cần có mức trừu tượng tương xứng với mức ổn định của nó.

Một số thành phần trong hệ thống phần mềm không thường xuyên thay đổi. Chúng thể hiện các kiến trúc và chính sách cấp cao. Chúng ta không muốn chúng bị ép buộc phải thay đổi (do sự thay đổi tới từ những thành phần cấp thấp). Do đó chúng ta sẽ đặt các thành phần này vào những component có độ ổn định cao (chỉ số bất ổn thấp).

Tuy vậy, điều này tạo ra một hệ quả xấu là đánh mất sự uyển chuyển. Do được đặt vào các component rất ổn định, các chính sách cấp cao của hệ thống trở nên khó thay đổi. Câu hỏi ở đây là làm sao để các component có độ ổn định cao vẫn giữ được khả năng thay đổi uyển chuyển? Câu trả lời nằm ở Nguyên tắc Đóng/Mở, thứ cho phép một hệ thống có thể được phát triển và bảo trì thường xuyên thông qua việc mở rộng thay vì phải sửa đổi. Thứ cho phép chúng ta đáp ứng được nguyên tắc này là các abstract class.

Nguyên tắc SAP theo đó đặt ra một mối quan hệ giữa tính ổn định và tính trừu tượng của một component. Những component có độ ổn định cao thì cũng phải có mức trừu tượng đủ cao để không hạn chế khả năng mở rộng của chúng. Đổi lại những component có độ ổn định thấp thì cũng phải có mức chi tiết đủ cao để cho phép chúng thay đổi một cách nhanh chóng.

Khi kết hợp nguyên tắc SAP và SDP, chúng ta có được một hệ quả là các phụ thuộc hướng về phía các cấu trúc trừu tượng. Trông có vẻ tương tự với phát biểu của nguyên tắc DIP, nhưng thực tế đằng sau sẽ hơi khác một chút, bởi component khác với class, do component là một tập hợp kết tụ của các class, interface và các thành phần mã nguồn khác, chúng ít khi trừu tượng hay chi tiết một cách thuần túy mà thường là có cả hai tính chất cùng một lúc.

Chỉ số trừu tượng

Độ trừu tượng của component được thể hiện thông qua một chỉ số được gọi là mức trừu tượng có dải giá trị nằm trong khoảng từ 0 ~ 1, được tính bằng công thức:

Mức trừu tượng (Abstractness) = Số lượng abstract class và interface / Tổng số lượng class

Giá trị A bằng 0 sẽ cho biết rằng component hoàn toàn không có sự trừu tượng. Và A bằng 1 cho biết rằng component chỉ thuần túy chứa các class trừu tượng (và interface).

Đường hiệu dụng

Nếu biểu diễn chỉ số mức trừu tượng và mức bất ổn của các component lên hệ tọa độ như dưới đây, chúng ta có thể chia phân bố của các component làm ba khu vực chính:

  • Khu vực của các component trừu tượng và bất ổn: những component này thuần túy phụ thuộc vào những component khác (mức bất ổn cao), nhưng cũng thuần túy trừu tượng, chúng không phục vụ bất kỳ mục đích nào cho mục tiêu hoạt động của hệ thống phần mềm, thuần túy vô dụng.
  • Khu vực của các component ít trừu tượng và ổn định: những component này chứa nhiều chi tiết cấp thấp (mức trừu tượng thấp) và là những thành phần hay phải sửa đổi. Tuy vậy chúng có độ ổn định rất cao, được (bị) rất nhiều component khác phụ thuộc vào, do đó sửa đổi chúng đòi hỏi một lượng nỗ lực khổng lồ. Sẽ rất vất vả khi làm việc với những component trong vùng này.
  • Khu vực xung quanh đường hiệu dụng: khu vực này có mức trừu tượng tương xứng với độ ổn định. Đặc biệt, ở hai đầu của đường hiệu dụng là khu vực mà phần lớn nhà thiết kế sẽ cố gắng đặt phần lớn component trong hệ thống vào đó.

Kết luận

Chúng ta thậm chí có thể đi xa hơn với đường hiệu dụng bằng cách đo đếm khoảng cách của các component tới đường hiệu dụng. Về cơ bản, khoảng cách đó càng tiệm cận với 0 thì càng tốt. Mô hình này cho phép chúng ta đo đếm sự phù hợp của thiết kế của chúng ta với một mẫu thiết kế về mức phụ thuộc và mức trừu tượng có thể coi là “tốt”. Tất nhiên khi quy kết các chi tiết thiết kế về một chỉ số đo duy nhất thì sẽ có những chi tiết bị làm mờ và che dấu đi, nhưng việc cố gắng cải thiện chỉ số đó vẫn tạo ra các hệ quả tốt trên thiết kế.

Loading

Leave a Reply

Your email address will not be published. Required fields are marked *