Jaka jest zasada inwersji zależności i dlaczego jest ważna?
Jaka jest zasada inwersji zależności i dlaczego jest ważna?
Odpowiedzi:
Sprawdź ten dokument: Zasada odwrócenia zależności .
Zasadniczo mówi:
Krótko mówiąc, dlaczego jest to ważne: zmiany są ryzykowne, a polegając na koncepcji zamiast na wdrożeniu, zmniejszasz potrzebę zmian w miejscach połączeń telefonicznych.
W efekcie DIP zmniejsza sprzężenie między różnymi fragmentami kodu. Chodzi o to, że chociaż istnieje wiele sposobów implementacji, powiedzmy, narzędzia do rejestrowania, sposób, w jaki z niego korzystasz, powinien być stosunkowo stabilny w czasie. Jeśli możesz wyodrębnić interfejs, który reprezentuje koncepcję rejestrowania, ten interfejs powinien być znacznie bardziej stabilny w czasie niż jego implementacja, a strony wywołujące powinny być znacznie mniej dotknięte zmianami, które możesz wprowadzić podczas utrzymywania lub rozszerzania tego mechanizmu rejestrowania.
Zależność implementacji od interfejsu daje możliwość wyboru w czasie wykonywania, która implementacja jest lepiej dopasowana do konkretnego środowiska. W zależności od przypadków może to być również interesujące.
Książki Agile Software Development, Principles, Patterns and Practices oraz Agile Principles, Patterns i Practices in C # są najlepszymi zasobami do pełnego zrozumienia pierwotnych celów i motywacji stojących za zasadą odwrócenia zależności. Artykuł „The Dependency Inversion Principle” jest również dobrym źródłem, ale ze względu na fakt, że jest to skondensowana wersja szkicu, który ostatecznie trafił do wcześniej wspomnianych książek, pomija ważną dyskusję na temat koncepcji posiadanie pakietu i interfejsu, które są kluczem do odróżnienia tej zasady od bardziej ogólnych zaleceń „programować do interfejsu, a nie implementacji”, które można znaleźć w książce Design Patterns (Gamma, et. al).
Podsumowując, zasada odwrócenia zależności polega przede wszystkim na odwróceniu konwencjonalnego kierunku zależności z komponentów „wyższego poziomu” na komponenty „niższego poziomu”, tak że komponenty „niższego poziomu” są zależne od interfejsów należących do komponentów „wyższego poziomu” . (Uwaga: komponent „wyższego poziomu” odnosi się tutaj do komponentu wymagającego zewnętrznych zależności / usług, niekoniecznie jego pozycji koncepcyjnej w architekturze warstwowej). W ten sposób sprzężenie nie jest tak bardzo zredukowane , jak przesunięte z komponentów, które są teoretycznie mniej wartościowe dla składników, które są teoretycznie bardziej wartościowe.
Osiąga się to poprzez projektowanie komponentów, których zewnętrzne zależności są wyrażone w postaci interfejsu, dla którego implementacja musi być zapewniona przez konsumenta komponentu. Innymi słowy, zdefiniowane interfejsy wyrażają to, czego potrzebuje komponent, a nie to, jak używasz komponentu (np. „INeedSomething”, a nie „IDoSomething”).
Zasada inwersji zależności nie odnosi się do prostej praktyki wyodrębniania zależności za pomocą interfejsów (np. MyService → [ILogger ⇐ Logger]). Chociaż powoduje to oddzielenie komponentu od szczegółów implementacji zależności, nie odwraca relacji między konsumentem a zależnością (np. [MyService → IMyServiceLogger] ⇐ Logger.
Znaczenie zasady odwracania zależności można sprowadzić do pojedynczego celu, jakim jest możliwość ponownego wykorzystania komponentów oprogramowania, które opierają się na zewnętrznych zależnościach w odniesieniu do części ich funkcjonalności (rejestrowanie, walidacja itp.)
W ramach tego ogólnego celu ponownego wykorzystania możemy wyróżnić dwa podtypy ponownego wykorzystania:
Używanie komponentu oprogramowania w wielu aplikacjach z implementacjami zależnymi (np. Opracowałeś kontener DI i chcesz zapewnić rejestrowanie, ale nie chcesz łączyć swojego kontenera z określonym rejestratorem, tak że każdy, kto używa twojego kontenera, musi również użyj wybranej biblioteki logowania).
Korzystanie z komponentów oprogramowania w kontekście ewolucji (np. Opracowano komponenty logiki biznesowej, które pozostają takie same w wielu wersjach aplikacji, w których szczegóły implementacji ewoluują).
W pierwszym przypadku ponownego wykorzystania komponentów w wielu aplikacjach, na przykład w przypadku biblioteki infrastruktury, celem jest zapewnienie klientom podstawowej infrastruktury bez łączenia ich z pod-zależnościami własnej biblioteki, ponieważ przyjmowanie zależności od takich zależności wymaga konsumenci również wymagają tych samych zależności. Może to być problematyczne, gdy konsumenci Twojej biblioteki zdecydują się użyć innej biblioteki do tych samych potrzeb infrastruktury (np. NLog vs. log4net) lub jeśli zdecydują się użyć późniejszej wersji wymaganej biblioteki, która nie jest wstecznie kompatybilna z wersją wymagane przez Twoją bibliotekę.
W drugim przypadku ponownego wykorzystania komponentów logiki biznesowej (tj. „Komponentów wyższego poziomu”) celem jest odizolowanie podstawowej implementacji domeny aplikacji od zmieniających się potrzeb szczegółów implementacji (np. Zmiana / aktualizacja bibliotek trwałości, bibliotek wiadomości , strategie szyfrowania itp.). W idealnym przypadku zmiana szczegółów implementacji aplikacji nie powinna powodować uszkodzenia składników hermetyzujących logikę biznesową aplikacji.
Uwaga: Niektórzy mogą sprzeciwić się opisaniu tego drugiego przypadku jako faktycznego ponownego wykorzystania, rozumując, że komponenty, takie jak komponenty logiki biznesowej używane w jednej rozwijającej się aplikacji, reprezentują tylko jedno użycie. Pomysł polega jednak na tym, że każda zmiana szczegółów implementacji aplikacji tworzy nowy kontekst, a zatem inny przypadek użycia, chociaż ostateczne cele można rozróżnić jako izolację i przenośność.
Chociaż przestrzeganie zasady odwracania zależności w tym drugim przypadku może przynieść pewne korzyści, należy zauważyć, że jej wartość w odniesieniu do nowoczesnych języków, takich jak Java i C #, jest znacznie ograniczona, być może do tego stopnia, że nie ma ona znaczenia. Jak wspomniano wcześniej, DIP polega na całkowitym rozdzieleniu szczegółów implementacji na osobne pakiety. Jednak w przypadku rozwijającej się aplikacji zwykłe wykorzystanie interfejsów zdefiniowanych w zakresie domeny biznesowej będzie chronić przed koniecznością modyfikowania komponentów wyższego poziomu ze względu na zmieniające się potrzeby komponentów szczegółów implementacji, nawet jeśli szczegóły implementacji ostatecznie znajdują się w tym samym pakiecie . Ta część zasady odzwierciedla aspekty, które były związane z językiem w perspektywie, gdy zasada została skodyfikowana (tj. C ++), a które nie są istotne dla nowszych języków. To mówi,
Dłuższe omówienie tej zasady w odniesieniu do prostego użycia interfejsów, iniekcji zależności i wzorca Separated Interface można znaleźć tutaj . Dodatkowo można znaleźć tutaj dyskusję na temat tego, jak ta zasada odnosi się do języków dynamicznie typowanych, takich jak JavaScript .
Projektując aplikacje oprogramowania, możemy brać pod uwagę klasy niskiego poziomu, które realizują podstawowe i podstawowe operacje (dostęp do dysku, protokoły sieciowe, ...), a klasy wysokiego poziomu - klasy, które hermetyzują złożoną logikę (przepływy biznesowe, ...).
Te ostatnie polegają na klasach niskiego poziomu. Naturalnym sposobem implementacji takich struktur byłoby pisanie klas niskopoziomowych, a gdy już mamy je do pisania złożonych klas wysokiego poziomu. Ponieważ klasy na wysokim poziomie są definiowane w kategoriach innych, wydaje się to logicznym sposobem. Ale to nie jest elastyczny projekt. Co się stanie, jeśli zajdzie potrzeba zastąpienia klasy niskiego poziomu?
Zasada odwrócenia zależności stwierdza, że:
Zasada ta ma na celu „odwrócenie” konwencjonalnego poglądu, że moduły wysokiego poziomu w oprogramowaniu powinny zależeć od modułów niższego poziomu. Tutaj moduły wysokiego poziomu są właścicielami abstrakcji (na przykład decydują o metodach interfejsu), które są implementowane przez moduły niższego poziomu. W ten sposób uzależniamy moduły niższego poziomu od modułów wyższego poziomu.
Dobrze zastosowana inwersja zależności zapewnia elastyczność i stabilność na poziomie całej architektury aplikacji. Umożliwi to bezpieczniejszą i stabilniejszą ewolucję aplikacji.
Tradycyjnie interfejs użytkownika architektury warstwowej zależał od warstwy biznesowej, a to z kolei zależało od warstwy dostępu do danych.
Musisz zrozumieć warstwę, pakiet lub bibliotekę. Zobaczmy, jak wyglądałby kod.
Mielibyśmy bibliotekę lub pakiet dla warstwy dostępu do danych.
// DataAccessLayer.dll
public class ProductDAO {
}
I kolejna logika biznesowa biblioteki lub warstwy pakietu, która zależy od warstwy dostępu do danych.
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private ProductDAO productDAO;
}
Odwrócenie zależności wskazuje, co następuje:
Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Obie powinny zależeć od abstrakcji.
Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji.
Jakie są moduły wysokiego poziomu i niskiego poziomu? Myśląc o modułach, takich jak biblioteki lub pakiety, moduł wysokiego poziomu to te, które tradycyjnie mają zależności i niski poziom, od których zależą.
Innymi słowy, wysoki poziom modułu byłby miejscem wywołania akcji, a niski - miejscem wykonywania akcji.
Rozsądny wniosek, jaki można wyciągnąć z tej zasady, jest taki, że nie powinno być zależności między konkrecjami, ale musi istnieć zależność od abstrakcji. Ale zgodnie z przyjętym podejściem możemy niewłaściwie zastosować zależność od inwestycji, ale abstrakcję.
Wyobraź sobie, że dostosowujemy nasz kod w następujący sposób:
Mielibyśmy bibliotekę lub pakiet dla warstwy dostępu do danych, która definiuje abstrakcję.
// DataAccessLayer.dll
public interface IProductDAO
public class ProductDAO : IProductDAO{
}
I kolejna logika biznesowa biblioteki lub warstwy pakietu, która zależy od warstwy dostępu do danych.
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private IProductDAO productDAO;
}
Chociaż jesteśmy uzależnieni od abstrakcji, zależność między biznesem a dostępem do danych pozostaje taka sama.
Aby uzyskać odwrócenie zależności, interfejs trwałości musi być zdefiniowany w module lub pakiecie, w którym znajduje się ta logika lub domena wysokiego poziomu, a nie w module niskiego poziomu.
Najpierw zdefiniuj, czym jest warstwa domeny, a abstrakcją jej komunikacji jest zdefiniowana trwałość.
// Domain.dll
public interface IProductRepository;
using DataAccessLayer;
public class ProductBO {
private IProductRepository productRepository;
}
Po warstwie trwałości zależy od domeny, należy teraz odwrócić, jeśli zależność jest zdefiniowana.
// Persistence.dll
public class ProductDAO : IProductRepository{
}
(źródło: xurxodev.com )
Ważne jest, aby dobrze przyswoić sobie tę koncepcję, pogłębiając cel i korzyści. Jeśli pozostaniemy mechanicznie i nauczymy się typowego repozytorium przypadków, nie będziemy w stanie zidentyfikować, gdzie możemy zastosować zasadę zależności.
Ale dlaczego odwracamy zależność? Jaki jest główny cel poza konkretnymi przykładami?
Zwykle pozwala to na częstsze zmiany rzeczy najbardziej stabilnych, które nie są zależne od rzeczy mniej stabilnych.
Łatwiej jest zmienić typ trwałości, czyli bazę danych lub technologię dostępu do tej samej bazy danych niż logika domeny lub akcje zaprojektowane do komunikacji z trwałością. Z tego powodu zależność jest odwrócona, ponieważ łatwiej jest zmienić trwałość, jeśli ta zmiana nastąpi. W ten sposób nie będziemy musieli zmieniać domeny. Warstwa domeny jest najbardziej stabilna ze wszystkich, dlatego nie powinna od niczego zależeć.
Ale nie ma tylko tego przykładu repozytorium. Istnieje wiele scenariuszy, w których ta zasada ma zastosowanie, i istnieją architektury oparte na tej zasadzie.
Istnieją architektury, w których inwersja zależności jest kluczem do jej definicji. We wszystkich domenach jest najważniejszy i to abstrakcje wskażą, jaki protokół komunikacyjny pomiędzy domeną a pozostałymi pakietami czy bibliotekami jest zdefiniowany.
W czystej architekturze domena znajduje się w centrum i jeśli spojrzy się w kierunku strzałek wskazujących na zależności, widać, jakie warstwy są najważniejsze i stabilne. Warstwy zewnętrzne są uważane za niestabilne narzędzia, więc unikaj polegania na nich.
(źródło: 8thlight.com )
Podobnie dzieje się z architekturą heksagonalną, w której domena znajduje się również w części centralnej, a porty są abstrakcjami komunikacji wychodzącej z domina na zewnątrz. Tutaj znowu widać, że domena jest najbardziej stabilna, a tradycyjna zależność jest odwrócona.
Dla mnie zasada odwrócenia zależności, opisana w oficjalnym artykule , jest naprawdę błędną próbą zwiększenia możliwości ponownego wykorzystania modułów, które są z natury mniej wielokrotnego użytku, a także sposobem na obejście problemu w języku C ++.
Problem w C ++ polega na tym, że pliki nagłówkowe zwykle zawierają deklaracje prywatnych pól i metod. Dlatego jeśli moduł C ++ wysokiego poziomu zawiera plik nagłówkowy modułu niskiego poziomu, będzie to zależeć od rzeczywistych szczegółów implementacji tego modułu. A to oczywiście nie jest dobre. Ale to nie jest problem w bardziej współczesnych językach powszechnie używanych dzisiaj.
Moduły wysokiego poziomu są z natury mniej przydatne do ponownego wykorzystania niż moduły niskiego poziomu, ponieważ te pierwsze są zwykle bardziej specyficzne dla aplikacji / kontekstu niż te drugie. Na przykład składnik implementujący ekran interfejsu użytkownika jest na najwyższym poziomie, a także bardzo (całkowicie?) Specyficzny dla aplikacji. Próba ponownego wykorzystania takiego komponentu w innym zastosowaniu przynosi efekt przeciwny do zamierzonego i może prowadzić jedynie do nadmiernej inżynierii.
Zatem stworzenie oddzielnej abstrakcji na tym samym poziomie komponentu A, który zależy od komponentu B (który nie zależy od A), może być wykonane tylko wtedy, gdy komponent A będzie naprawdę przydatny do ponownego wykorzystania w różnych aplikacjach lub kontekstach. Jeśli tak nie jest, zastosowanie DIP byłoby złym projektem.
Zasadniczo mówi:
Klasa powinna zależeć od abstrakcji (np. Interfejs, klasy abstrakcyjne), a nie od konkretnych szczegółów (implementacje).
Inni już podają dobre odpowiedzi i dobre przykłady.
Powodem, dla którego DIP jest ważny, jest to, że zapewnia on zasadę OO „luźno powiązany projekt”.
Obiekty w oprogramowaniu NIE powinny wchodzić w hierarchię, w której niektóre obiekty są obiektami najwyższego poziomu, zależnymi od obiektów niskiego poziomu. Zmiany w obiektach niskiego poziomu będą następnie przenikać do obiektów najwyższego poziomu, co sprawia, że oprogramowanie jest bardzo wrażliwe na zmiany.
Chcesz, aby obiekty „najwyższego poziomu” były bardzo stabilne i nie były wrażliwe na zmiany, dlatego musisz odwrócić zależności.
Znacznie jaśniejszym sposobem określenia zasady odwracania zależności jest:
Twoje moduły hermetyzujące złożoną logikę biznesową nie powinny bezpośrednio zależeć od innych modułów hermetyzujących logikę biznesową. Zamiast tego powinny polegać tylko na interfejsach do prostych danych.
To znaczy, zamiast implementować swoją klasę, Logic
jak to zwykle robią ludzie:
class Dependency { ... }
class Logic {
private Dependency dep;
int doSomething() {
// Business logic using dep here
}
}
powinieneś zrobić coś takiego:
class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
private Dependency dep;
...
}
class Logic {
int doSomething(Data data) {
// compute something with data
}
}
Data
i DataFromDependency
powinien mieszkać w tym samym module co Logic
, a nie z Dependency
.
Czemu to robić?
Dependency
zmiany, nie musisz ich zmieniać Logic
.Logic
robi, jest znacznie prostszym zadaniem: działa tylko na czymś, co wygląda jak ADT.Logic
można teraz łatwiej przetestować. Możesz teraz bezpośrednio tworzyć instancje Data
z fałszywymi danymi i przekazywać je. Nie ma potrzeby tworzenia prób ani skomplikowanego tworzenia szkieletów testowych.DataFromDependency
, który bezpośrednio się odwołuje Dependency
, znajduje się w tym samym module co Logic
, to Logic
moduł nadal bezpośrednio zależy od Dependency
modułu w czasie kompilacji. Według wujka Boba wyjaśnienie zasady , unikanie tego jest celem DIP. Zamiast tego, aby podążać za DIP, Data
powinien znajdować się w tym samym module co Logic
, ale DataFromDependency
powinien znajdować się w tym samym module co Dependency
.
Odwrócenie kontroli (IoC) to wzorzec projektowy, w którym obiekt otrzymuje swoją zależność przez zewnętrzną strukturę, zamiast prosić strukturę o swoją zależność.
Przykład pseudokodu używający tradycyjnego wyszukiwania:
class Service {
Database database;
init() {
database = FrameworkSingleton.getService("database");
}
}
Podobny kod wykorzystujący IoC:
class Service {
Database database;
init(database) {
this.database = database;
}
}
Korzyści z IoC to:
Celem odwrócenia zależności jest stworzenie oprogramowania wielokrotnego użytku.
Chodzi o to, że zamiast dwóch zależnych od siebie fragmentów kodu, opierają się one na jakimś abstrakcyjnym interfejsie. Następnie możesz ponownie użyć dowolnego elementu bez drugiego.
Najczęściej osiąga się to poprzez odwrócenie kontenera sterowania (IoC), takiego jak Spring w Javie. W tym modelu właściwości obiektów są ustawiane poprzez konfigurację XML, zamiast wychodzenia obiektów i znajdowania ich zależności.
Wyobraź sobie ten pseudokod ...
public class MyClass
{
public Service myService = ServiceLocator.service;
}
MyClass zależy bezpośrednio od klasy Service i klasy ServiceLocator. Potrzebuje obu, jeśli chcesz go używać w innej aplikacji. Teraz wyobraź sobie to ...
public class MyClass
{
public IService myService;
}
Teraz MyClass opiera się na jednym interfejsie, interfejsie IService. Pozwolilibyśmy, aby kontener IoC faktycznie ustawił wartość tej zmiennej.
Tak więc teraz MyClass można łatwo ponownie wykorzystać w innych projektach, bez przynoszenia wraz z nią zależności tych dwóch pozostałych klas.
Co więcej, nie musisz przeciągać zależności MyService i zależności tych zależności, a ... cóż, masz pomysł.
Jeśli możemy przyjąć za pewnik, że pracownik "wysokiego szczebla" w korporacji otrzymuje wynagrodzenie za wykonanie swoich planów i że plany te są dostarczane przez zagregowane wykonanie wielu planów pracowników "niskiego poziomu", to możemy powiedzieć generalnie okropny jest plan, jeśli opis planu pracownika wysokiego szczebla jest w jakikolwiek sposób powiązany z konkretnym planem dowolnego pracownika niższego szczebla.
Jeśli dyrektor wysokiego szczebla ma plan „skrócenia czasu dostawy” i wskazuje, że pracownik linii żeglugowej musi wypić kawę i robić ćwiczenia rozciągające każdego ranka, to plan ten jest silnie powiązany i ma niską spójność. Ale jeśli plan nie wspomina o żadnym konkretnym pracowniku, a w rzeczywistości po prostu wymaga, aby „podmiot, który może wykonywać pracę, jest przygotowany do pracy”, wówczas plan jest luźno powiązany i bardziej spójny: plany nie pokrywają się i można je łatwo zastąpić . Kontrahenci, czyli roboty, mogą z łatwością wymienić pracowników, a plan wysokiego szczebla pozostaje niezmieniony.
„Wysoki poziom” w zasadzie odwrócenia zależności oznacza „ważniejszy”.
Widzę, że powyższe odpowiedzi dają dobre wyjaśnienie. Chcę jednak podać proste wyjaśnienie za pomocą prostego przykładu.
Zasada odwrócenia zależności umożliwia programiście usunięcie zakodowanych na stałe zależności, dzięki czemu aplikacja staje się luźno powiązana i rozszerzalna.
Jak to osiągnąć: poprzez abstrakcję
Bez inwersji zależności:
class Student {
private Address address;
public Student() {
this.address = new Address();
}
}
class Address{
private String perminentAddress;
private String currentAdrress;
public Address() {
}
}
W powyższym fragmencie kodu obiekt adresu jest zakodowany na stałe. Zamiast tego, jeśli możemy użyć odwrócenia zależności i wstrzyknąć obiekt adresowy, przechodząc przez konstruktor lub metodę ustawiającą. Zobaczmy.
Z odwróceniem zależności:
class Student{
private Address address;
public Student(Address address) {
this.address = address;
}
//or
public void setAddress(Address address) {
this.address = address;
}
}
Odwrócenie zależności: zależy od abstrakcji, a nie konkrecji.
Odwrócenie kontroli: główna vs abstrakcja i jak główna jest spoiwem systemów.
Oto kilka dobrych postów, które mówią o tym:
https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/
https://coderstower.com/2019/04/02/main-and-abstraction-the-decoupled-peers/
https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/
Powiedzmy, że mamy dwie klasy: Engineer
i Programmer
:
Class Engineer ma zależność od klasy Programmer, jak poniżej:
class Engineer () {
fun startWork(programmer: Programmer){
programmer.work()
}
}
class Programmer {
fun work(){
//TODO Do some work here!
}
}
W tym przykładzie klasa Engineer
jest zależna od naszej Programmer
klasy. Co się stanie, jeśli będę musiał zmienić Programmer
?
Oczywiście muszę też zmienić Engineer
. (Wow, w tym momencie też OCP
jest naruszone)
Więc co musimy posprzątać ten bałagan? Właściwie odpowiedź brzmi: abstrakcja. Abstrakcji możemy usunąć zależność między tymi dwiema klasami. Na przykład mogę utworzyć Interface
klasę for Programmer i od teraz każda klasa, która chce używać tej klasy, Programmer
musi jej używać. Interface
Następnie zmieniając klasę Programmer, nie musimy zmieniać żadnych klas, które jej używały, Ze względu na abstrakcję, którą używany.
Uwaga: DependencyInjection
może pomóc nam zrobić DIP
i SRP
zbyt.
Dodając do lawiny ogólnie dobrych odpowiedzi, chciałbym dodać własną małą próbkę, aby zademonstrować dobre i złe praktyki. I tak, nie jestem kimś, kto rzuca kamieniami!
Powiedzmy, że potrzebujesz małego programu do konwersji ciągu znaków na format base64 za pośrednictwem konsoli we / wy. Oto naiwne podejście:
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())));
}
}
DIP zasadniczo mówi, że komponenty wysokopoziomowe nie powinny być zależne od implementacji niskiego poziomu, gdzie „poziom” to odległość od I / O według Roberta C. Martina („Czysta architektura”). Ale jak wydostać się z tej kłopotliwej sytuacji? Po prostu uzależniając centralny Enkoder tylko od interfejsów, nie martwiąc się o ich implementację:
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);
}
}
Zauważ, że nie musisz dotykać GoodEncoder
, aby zmienić tryb I / O - ta klasa jest zadowolona z interfejsów I / O, które zna; wszelkie niskopoziomowe implementacje IReadable
i IWriteable
nigdy nie będą im przeszkadzać.
GoodEncoder
twoim drugim przykładzie. Aby stworzyć przykład DIP, musisz wprowadzić pojęcie o tym, co „jest właścicielem” interfejsów, które tutaj wyodrębniłeś - aw szczególności umieścić je w tym samym pakiecie co GoodEncoder, podczas gdy ich implementacje pozostają na zewnątrz.
Mówi o tym zasada odwrócenia zależności (DIP)
i) Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Obie powinny zależeć od abstrakcji.
ii) Abstrakcje nigdy nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji.
Przykład:
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();
}
}
Uwaga: Klasa powinna zależeć od abstrakcji, takich jak interfejs lub klasy abstrakcyjne, a nie od konkretnych szczegółów (implementacja interfejsu).