22. Tối thiểu những định nghĩa phụ thuộc. Tránh những phụ thuộc qua lại/ tuần hoàn.

Estimated reading: 8 minutes 275 views

Tóm tắt

Tránh phụ thuộc quá mức: Đừng #include toàn bộ định nghĩa khi một khai báo chuyển tiếp (forward declaration) là đủ.

Tránh phụ thuộc vòng lặp: Sự phụ thuộc vòng lặp xảy ra khi hai module phụ thuộc trực tiếp hoặc gián tiếp vào nhau. Một module là một đơn vị phát hành có tính kết nối chặt chẽ (xem Không gian tên); nếu các module phụ thuộc lẫn nhau, chúng không còn là các module riêng lẻ nữa mà bị “dính chặt” lại thành một khối lớn hơn.

Sự phụ thuộc vòng lặp phá vỡ tính module, gây khó khăn cho các dự án lớn. Hãy tránh chúng.

Thảo luận

Ưu tiên khai báo chuyển tiếp (Forward Declaration) trừ trường hợp bạn thật sự cần định nghĩa kiểu. Bạn chỉ cần định nghĩa đầy đủ của một lớp (ví dụ class C) trong hai trường hợp chính:

  • Khi cần biết kích thước của một đối tượng C. Ví dụ: Khi cấp phát C trên stack hoặc sử dụng C làm thành viên trực tiếp của một kiểu (lớp) khác.
  • Khi cần gọi hoặc sử dụng thành viên của C. Ví dụ: Khi gọi một hàm thành viên của C.

Nếu không thuộc hai trường hợp trên, hãy dùng forward declaration thay vì #include để tránh phụ thuộc không cần thiết. → Chi tiết kèm Ví dụ

Theo đúng mục tiêu của cuốn sách này, chúng ta sẽ bỏ qua ngay từ đầu những phụ thuộc vòng lặp gây ra lỗi khi biên dịch, vì bạn đã xử lý được chúng bằng cách làm theo các lời khuyên đúng đắn đã được trình bày trong tài liệu và ở Mục 1. Thay vào đó, chúng ta sẽ tập trung vào những phụ thuộc vòng lặp vẫn tồn tại trong mã có thể biên dịch được, tìm hiểu chúng ảnh hưởng đến chất lượng mã ra sao, và các bước cần thực hiện để tránh chúng.

Nói chung, các mối phụ thuộc và vòng lặp phụ thuộc nên được xem xét ở cấp độ module. Một module là một tập hợp chặt chẽ các lớp và hàm được phát hành cùng nhau (xem Mục 5 và phần Không gian tên). Ở dạng đơn giản nhất, một vòng lặp phụ thuộc xảy ra khi hai lớp phụ thuộc trực tiếp lẫn nhau:

				
					class Child; // Phá vỡ vòng lặp

class Parent {
    Child* myChild_; 
};

class Child { // Có thể ở 1 header khác
    Parent* myParent_; 
};				
			

Lớp Parent và lớp Child phụ thuộc lẫn nhau. Mặc dù đoạn mã này vẫn có thể biên dịch được, nhưng nó đã đặt nền móng cho một vấn đề nghiêm trọng: hai lớp này không còn độc lập nữa, mà đã trở thành phụ thuộc lẫn nhau. Điều này không nhất thiết là xấu, nhưng chỉ nên xảy ra khi cả hai đều thuộc cùng một module (được phát triển bởi cùng một người hoặc nhóm, và được kiểm thử cũng như phát hành như một thể thống nhất).

Ngược lại, hãy xem xét trường hợp: nếu Child không cần lưu liên kết ngược trở về đối tượng Parent thì sao? Khi đó, Child hoàn toàn có thể được phát hành như một module riêng biệt, nhỏ gọn hơn (và thậm chí có thể mang một tên khác), độc lập hoàn toàn với Parent — rõ ràng đây là một thiết kế linh hoạt hơn nhiều.

Mọi thứ còn tệ hơn nữa khi các vòng lặp phụ thuộc lan rộng ra nhiều module, khiến tất cả chúng bị gắn chặt lại với nhau bởi “keo phụ thuộc”, tạo thành một khối nguyên khối duy nhất khi phát hành. Đó là lý do vì sao các vòng lặp phụ thuộc là kẻ thù nguy hiểm nhất của tính module.

Để phá vỡ các vòng lặp này, hãy áp dụng Nguyên lý Đảo ngược Phụ thuộc (Dependency Inversion Principle – DIP) được trình bày trong [Martin96a] và [Martin00] (Mục 36): Đừng để các module cấp cao phụ thuộc vào module cấp thấp; thay vào đó, hãy để cả hai cùng phụ thuộc vào các lớp trừu tượng. Nếu bạn có thể định nghĩa các lớp trừu tượng độc lập cho Parent hoặc Child, vòng lặp phụ thuộc sẽ bị phá vỡ. Còn nếu không thể, bạn phải đảm bảo tạo ra chúng trong cùng một module. → Chi tiết kèm Ví dụ

Một dạng phụ thuộc đặc biệt mà một số thiết kế gặp phải là phụ thuộc bắc cầu vào các lớp dẫn xuất (transitive dependency on derived classes), xảy ra khi một lớp cơ sở phụ thuộc vào tất cả các lớp con của nó, dù là trực tiếp hay gián tiếp. Một số triển khai theo mẫu thiết kế Visitor (Visitor design pattern) dẫn đến loại phụ thuộc này. Loại phụ thuộc như vậy chỉ có thể chấp nhận được khi hệ phân cấp kế thừa cực kỳ ổn định. Còn nếu không, bạn nên xem xét thay đổi thiết kế; ví dụ, có thể sử dụng mẫu thiết kế Acyclic Visitor (Acyclic Visitor design pattern) được đề xuất trong [Martin98]. → Chi tiết kèm Ví dụ

Một triệu chứng của sự phụ thuộc lẫn nhau quá mức là việc biên dịch tăng dần (incremental build), tức là phải biên dịch lại phần lớn dự án chỉ vì những thay đổi nhỏ tại một điểm cục bộ (Mục 2).

Ngoại lệ

Vòng lặp giữa các lớp không phải lúc nào cũng xấu, nếu các lớp thuộc cùng một module, được kiểm thử (test) & phát hành (release) cùng nhau, vòng lặp có thể chấp nhận được.

Các mẫu thiết kế như Command Pattern và Visitor Pattern đôi khi có sự phụ thuộc tự nhiên giữa các thành phần. Việc phụ thuộc này có thể được phá vỡ nhưng cần thiết kế lại cẩn thận.

Tham khảo

[AlexandrescuOl] §3 • [Boost] • [Gamma95] • [Lakos96] §0.2.1, §4.6-14, §5 • [Martin96a] • [Martin96b] • [Martin98] §7 • [MartinOO] • [McConnell93] §5 • [Meyers97] §46 • [StroustrupOO] §24.3.5 • [SutterOO] §26 • [SutterO2] §37 • [SutterO3]

Link

Leave a Comment

Chia sẻ:

22. Tối thiểu những định nghĩa phụ thuộc. Tránh những phụ thuộc qua lại/ tuần hoàn.

Or copy link

CONTENTS