Nguyên tắc đảo ngược phụ thuộc là gì và tại sao nó quan trọng?


178

Nguyên tắc đảo ngược phụ thuộc là gì và tại sao nó quan trọng?



3
Số lượng câu trả lời lố bịch ở đây bằng cách sử dụng thuật ngữ "cấp độ cao" và "cấp độ thấp" từ Wikipedia. Những điều khoản này không thể truy cập và dẫn nhiều người đọc đến trang này. Nếu bạn định lấy lại wikipedia, vui lòng xác định các thuật ngữ này trong câu trả lời của bạn để đưa ra ngữ cảnh!
8bitjunkie

Câu trả lời:


107

Kiểm tra tài liệu này: Nguyên tắc đảo ngược phụ thuộc .

Về cơ bản nó nói:

  • Các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp. Cả hai nên phụ thuộc vào trừu tượng.
  • Trừu tượng không bao giờ nên phụ thuộc vào chi tiết. Thông tin chi tiết nên phụ thuộc vào trừu tượng.

Về lý do tại sao nó quan trọng, tóm lại: thay đổi là rủi ro và bằng cách phụ thuộc vào một khái niệm thay vì thực hiện, bạn giảm nhu cầu thay đổi tại các trang web cuộc gọi.

Có hiệu quả, DIP làm giảm sự ghép nối giữa các đoạn mã khác nhau. Ý tưởng là mặc dù có nhiều cách để thực hiện, giả sử, một cơ sở khai thác gỗ, cách bạn sẽ sử dụng nó sẽ tương đối ổn định về thời gian. Nếu bạn có thể trích xuất một giao diện đại diện cho khái niệm ghi nhật ký, giao diện này sẽ ổn định hơn về thời gian so với triển khai của nó và các trang web cuộc gọi sẽ ít bị ảnh hưởng hơn bởi những thay đổi bạn có thể thực hiện trong khi duy trì hoặc mở rộng cơ chế ghi nhật ký đó.

Bằng cách làm cho việc triển khai phụ thuộc vào một giao diện, bạn có thể chọn trong thời gian chạy, việc triển khai nào phù hợp hơn với môi trường cụ thể của bạn. Tùy thuộc vào các trường hợp, điều này cũng có thể thú vị.


31
Câu trả lời này không nói rõ tại sao DIP lại quan trọng, hay thậm chí là DIP là gì. Tôi đã đọc tài liệu chính thức của DIP và nghĩ rằng đây thực sự là một "nguyên tắc" kém và không chính đáng, bởi vì nó dựa trên một giả định sai lầm: các mô-đun cấp cao có thể tái sử dụng.
Rogério

3
Xem xét một biểu đồ phụ thuộc cho một số đối tượng. Áp dụng DIP cho các đối tượng. Bây giờ bất kỳ đối tượng sẽ phụ thuộc vào việc thực hiện các đối tượng khác. Kiểm tra đơn vị bây giờ là đơn giản. Tái cấu trúc sau này để tái sử dụng là có thể. Thay đổi thiết kế có phạm vi thay đổi rất hạn chế. Vấn đề thiết kế không xếp tầng. Xem thêm mẫu AI "Bảng đen" để biết đảo ngược dữ liệu phụ thuộc dữ liệu. Cùng với nhau, các công cụ rất mạnh để làm cho phần mềm dễ hiểu, có thể bảo trì và đáng tin cậy. Bỏ qua tiêm phụ thuộc trong bối cảnh này. Nó không liên quan.
Tim Williscroft

6
Một lớp A sử dụng một lớp B không có nghĩa là A "phụ thuộc" vào việc thực hiện B; nó chỉ phụ thuộc vào giao diện công cộng của B. Thêm một sự trừu tượng riêng biệt mà cả A và B bây giờ chỉ phụ thuộc có nghĩa là A sẽ không còn phụ thuộc thời gian biên dịch trên giao diện công khai của B. Có thể dễ dàng đạt được sự cách ly giữa các đơn vị mà không cần các tóm tắt thêm này; có các công cụ mô phỏng cụ thể cho điều đó, trong Java và .NET, xử lý tất cả các tình huống (phương thức tĩnh, hàm tạo, v.v.). Áp dụng DIP có xu hướng làm cho phần mềm phức tạp hơn và ít bảo trì hơn, và không thể kiểm tra được nữa.
Rogério

1
Vậy thì đảo ngược điều khiển là gì? một cách để đạt được sự đảo ngược phụ thuộc?
dùng2058

2
@Rogerio, xem phản hồi của Derek Greer bên dưới, anh giải thích nó. AFAIK, DIP nói rằng đó là A, người bắt buộc những gì A cần, chứ không phải B, những người nói những gì A cần. Vì vậy, giao diện mà A cần không nên được cung cấp bởi B, nhưng đối với A.
ejaenv

145

Các cuốn sách Phát triển phần mềm, nguyên tắc, mô hình và thực tiễn và nguyên tắc, mô hình và thực tiễn Agile trong C # là những tài nguyên tốt nhất để hiểu đầy đủ các mục tiêu và động lực ban đầu đằng sau Nguyên tắc đảo ngược phụ thuộc. Bài báo "Nguyên tắc đảo ngược phụ thuộc" cũng là một tài nguyên tốt, nhưng do thực tế nó là một phiên bản cô đọng của một bản thảo mà cuối cùng đã đi vào các cuốn sách được đề cập trước đó, nó bỏ qua một số thảo luận quan trọng về khái niệm của một quyền sở hữu gói và giao diện là chìa khóa để phân biệt nguyên tắc này với lời khuyên chung hơn là "lập trình cho giao diện, không phải triển khai" được tìm thấy trong sách Mẫu thiết kế (Gamma, et. al).

Để cung cấp một bản tóm tắt, Nguyên tắc đảo ngược phụ thuộc chủ yếu là đảo ngược hướng phụ thuộc thông thường từ các thành phần "cấp cao hơn" sang các thành phần "cấp thấp hơn" sao cho các thành phần "cấp thấp hơn" phụ thuộc vào các giao diện thuộc sở hữu của các thành phần "cấp cao hơn" . (Lưu ý: thành phần "mức cao hơn" ở đây đề cập đến thành phần yêu cầu phụ thuộc / dịch vụ bên ngoài, không nhất thiết là vị trí khái niệm của nó trong kiến ​​trúc phân lớp.) Khi làm như vậy, khớp nối không bị giảm quá nhiều do nó bị dịch chuyển khỏi các thành phần theo lý thuyết ít giá trị hơn đối với các thành phần có giá trị về mặt lý thuyết.

Điều này đạt được bằng cách thiết kế các thành phần có sự phụ thuộc bên ngoài được thể hiện dưới dạng giao diện mà người tiêu dùng của thành phần phải thực hiện. Nói cách khác, các giao diện được xác định biểu thị những gì cần thiết cho thành phần, chứ không phải cách bạn sử dụng thành phần đó (ví dụ: "INeedS Something", không phải "IDoS Something").

Điều mà Nguyên tắc đảo ngược phụ thuộc không đề cập đến là thực tiễn đơn giản về trừu tượng hóa các phụ thuộc thông qua việc sử dụng các giao diện (ví dụ MyService → [ILogger Logger]). Mặc dù điều này tách rời một thành phần từ chi tiết triển khai cụ thể của phụ thuộc, nhưng nó không đảo ngược mối quan hệ giữa người tiêu dùng và người phụ thuộc (ví dụ [MyService → IMyServiceLogger] Logger.

Tầm quan trọng của Nguyên tắc đảo ngược phụ thuộc có thể được chắt lọc vào một mục tiêu duy nhất là có thể sử dụng lại các thành phần phần mềm phụ thuộc vào các phụ thuộc bên ngoài cho một phần chức năng của chúng (ghi nhật ký, xác nhận, v.v.)

Trong mục tiêu chung này của việc tái sử dụng, chúng ta có thể phân định hai loại tái sử dụng phụ:

  1. Sử dụng một thành phần phần mềm trong nhiều ứng dụng với triển khai phụ thuộc phụ (ví dụ: Bạn đã phát triển bộ chứa DI và muốn cung cấp ghi nhật ký, nhưng không muốn ghép cặp container của bạn với một bộ ghi nhật ký cụ thể để mọi người sử dụng bộ chứa của bạn cũng phải sử dụng thư viện đăng nhập đã chọn của bạn).

  2. Sử dụng các thành phần phần mềm trong bối cảnh phát triển (ví dụ: Bạn đã phát triển các thành phần logic nghiệp vụ vẫn giống nhau trên nhiều phiên bản của một ứng dụng có chi tiết triển khai đang phát triển).

Với trường hợp đầu tiên sử dụng lại các thành phần trên nhiều ứng dụng, chẳng hạn như với thư viện cơ sở hạ tầng, mục tiêu là cung cấp cơ sở hạ tầng cốt lõi cho người tiêu dùng của bạn mà không ghép người tiêu dùng của bạn với phụ thuộc của thư viện của bạn vì phải phụ thuộc vào các phụ thuộc đó người tiêu dùng để yêu cầu các phụ thuộc tương tự là tốt. Điều này có thể có vấn đề khi người tiêu dùng thư viện của bạn chọn sử dụng một thư viện khác cho cùng một nhu cầu cơ sở hạ tầng (ví dụ: NLog so với log4net) hoặc nếu họ chọn sử dụng phiên bản mới hơn của thư viện bắt buộc không tương thích ngược với phiên bản yêu cầu của thư viện của bạn.

Với trường hợp thứ hai là sử dụng lại các thành phần logic nghiệp vụ (nghĩa là "các thành phần cấp cao hơn"), mục tiêu là cách ly việc triển khai miền lõi của ứng dụng của bạn khỏi nhu cầu thay đổi của chi tiết triển khai của bạn (ví dụ: thay đổi / nâng cấp thư viện lưu trữ, thư viện nhắn tin , chiến lược mã hóa, v.v.). Lý tưởng nhất là thay đổi các chi tiết triển khai của một ứng dụng không nên phá vỡ các thành phần gói gọn logic kinh doanh của ứng dụng.

Lưu ý: Một số có thể phản đối việc mô tả trường hợp thứ hai này là tái sử dụng thực tế, lý do rằng các thành phần như các thành phần logic nghiệp vụ được sử dụng trong một ứng dụng phát triển duy nhất chỉ đại diện cho một lần sử dụng. Tuy nhiên, ý tưởng ở đây là mỗi thay đổi đối với các chi tiết triển khai của ứng dụng sẽ tạo ra một bối cảnh mới và do đó là một trường hợp sử dụng khác nhau, mặc dù các mục tiêu cuối cùng có thể được phân biệt là cô lập so với tính di động.

Mặc dù tuân theo Nguyên tắc đảo ngược phụ thuộc trong trường hợp thứ hai này có thể mang lại một số lợi ích, cần lưu ý rằng giá trị của nó khi áp dụng cho các ngôn ngữ hiện đại như Java và C # bị giảm đi rất nhiều, có lẽ đến mức không liên quan. Như đã thảo luận trước đó, DIP liên quan đến việc tách hoàn toàn các chi tiết thực hiện thành các gói riêng biệt. Tuy nhiên, trong trường hợp ứng dụng đang phát triển, chỉ cần sử dụng các giao diện được xác định theo thuật ngữ của lĩnh vực kinh doanh sẽ bảo vệ không cần phải sửa đổi các thành phần cấp cao hơn do thay đổi nhu cầu của các thành phần chi tiết triển khai, ngay cả khi các chi tiết triển khai cuối cùng nằm trong cùng một gói . Phần này của nguyên tắc phản ánh các khía cạnh phù hợp với ngôn ngữ theo quan điểm khi nguyên tắc được mã hóa (tức là C ++) không liên quan đến các ngôn ngữ mới hơn. Mà nói,

Một cuộc thảo luận dài hơn về nguyên tắc này vì nó liên quan đến việc sử dụng đơn giản các giao diện, Dependency Injection và mẫu Giao diện tách biệt có thể được tìm thấy ở đây . Ngoài ra, một cuộc thảo luận về cách nguyên tắc liên quan đến các ngôn ngữ được gõ động như JavaScript có thể được tìm thấy ở đây .


17
Cảm ơn. Tôi thấy bây giờ làm thế nào câu trả lời của tôi bỏ lỡ điểm. Sự khác biệt giữa MyService → [ILogger Logger][MyService → IMyServiceLogger] Logger rất tinh tế nhưng quan trọng.
Patrick McElhaney

2
Trong cùng một dòng, nó được giải thích rất rõ ở đây: Lostechies.com/derickbailey/2011/09/22/NH
ejaenv

1
Làm thế nào tôi muốn mọi người sẽ ngừng sử dụng loggers như là usecase chính tắc cho tiêm phụ thuộc. Đặc biệt là trong kết nối với log4net, nơi nó gần như là một mô hình chống. Điều đó sang một bên, giải thích kickass!
Casper Leon Nielsen

4
@ Casper Leon Nielsen - DIP không liên quan gì đến DI Chúng không phải là từ đồng nghĩa hay khái niệm tương đương.
TSmith

4
@ VF1 Như đã nêu trong đoạn tóm tắt, tầm quan trọng của Nguyên tắc đảo ngược phụ thuộc chủ yếu là tái sử dụng. Nếu John phát hành một thư viện có sự phụ thuộc bên ngoài vào thư viện ghi nhật ký và Sam muốn sử dụng thư viện của John, thì Sam sẽ phụ thuộc vào việc ghi nhật ký tạm thời. Sam sẽ không bao giờ có thể triển khai ứng dụng của mình mà không có thư viện đăng nhập mà John đã chọn. Tuy nhiên, nếu John tuân theo DIP, Sam có thể tự do cung cấp bộ điều hợp và sử dụng bất kỳ thư viện ghi nhật ký nào mà anh ta chọn. DIP không phải là về sự tiện lợi, mà là khớp nối.
Derek Greer

14

Khi chúng ta thiết kế các ứng dụng phần mềm, chúng ta có thể xem xét các lớp cấp thấp, các lớp thực hiện các hoạt động cơ bản và chính (truy cập đĩa, giao thức mạng, ...) và các lớp cấp cao, các lớp đóng gói logic phức tạp (luồng kinh doanh, ...).

Những người cuối cùng dựa vào các lớp cấp thấp. Một cách tự nhiên để thực hiện các cấu trúc như vậy sẽ là viết các lớp cấp thấp và một khi chúng ta có chúng để viết các lớp cấp cao phức tạp. Vì các lớp cấp cao được định nghĩa theo nghĩa của người khác, điều này có vẻ là cách hợp lý để làm điều đó. Nhưng đây không phải là một thiết kế linh hoạt. Điều gì xảy ra nếu chúng ta cần thay thế một lớp cấp thấp?

Nguyên tắc đảo ngược phụ thuộc quy định rằng:

  • Các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp. Cả hai nên phụ thuộc vào trừu tượng.
  • Trừu tượng không nên phụ thuộc vào chi tiết. Thông tin chi tiết nên phụ thuộc vào trừu tượng.

Nguyên tắc này tìm cách "đảo ngược" khái niệm thông thường rằng các mô-đun cấp cao trong phần mềm nên phụ thuộc vào các mô-đun cấp thấp hơn. Ở đây các mô-đun cấp cao sở hữu sự trừu tượng hóa (ví dụ: quyết định các phương thức của giao diện) được thực hiện bởi các mô-đun cấp thấp hơn. Do đó làm cho các mô-đun cấp thấp hơn phụ thuộc vào các mô-đun cấp cao hơn.


11

Sự phụ thuộc đảo ngược được áp dụng tốt mang lại sự linh hoạt và ổn định ở cấp độ của toàn bộ kiến ​​trúc ứng dụng của bạn. Nó sẽ cho phép ứng dụng của bạn phát triển an toàn và ổn định hơn.

Kiến trúc lớp truyền thống

Theo truyền thống, giao diện người dùng kiến ​​trúc phân lớp phụ thuộc vào lớp nghiệp vụ và điều này phụ thuộc vào lớp truy cập dữ liệu.

Bạn phải hiểu lớp, gói hoặc thư viện. Chúng ta hãy xem mã sẽ như thế nào.

Chúng tôi sẽ có một thư viện hoặc gói cho lớp truy cập dữ liệu.

// DataAccessLayer.dll
public class ProductDAO {

}

Và một thư viện hoặc gói kinh doanh logic gói phụ thuộc vào lớp truy cập dữ liệu.

// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO { 
    private ProductDAO productDAO;
}

Kiến trúc phân lớp với sự đảo ngược phụ thuộc

Sự đảo ngược phụ thuộc chỉ ra những điều sau đây:

Các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp. Cả hai nên phụ thuộc vào trừu tượng.

Trừu tượng không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào trừu tượng.

Các mô-đun cấp cao và cấp thấp là gì? Các mô-đun suy nghĩ như thư viện hoặc gói, mô-đun cấp cao sẽ là những mô-đun mà theo truyền thống có phụ thuộc và mức độ thấp mà chúng phụ thuộc.

Nói cách khác, mức cao của mô-đun sẽ là nơi hành động được gọi và mức thấp nơi hành động được thực hiện.

Một kết luận hợp lý để rút ra từ nguyên tắc này là không nên có sự phụ thuộc giữa các bê tông hóa, nhưng phải có sự phụ thuộc vào một sự trừu tượng. Nhưng theo cách tiếp cận chúng ta thực hiện, chúng ta có thể đang sử dụng sai sự phụ thuộc đầu tư, nhưng là một sự trừu tượng.

Hãy tưởng tượng rằng chúng tôi điều chỉnh mã của chúng tôi như sau:

Chúng ta sẽ có một thư viện hoặc gói cho lớp truy cập dữ liệu xác định sự trừu tượng hóa.

// DataAccessLayer.dll
public interface IProductDAO
public class ProductDAO : IProductDAO{

}

Và một thư viện hoặc gói kinh doanh logic gói phụ thuộc vào lớp truy cập dữ liệu.

// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO { 
    private IProductDAO productDAO;
}

Mặc dù chúng tôi phụ thuộc vào sự phụ thuộc trừu tượng giữa doanh nghiệp và truy cập dữ liệu vẫn giống nhau.

Để có được nghịch đảo phụ thuộc, giao diện bền vững phải được xác định trong mô-đun hoặc gói trong đó logic hoặc miền cấp cao này chứ không phải trong mô-đun cấp thấp.

Đầu tiên xác định lớp miền là gì và sự trừu tượng của giao tiếp được xác định tính bền vững.

// Domain.dll
public interface IProductRepository;

using DataAccessLayer;
public class ProductBO { 
    private IProductRepository productRepository;
}

Sau lớp kiên trì phụ thuộc vào miền, bắt đầu đảo ngược ngay bây giờ nếu một phụ thuộc được xác định.

// Persistence.dll
public class ProductDAO : IProductRepository{

}


(nguồn: xurxodev.com )

Làm sâu sắc thêm nguyên tắc

Điều quan trọng là phải đồng hóa khái niệm tốt, đào sâu mục đích và lợi ích. Nếu chúng ta ở lại một cách máy móc và tìm hiểu kho lưu trữ trường hợp điển hình, chúng ta sẽ không thể xác định nơi chúng ta có thể áp dụng nguyên tắc phụ thuộc.

Nhưng tại sao chúng ta đảo ngược một sự phụ thuộc? Mục tiêu chính ngoài các ví dụ cụ thể là gì?

Như vậy thường cho phép những thứ ổn định nhất, không phụ thuộc vào những thứ kém ổn định hơn, thay đổi thường xuyên hơn.

Loại thay đổi liên tục sẽ dễ dàng hơn, cả cơ sở dữ liệu hoặc công nghệ truy cập vào cùng một cơ sở dữ liệu so với logic miền hoặc các hành động được thiết kế để giao tiếp với sự kiên trì. Bởi vì điều này, sự phụ thuộc bị đảo ngược bởi vì nó dễ dàng thay đổi sự kiên trì hơn nếu thay đổi này xảy ra. Bằng cách này, chúng tôi sẽ không phải thay đổi tên miền. Lớp miền ổn định nhất trong tất cả, đó là lý do tại sao nó không nên phụ thuộc vào bất cứ điều gì.

Nhưng không chỉ có ví dụ về kho lưu trữ này. Có nhiều kịch bản áp dụng nguyên tắc này và có những kiến ​​trúc dựa trên nguyên tắc này.

Kiến trúc

Có những kiến ​​trúc trong đó nghịch đảo phụ thuộc là chìa khóa cho định nghĩa của nó. Trong tất cả các miền, điều quan trọng nhất và đó là sự trừu tượng sẽ chỉ ra giao thức giao tiếp giữa miền và phần còn lại của các gói hoặc thư viện được xác định.

Kiến trúc sạch

Trong Kiến trúc sạch , miền nằm ở trung tâm và nếu bạn nhìn theo hướng mũi tên biểu thị sự phụ thuộc, thì rõ ràng đâu là các lớp quan trọng và ổn định nhất. Các lớp bên ngoài được coi là công cụ không ổn định vì vậy tránh phụ thuộc vào chúng.


(nguồn: 8thlight.com )

Kiến trúc lục giác

Nó xảy ra theo cách tương tự với kiến ​​trúc lục giác, nơi miền cũng nằm ở phần trung tâm và các cổng là sự trừu tượng của giao tiếp từ domino ra bên ngoài. Ở đây một lần nữa, rõ ràng là miền là sự phụ thuộc truyền thống và ổn định nhất được đảo ngược.


Tôi không chắc nó có ý gì để nói, nhưng câu này không mạch lạc: "Nhưng theo cách tiếp cận, chúng ta có thể đang sử dụng sai sự phụ thuộc đầu tư, nhưng là một sự trừu tượng." Có lẽ bạn muốn sửa chữa?
Mark Amery

9

Đối với tôi, Nguyên tắc đảo ngược phụ thuộc, như được mô tả trong bài viết chính thức , thực sự là một nỗ lực sai lầm để tăng khả năng sử dụng lại các mô-đun vốn ít sử dụng lại, cũng như cách khắc phục vấn đề trong ngôn ngữ C ++.

Vấn đề trong C ++ là các tệp tiêu đề thường chứa các khai báo của các trường và phương thức riêng. Do đó, nếu mô-đun C ++ cấp cao bao gồm tệp tiêu đề cho mô-đun cấp thấp, nó sẽ phụ thuộc vào việc triển khai thực tế chi tiết của mô-đun đó. Và đó, rõ ràng, không phải là một điều tốt. Nhưng đây không phải là một vấn đề trong các ngôn ngữ hiện đại hơn thường được sử dụng ngày nay.

Các mô-đun cấp cao vốn đã ít tái sử dụng hơn các mô-đun cấp thấp bởi vì mô-đun trước thường là ứng dụng / bối cảnh cụ thể hơn so với mô-đun sau. Ví dụ, một thành phần thực hiện màn hình UI là mức cao nhất và cũng rất (hoàn toàn?) Cụ thể cho ứng dụng. Cố gắng sử dụng lại một thành phần như vậy trong một ứng dụng khác là phản tác dụng và chỉ có thể dẫn đến kỹ thuật quá mức.

Vì vậy, việc tạo ra một sự trừu tượng riêng biệt ở cùng cấp độ của một thành phần A phụ thuộc vào thành phần B (không phụ thuộc vào A) chỉ có thể được thực hiện nếu thành phần A sẽ thực sự hữu ích để tái sử dụng trong các ứng dụng hoặc bối cảnh khác nhau. Nếu đó không phải là trường hợp, thì áp dụng DIP sẽ là thiết kế xấu.


Ngay cả khi tính trừu tượng mức cao chỉ hữu ích trong ngữ cảnh của ứng dụng đó, nó có thể có giá trị khi bạn muốn thay thế triển khai thực sự bằng một sơ khai để thử nghiệm (hoặc cung cấp giao diện dòng lệnh cho một ứng dụng thường có sẵn trên web)
Przemek Pokrywka

3
DIP rất quan trọng đối với các ngôn ngữ và không liên quan gì đến chính C ++. Ngay cả khi mã cấp cao của bạn sẽ không bao giờ rời khỏi ứng dụng của bạn, thì DIP cho phép bạn chứa thay đổi mã khi bạn thêm hoặc thay đổi mã cấp thấp hơn. Điều này làm giảm cả chi phí bảo trì và hậu quả không lường trước của sự thay đổi. DIP là một khái niệm cấp cao hơn. Nếu bạn không hiểu nó, bạn cần phải làm nhiều việc hơn.
Dirk Bester

3
Tôi nghĩ bạn đã hiểu sai những gì tôi nói về C ++. Đó chỉ là một động lực cho DIP; không còn nghi ngờ gì nữa, nó còn chung chung hơn thế Lưu ý bài viết chính thức về DIP cho thấy rõ động lực trung tâm là hỗ trợ tái sử dụng các mô-đun cấp cao, bằng cách làm cho chúng miễn nhiễm với các thay đổi trong các mô-đun cấp thấp; không cần tái sử dụng, nên rất có thể nó là quá mức cần thiết và quá kỹ thuật. (Bạn đã đọc nó chưa? Nó cũng nói về vấn đề C ++.)
Rogério

1
Không phải bạn đang xem ứng dụng ngược của DIP sao? Đó là, các mô-đun cấp cao hơn thực hiện ứng dụng của bạn, về cơ bản. Mối quan tâm hàng đầu của bạn không phải là tái sử dụng nó, nó làm cho nó ít phụ thuộc hơn vào việc triển khai các mô-đun cấp thấp hơn để việc cập nhật nó để theo kịp các thay đổi trong tương lai sẽ tách ứng dụng của bạn khỏi sự tàn phá của thời gian và công nghệ mới. Không phải để thay thế WindowsOS dễ dàng hơn, nó giúp WindowsOS bớt phụ thuộc vào các chi tiết triển khai của FAT / HDD để các NTFS / SSD mới hơn có thể được cắm vào WindowsOS mà không có hoặc có ít tác động.
gõNrod

1
Màn hình UI chắc chắn không phải là mô-đun cấp cao nhất hoặc không nên dành cho bất kỳ ứng dụng nào. Các mô-đun cấp cao nhất là những mô-đun có chứa các quy tắc kinh doanh. Một mô-đun cấp cao nhất cũng không được xác định bởi tiềm năng tái sử dụng của nó. Vui lòng kiểm tra lời giải thích đơn giản của chú Bob trong bài viết này: blog.cleancoder.com/uncle-bob/2016/01/04/
humbaba

8

Về cơ bản nó nói:

Lớp nên phụ thuộc vào trừu tượng (ví dụ: giao diện, lớp trừu tượng), không phải chi tiết cụ thể (triển khai).


Nó có thể đơn giản như vậy? Có những bài báo dài và thậm chí cả những cuốn sách như DerekGreer đã đề cập? Tôi thực sự đã tìm kiếm câu trả lời đơn giản, nhưng thật không thể tin được nếu nó đơn giản: D
Darius.V

1
@ darius-v nó không phải là một phiên bản khác của câu nói "sử dụng trừu tượng". Đó là về người sở hữu các giao diện. Hiệu trưởng nói rằng máy khách (các thành phần cấp cao hơn) nên xác định các giao diện và các thành phần cấp thấp hơn nên thực hiện chúng.
boran

6

Câu trả lời tốt và ví dụ tốt đã được đưa ra bởi những người khác ở đây.

Lý do DIP rất quan trọng là vì nó đảm bảo nguyên tắc OO "thiết kế ghép lỏng lẻo".

Các đối tượng trong phần mềm của bạn KHÔNG được vào một hệ thống phân cấp trong đó một số đối tượng là đối tượng cấp cao nhất, phụ thuộc vào các đối tượng cấp thấp. Các thay đổi trong các đối tượng cấp thấp sau đó sẽ chuyển sang các đối tượng cấp cao nhất của bạn, điều này làm cho phần mềm rất dễ bị thay đổi.

Bạn muốn các đối tượng 'cấp cao nhất' của mình rất ổn định và không dễ bị thay đổi, do đó bạn cần phải đảo ngược các phụ thuộc.


6
Chính xác thì DIP làm điều đó như thế nào, đối với mã Java hay .NET? Trong các ngôn ngữ đó, trái với C ++, các thay đổi đối với việc triển khai mô-đun cấp thấp không yêu cầu thay đổi trong các mô-đun cấp cao sử dụng nó. Chỉ những thay đổi trong giao diện công cộng sẽ gợn qua, nhưng sau đó sự trừu tượng được xác định ở cấp cao hơn cũng sẽ phải thay đổi.
Rogério

5

Một cách rõ ràng hơn nhiều để nêu Nguyên tắc đảo ngược phụ thuộc là:

Các mô-đun của bạn đóng gói logic kinh doanh phức tạp không nên phụ thuộc trực tiếp vào các mô-đun khác đóng gói logic kinh doanh. Thay vào đó, họ chỉ nên phụ thuộc vào giao diện với dữ liệu đơn giản.

Tức là, thay vì triển khai lớp của bạn Logicnhư mọi người thường làm:

class Dependency { ... }
class Logic {
    private Dependency dep;
    int doSomething() {
        // Business logic using dep here
    }
}

bạn nên làm một cái gì đó như:

class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
    private Dependency dep;
    ...
}
class Logic {
    int doSomething(Data data) {
        // compute something with data
    }
}

DataDataFromDependencynên sống trong cùng một mô-đun như Logic, không phải vớiDependency .

Tại sao làm điều này?

  1. Hai mô-đun logic kinh doanh hiện đang được tách rời. Khi Dependencythay đổi, bạn không cần thay đổi Logic.
  2. Hiểu những gì Logiclàm là một nhiệm vụ đơn giản hơn nhiều: nó chỉ hoạt động trên những gì trông giống như một ADT.
  3. Logicbây giờ có thể được kiểm tra dễ dàng hơn. Bây giờ bạn có thể trực tiếp khởi tạo Datavới dữ liệu giả mạo và chuyển nó vào. Không cần giả hoặc giàn giáo thử nghiệm phức tạp.

Tôi không nghĩ rằng điều này là đúng. Nếu DataFromDependency, tham chiếu trực tiếp Dependency, nằm trong cùng một mô-đun Logic, thì Logicmô-đun vẫn trực tiếp phụ thuộc vào Dependencymô-đun tại thời gian biên dịch. Theo lời giải thích của bác Bob về nguyên tắc , tránh đó là toàn bộ quan điểm của DIP. Thay vào đó, để tuân theo DIP, Datanên ở trong cùng một mô-đun Logic, nhưng DataFromDependencynên ở trong cùng một mô-đun như Dependency.
Đánh dấu Amery

3

Đảo ngược kiểm soát (IoC) là một mẫu thiết kế trong đó một đối tượng được trao phần phụ thuộc của nó bằng một khung bên ngoài, thay vì yêu cầu một khung cho sự phụ thuộc của nó.

Ví dụ mã giả sử dụng tra cứu truyền thống:

class Service {
    Database database;
    init() {
        database = FrameworkSingleton.getService("database");
    }
}

Mã tương tự sử dụng IoC:

class Service {
    Database database;
    init(database) {
        this.database = database;
    }
}

Lợi ích của IoC là:

  • Bạn không có sự phụ thuộc vào một khung trung tâm, vì vậy điều này có thể được thay đổi nếu muốn.
  • Vì các đối tượng được tạo bằng cách tiêm, tốt nhất là sử dụng các giao diện, nên dễ dàng tạo các thử nghiệm đơn vị thay thế các phụ thuộc bằng các phiên bản giả.
  • Tách mã.

Là nguyên tắc đảo ngược phụ thuộc và đảo ngược của kiểm soát là điều tương tự?
Peter Mortensen

3
"Đảo ngược" trong DIP không giống như trong Đảo ngược điều khiển. Cái đầu tiên là về các phụ thuộc thời gian biên dịch, trong khi cái thứ hai là về luồng điều khiển giữa các phương thức khi chạy.
Rogério

Tôi cảm thấy như phiên bản IoC đang thiếu một cái gì đó. Làm thế nào là đối tượng cơ sở dữ liệu mà nó được định nghĩa, nó đến từ đâu. Tôi đang cố gắng hiểu làm thế nào điều này không chỉ trì hoãn việc chỉ định tất cả các phụ thuộc lên cấp cao nhất trong một phụ thuộc thần khổng lồ
Richard Tingle

1
Như đã nói bởi @ Rogério, DIP không phải là DI / IoC. Câu trả lời này sai IMO
zeraDev 25/03/19

1

Điểm của sự đảo ngược phụ thuộc là làm cho phần mềm có thể tái sử dụng.

Ý tưởng là thay vì hai đoạn mã dựa vào nhau, họ dựa vào một số giao diện trừu tượng. Sau đó, bạn có thể tái sử dụng một trong hai mà không có phần khác.

Cách thức này thường đạt được là thông qua bộ chứa điều khiển đảo ngược (IoC) như Spring trong Java. Trong mô hình này, các thuộc tính của các đối tượng được thiết lập thông qua cấu hình XML thay vì các đối tượng đi ra ngoài và tìm thấy sự phụ thuộc của chúng.

Hãy tưởng tượng mã giả này ...

public class MyClass
{
  public Service myService = ServiceLocator.service;
}

MyClass trực tiếp phụ thuộc vào cả lớp Service và lớp ServiceLocator. Nó cần cả hai thứ đó nếu bạn muốn sử dụng nó trong một ứng dụng khác. Bây giờ hãy tưởng tượng điều này ...

public class MyClass
{
  public IService myService;
}

Bây giờ, MyClass dựa trên một giao diện duy nhất, giao diện IService. Chúng tôi sẽ để bộ chứa IoC thực sự đặt giá trị của biến đó.

Vì vậy, bây giờ, MyClass có thể dễ dàng được sử dụng lại trong các dự án khác, mà không mang lại sự phụ thuộc của hai lớp khác cùng với nó.

Thậm chí tốt hơn, bạn không phải kéo các phụ thuộc của MyService và các phụ thuộc của các phụ thuộc đó và ... tốt, bạn hiểu ý.


2
Câu trả lời này thực sự không phải là về DIP, mà là một cái gì đó khác. Hai đoạn mã có thể dựa trên một số giao diện trừu tượng và không nằm trong nhau, và vẫn không tuân theo Nguyên tắc đảo ngược phụ thuộc.
Rogério

1

Nếu chúng ta có thể coi như một nhân viên "cấp cao" tại một công ty được trả tiền cho việc thực hiện kế hoạch của họ, và những kế hoạch này được thực hiện bằng cách thực hiện tổng hợp nhiều kế hoạch của nhân viên "cấp thấp", thì chúng ta có thể nói nói chung là một kế hoạch khủng khiếp nếu mô tả kế hoạch của nhân viên cấp cao theo bất kỳ cách nào được kết hợp với kế hoạch cụ thể của bất kỳ nhân viên cấp dưới nào.

Nếu một giám đốc điều hành cấp cao có kế hoạch "cải thiện thời gian giao hàng" và chỉ ra rằng một nhân viên trong hãng tàu phải uống cà phê và kéo dài mỗi buổi sáng, thì kế hoạch đó được kết hợp chặt chẽ và có độ gắn kết thấp. Nhưng nếu kế hoạch không đề cập đến bất kỳ nhân viên cụ thể nào và trên thực tế chỉ cần "một thực thể có thể thực hiện công việc được chuẩn bị để làm việc", thì kế hoạch được kết nối lỏng lẻo và gắn kết hơn: các kế hoạch không chồng chéo và có thể dễ dàng thay thế . Các nhà thầu, hoặc robot, có thể dễ dàng thay thế nhân viên và kế hoạch cấp cao vẫn không thay đổi.

"Cấp cao" trong nguyên tắc đảo ngược phụ thuộc có nghĩa là "quan trọng hơn".


1
Đây là một sự tương tự tốt đáng chú ý. Phần lớn, theo DIC, các thành phần cấp cao vẫn phụ thuộc vào thời gian chạy trên các mức thấp, trong trường hợp tương tự này, việc thực hiện các kế hoạch cấp cao phụ thuộc vào các kế hoạch cấp thấp. Nhưng cũng như, theo DIC, các kế hoạch cấp thấp phụ thuộc vào thời gian biên dịch vào các kế hoạch cấp cao, trường hợp của bạn là sự hình thành của các kế hoạch cấp thấp phụ thuộc vào các kế hoạch cấp cao.
Mark Amery

0

Tôi có thể thấy lời giải thích tốt đã được đưa ra trong câu trả lời ở trên. Tuy nhiên tôi muốn cung cấp một số giải thích dễ dàng với ví dụ đơn giản.

Nguyên tắc đảo ngược phụ thuộc cho phép lập trình viên loại bỏ các phụ thuộc được mã hóa cứng để ứng dụng trở nên lỏng lẻo và có thể mở rộng.

Làm thế nào để đạt được điều này: thông qua sự trừu tượng

Không phụ thuộc đảo ngược:

 class Student {
    private Address address;

    public Student() {
        this.address = new Address();
    }
}
class Address{
    private String perminentAddress;
    private String currentAdrress;

    public Address() {
    }
} 

Trong đoạn mã trên, đối tượng địa chỉ được mã hóa cứng. Thay vào đó, nếu chúng ta có thể sử dụng đảo ngược phụ thuộc và tiêm đối tượng địa chỉ bằng cách chuyển qua phương thức constructor hoặc setter. Hãy xem nào.

Với sự đảo ngược phụ thuộc:

class Student{
    private Address address;

    public Student(Address address) {
        this.address = address;
    }
    //or
    public void setAddress(Address address) {
        this.address = address;
    }
}

-1

Phụ thuộc đảo ngược: Phụ thuộc vào trừu tượng, không phụ thuộc vào cụ thể.

Đảo ngược điều khiển: Chính so với Trừu tượng và cách Chính là chất keo của các hệ thống.

Dip và IoC

Đây là một số bài viết tốt nói về điều này:

https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/

https://coderstower.com/2019/04/02/main-and-abstraction-the-decoupling-peers/

https://coderstower.com/2019/04/09/inversion-of-control-pocking-all-together/


-1

Hãy nói rằng chúng ta có hai lớp: EngineerProgrammer:

Kỹ sư lớp có sự phụ thuộc vào lớp Lập trình viên, như dưới đây:

class Engineer () {

    fun startWork(programmer: Programmer){
        programmer.work()
    }
}

class Programmer {

    fun work(){
        //TODO Do some work here!
    }
}

Trong ví dụ này lớp Engineercó một sự phụ thuộc vào Programmerlớp của chúng tôi . Điều gì sẽ xảy ra nếu tôi cần thay đổi Programmer?

Rõ ràng tôi cũng cần thay đổi Engineer. (Wow, tại thời điểm OCPnày cũng bị vi phạm)

Sau đó, những gì chúng ta phải dọn dẹp mớ hỗn độn này? Câu trả lời là trừu tượng thực sự. Bằng cách trừu tượng hóa, chúng ta có thể loại bỏ sự phụ thuộc giữa hai lớp này. Ví dụ, tôi có thể tạo một Interfacelớp cho Lập trình viên và từ giờ mọi lớp muốn sử dụng Programmerđều phải sử dụng lớp đó.Interface đó, sau đó bằng cách thay đổi lớp Lập trình viên, chúng tôi không cần thay đổi bất kỳ lớp nào đã sử dụng nó, vì sự trừu tượng mà chúng tôi đã sử dụng.

Lưu ý: DependencyInjectioncó thể giúp chúng tôi làm DIPSRPquá.


-1

Thêm vào hàng loạt câu trả lời hay, tôi muốn thêm một mẫu nhỏ của riêng tôi để chứng minh thực hành tốt so với thực hành xấu. Và vâng, tôi không phải là người ném đá!

Giả sử, bạn muốn có một chương trình nhỏ để chuyển đổi một chuỗi thành định dạng base64 thông qua I / O của bàn điều khiển. Đây là cách tiếp cận ngây thơ:

class Program
{
    static void Main(string[] args)
    {
        /*
         * BadEncoder: High-level class *contains* low-level I/O functionality.
         * Hence, you'll have to fiddle with BadEncoder whenever you want to change
         * the I/O mode or details. Not good. A good encoder should be I/O-agnostic --
         * problems with I/O shouldn't break the encoder!
         */
        BadEncoder.Run();            
    }
}

public static class BadEncoder
{
    public static void Run()
    {
        Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(Console.ReadLine())));
    }
}    

Về cơ bản, DIP nói rằng các thành phần cấp cao không nên phụ thuộc vào việc triển khai ở mức độ thấp, trong đó "cấp độ" là khoảng cách từ I / O theo Robert C. Martin ("Kiến trúc sạch"). Nhưng làm thế nào để bạn thoát khỏi tình trạng khó khăn này? Đơn giản bằng cách làm cho Bộ mã hóa trung tâm chỉ phụ thuộc vào các giao diện mà không bận tâm đến cách thức triển khai chúng:

class Program
{
    static void Main(string[] args)
    {           
        /* Demo of the Dependency Inversion Principle (= "High-level functionality
         * should not depend upon low-level implementations"): 
         * You can easily implement new I/O methods like
         * ConsoleReader, ConsoleWriter without ever touching the high-level
         * Encoder class!!!
         */            
        GoodEncoder.Run(new ConsoleReader(), new ConsoleWriter());        }
}

public static class GoodEncoder
{
    public static void Run(IReadable input, IWriteable output)
    {
        output.WriteOutput(Convert.ToBase64String(Encoding.ASCII.GetBytes(input.ReadInput())));
    }
}

public interface IReadable
{
    string ReadInput();
}

public interface IWriteable
{
    void WriteOutput(string txt);
}

public class ConsoleReader : IReadable
{
    public string ReadInput()
    {
        return Console.ReadLine();
    }
}

public class ConsoleWriter : IWriteable
{
    public void WriteOutput(string txt)
    {
        Console.WriteLine(txt);
    }
}

Lưu ý rằng bạn không cần phải chạm GoodEncoderđể thay đổi chế độ I / O - lớp đó hài lòng với các giao diện I / O mà nó biết; bất kỳ triển khai cấp thấp nào IReadableIWriteablesẽ không bao giờ làm phiền nó.


Tôi không nghĩ rằng đây là nghịch đảo phụ thuộc. Rốt cuộc, bạn đã không đảo ngược bất kỳ sự phụ thuộc nào ở đây; độc giả và nhà văn của bạn không phụ thuộc vào GoodEncoderví dụ thứ hai của bạn. Để tạo một ví dụ DIP, bạn cần đưa ra khái niệm "sở hữu" các giao diện bạn đã trích xuất ở đây - và đặc biệt, để đặt chúng trong cùng một gói với GoodEncoder trong khi các triển khai của chúng vẫn ở bên ngoài.
Mark Amery

-2

Nguyên tắc đảo ngược phụ thuộc (DIP) nói rằng

i) Các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp. Cả hai nên phụ thuộc vào trừu tượng.

ii) Trừu tượng không bao giờ nên phụ thuộc vào chi tiết. Thông tin chi tiết nên phụ thuộc vào trừu tượng.

Thí dụ:

    public interface ICustomer
    {
        string GetCustomerNameById(int id);
    }

    public class Customer : ICustomer
    {
        //ctor
        public Customer(){}

        public string GetCustomerNameById(int id)
        {
            return "Dummy Customer Name";
        }
    }

    public class CustomerFactory
    {
        public static ICustomer GetCustomerData()
        {
            return new Customer();
        }
    }

    public class CustomerBLL
    {
        ICustomer _customer;
        public CustomerBLL()
        {
            _customer = CustomerFactory.GetCustomerData();
        }

        public string GetCustomerNameById(int id)
        {
            return _customer.GetCustomerNameById(id);
        }
    }

    public class Program
    {
        static void Main()
        {
            CustomerBLL customerBLL = new CustomerBLL();
            int customerId = 25;
            string customerName = customerBLL.GetCustomerNameById(customerId);


            Console.WriteLine(customerName);
            Console.ReadKey();
        }
    }

Lưu ý: Lớp nên phụ thuộc vào trừu tượng như giao diện hoặc lớp trừu tượng, không phải chi tiết cụ thể (triển khai giao diện).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.