36. Ưu tiên cung cấp những giao diện trừu tượng

Estimated reading: 9 minutes 260 views

101 Quy tắc lập trình C++

Tóm tắt

Hãy yêu nghệ thuật trừu tượng: Các giao diện trừu tượng (abstract interfaces) giúp bạn tập trung vào việc tạo ra một sự trừu tượng đúng đắn mà không bị lẫn với các chi tiết triển khai (implementation) hay quản lý trạng thái (state management). Hãy ưu tiên thiết kế các hệ phân cấp (hierarchies) để thực thi các giao diện trừu tượng nhằm mô hình hóa các khái niệm trừu tượng.

Thảo luận

Hãy ưu tiên định nghĩa và kế thừa từ các giao diện trừu tượng. Một giao diện trừu tượng là một lớp trừu tượng (abstract class) bao gồm hoàn toàn các hàm ảo thuần túy (pure virtual functions), không có trạng thái (member data) và thường không có phần thân hàm (function implementations). Lưu ý rằng việc tránh dùng trạng thái trong các giao diện trừu tượng giúp đơn giản hóa toàn bộ thiết kế hệ thống kế thừa. (xem [Meyers96] để biết ví dụ.)

Hãy tuân theo Nguyên tắc Đảo ngược Phụ thuộc (Dependency Inversion Principle – DIP) ([Martin96a] và [Martin00]). DIP phát biểu rằng:

  • Các mô-đun cấp cao (high-level modules) không nên phụ thuộc vào các mô-đun cấp thấp (low-level modules). Ngược lại, cả hai nên phụ thuộc vào các trừu tượng (abstractions).

  • Các trừu tượng không nên phụ thuộc vào các chi tiết (details). Ngược lại, các chi tiết nên phụ thuộc vào các trừu tượng.

Tuân thủ DIP có nghĩa là các hệ phân cấp nên được bắt đầu từ các lớp trừu tượng, không phải từ các lớp cụ thể (Mục 35). Các lớp cơ sở trừu tượng chỉ nên lo việc định nghĩa chức năng, chứ không phải triển khai nó. Nói cách khác: “Hãy đẩy chính sách (policy) lên trên và đẩy phần triển khai (implementation) xuống dưới.”

Nguyên tắc Đảo ngược Phụ thuộc mang lại 3 lợi ích thiết kế cơ bản:

• Cải thiện độ bền vững: Các phần ít ổn định hơn trong hệ thống (tức phần triển khai) phụ thuộc vào các phần ổn định hơn (các trừu tượng). Một thiết kế vững chắc là thiết kế mà thay đổi chỉ ảnh hưởng cục bộ. Ngược lại, trong hệ thống dễ vỡ (fragile system), một thay đổi nhỏ có thể lan truyền theo những cách không mong đợi qua các phần khác nhau. Điều này chính là điều xảy ra khi thiết kế dựa trên các lớp cơ sở cụ thể (concrete base classes).

• Linh hoạt hơn: Các thiết kế dựa trên giao diện trừu tượng thường linh hoạt hơn. Nếu các trừu tượng được mô hình hóa đúng, ta có thể dễ dàng tạo ra các triển khai mới để đáp ứng các yêu cầu mới. Ngược lại, một thiết kế phụ thuộc vào nhiều chi tiết cụ thể sẽ trở nên cứng nhắc (rigid) — các yêu cầu mới sẽ buộc phải thay đổi cốt lõi của hệ thống.

• Tính mô-đun tốt hơn: Một thiết kế dựa vào các trừu tượng có tính mô-đun cao vì các mối phụ thuộc của nó rất đơn giản: Các phần dễ thay đổi phụ thuộc vào các phần ổn định, chứ không phải ngược lại. Ngược lại, một thiết kế trộn lẫn giao diện và chi tiết triển khai thường có các mạng phụ thuộc phức tạp (intricate webs of dependency), khiến việc tái sử dụng hoặc cắm ghép (plug-in) vào hệ thống khác trở nên khó khăn.

Một nguyên tắc liên quan gọi là “Luật Cơ hội Thứ hai” (Law of Second Chances) phát biểu rằng:

“Điều quan trọng nhất là phải làm đúng phần giao diện (interface). Mọi thứ khác có thể được sửa sau. Nếu giao diện bị sai, có thể bạn sẽ chẳng bao giờ có cơ hội sửa lại nó.”
— [Sutter04]

Thông thường, hãy chọn hàm huỷ ảo công khai để cho phép xóa đa hình (polymorphic deletion) (Mục 50), trừ khi bạn đang dùng các cơ chế quản lý bộ nhớ đặc biệt như COM hoặc CORBA.

Hãy cẩn trọng khi dùng đa kế thừa (multiple inheritance) với các lớp không phải giao diện trừu tượng. Thiết kế có đa kế thừa có thể rất biểu đạt, nhưng cũng khó làm đúng và dễ làm sai. Đặc biệt, quản lý trạng thái trở nên rất khó trong các thiết kế có đa kế thừa.

Như đã đề cập ở Mục 34, kế thừa từ một kiểu có thể gây ra liên kết tra cứu tên (name lookup coupling) — vô tình kéo theo các hàm hoặc mẫu hàm (function templates) được định nghĩa trong cùng không gian tên (namespace) của kiểu đó (Mục 58).

Ví dụ

Ví dụ: Chương trình sao lưu (Backup program):  Trong các thiết kế ngây thơ, các thành phần cấp cao (high-level components) thường phụ thuộc trực tiếp vào các chi tiết cấp thấp (low-level details). Ví dụ, một chương trình sao lưu được thiết kế kém có thể có một thành phần lưu trữ (archiving component) phụ thuộc trực tiếp vào các kiểu hoặc hàm đọc cấu trúc thư mục và các hàm khác ghi dữ liệu ra băng (tape). Khi đó, việc thích ứng chương trình này với một hệ thống tệp mới hoặc phần cứng sao lưu mới sẽ đòi hỏi tái thiết kế lớn.

Nếu logic của hệ thống sao lưu được xây dựng xung quanh các trừu tượng được thiết kế tốt của hệ thống tệp (file system) và thiết bị sao lưu (backup device), thì không cần tái thiết kế — chỉ cần thêm các triển khai mới của các giao diện trừu tượng và cắm (plug) chúng vào hệ thống. Tự nhiên mà nói, các yêu cầu mới nên được đáp ứng bằng mã mới, chứ không phải bằng việc chỉnh sửa lại mã hiện có.

Ngoại lệ

Tối ưu hóa lớp cơ sở rỗng (Empty Base Optimization – EBO) là một trường hợp mà kế thừa (thường là không công khai) được dùng chỉ vì mục đích tối ưu hóa. (Nhưng xem lại Mục 8 để biết chi tiết.)

Có vẻ như các thiết kế dựa trên chính sách (policy-based designs) khiến thành phần cấp cao phụ thuộc vào chi tiết triển khai (implementation details) — tức là các chính sách. Tuy nhiên, điều này chỉ là một dạng đa hình tĩnh (static polymorphism). Các giao diện trừu tượng vẫn tồn tại — chỉ là ẩn, chứ không được biểu diễn rõ ràng bằng các hàm ảo thuần túy.

Tham khảo

[AlexandrescuOl] • [Cargill92] pp. 12-15, 215-218 • [Cline99] §5.18-20, 21.13 • [Lakos96] §6.4.1 • [Martin96a] • [MartinOO] • [Meyers96] §33 • [StroustrupOO] §12.3-4, §23.4.3.2, §23.4.3.5, §24.2-3, §25.3, §25.6 • [SutterO4] §17

Link

Leave a Comment

Chia sẻ:

36. Ưu tiên cung cấp những giao diện trừu tượng

Or copy link

CONTENTS