Nguyên tắc đảo ngược phụ thuộc là gì và tại sao nó quan trọng?
Nguyên tắc đảo ngược phụ thuộc là gì và tại sao nó quan trọng?
Câu trả lời:
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:
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ị.
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ụ:
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).
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 .
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:
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.
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.
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;
}
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 )
Đ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.
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.
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 )
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.
Đố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.
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).
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.
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 Logic
như 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
}
}
Data
và DataFromDependency
nê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?
Dependency
thay đổi, bạn không cần thay đổi Logic
.Logic
là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.Logic
bâ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 Data
vớ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.DataFromDependency
, tham chiếu trực tiếp Dependency
, nằm trong cùng một mô-đun Logic
, thì Logic
mô-đun vẫn trực tiếp phụ thuộc vào Dependency
mô-đ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, Data
nên ở trong cùng một mô-đun Logic
, nhưng DataFromDependency
nên ở trong cùng một mô-đun như Dependency
.
Đả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à:
Đ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 ý.
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".
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;
}
}
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.
Đâ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/
Hãy nói rằng chúng ta có hai lớp: Engineer
và Programmer
:
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 Engineer
có một sự phụ thuộc vào Programmer
lớ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 OCP
nà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 Interface
lớ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 ý: DependencyInjection
có thể giúp chúng tôi làm DIP
và SRP
quá.
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 IReadable
và IWriteable
sẽ không bao giờ làm phiền nó.
GoodEncoder
ví 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.
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).