DI - Dependency Inversion Principle

Принципът на инверсия на зависимостта (DIP) представлява част от обектно ориентираните принципи SOLID

DIP е проста – но въпреки това мощна – програмна парадигма, която можем да използва, за внедряване на добре структурирани, силно независими софтуерни компоненти за многократна употреба.

Когато не се използва DI софтуерните компоненти са плътно обвързани един с друг. Оттук те трудно се използват повторно, заменят и тестват, което води до сложни за подобрение дизайни.

Софтуера трябва да отговаря на следните правила за да изпълнява DI:

  1. Модулите на високо ниво не трябва да зависят от нископоставените модули. И двете трябва да зависят от абстракции.

  2. Абстракциите не трябва да зависят от подробности. Подробностите трябва да зависят от абстракциите

Oсновата на DIP е да обръщане на класическата зависимост между компонентите на високо ниво и ниско ниво, като се абстрахират от взаимодействието помежду си.

В традиционната разработка на софтуер компонентите на високо ниво зависят от такива от ниско ниво. По този начин е трудно да използвате повторно компонентите на високо ниво.

Използване на DIP

Следния пример се състой от клас StringProcessor, който получава стойност String с помощта на компонент на StringReader, и го записва с помощта на компонент на StringWriter:

public class StringProcessor {
    private final StringReader stringReader;
    private final StringWriter stringWriter;

    public StringProcessor(StringReader stringReader, StringWriter stringWriter) {
        this.stringReader = stringReader;
        this.stringWriter = stringWriter;
    }

    public void printString() {
        stringWriter.write(stringReader.getValue());
    }
}

Реализацията на този клас може да стане по няколко начина:

  1. StringReader и StringWriter, компонентите на ниско ниво, са твърдо зададени класове, поставени в една и същи пакет. StringProcessor, компонентът на високо ниво се поставя в различен пакет. StringProcessor зависи от StringReader и StringWriter. Няма инверсия на зависимости, оттам StringProcessor не може да се използва повторно в различен контекст.

  2. StringReader и StringWriter са интерфейси, поставени в една и същи пакет заедно с класа който ги имплементира. StringProcessor сега зависи от абстракции, но компонентите на ниско ниво не. Все още не сме постигнали инверсия на зависимостите.

  3. StringReader и StringWriter са интерфейси, поставени в един и същи пакет заедно с StringProcessor. Сега, StringProcessor има изрична собственост върху абстракциите. StringProcessor, StringReader и StringWriter всички зависят от абстракции. Постигнахме инверсия на зависимости от горе до долу, като абстрахиране на взаимодействието между компонентите. StringProcessor сега е многократна за използване в различен контекст.

  4. StringReader и StringWriter са интерфейси, поставени в отделен пакет от StringProcessor. Постигнахме инверсия на зависимостите и също така е по-лесно да заменим имплементациите на StringReader и StringWriter. StringProcessor също е много по използваем за различени контексти.

От всички горепосочени сценарии само елементи 3 и 4 са валидни внедрявания на DIP.

Точка 3 е пряко DIP изпълнение, където компонентът на високо ниво и абстракциите са поставени в един и същи пакет. От тук компонентът на високо ниво притежава абстракциите. При това изпълнение компонентът на високо равнище отговаря за определянето на абстрактния протокол, чрез който взаимодейства с компонентите на ниско ниво.

По същия начин т. 4 е по-добро DIP изпълнение. В този вариант на модела нито компонентът на високо ниво, нито нископоставените имат собствеността върху абстракциите.

Абстракциите се поставят в отделен слой, което улеснява превключването на компонентите на ниско ниво. В същото време всички компоненти са изолирани един от друг, което дава по-силна капсулиация.

Избор на правилното ниво на абстракция

В повечето случаи изборът на абстракциите, които компонентите на високо ниво ще използват, трябва да бъде доста ясен, но с едно предупреждение, което си заслужава да се отбележи: нивото на абстракция.

В примера по-горе използвахме DI, за да инжектираме тип StringReader в класа StringProcessor. Това би било ефективно, стига нивото на абстракция на StringReader да е близо до домейна на StringProcessor.

За разлика от това, просто бихме пропуснали присъщите ползи на DIP, ако StringReader например е обект на файл, който чете стойност на String от файл. В този случай нивото на абстракция на StringReader би било много по-ниско от нивото на домейна на StringProcessor.

Казано по-просто, нивото на абстракция, което компонентите на високо ниво ще използват, за да си сътрудничат с тези от ниско ниво, трябва винаги да са близо до домейна на първите.

Пряко изпълнение на DIP

Нека разгледаме следния пример, който да предоставя услуги на клиенти:

Основното хранилище на слоя обикновено е база данни, но за да поддържаме кода прост, тук ще използваме обикновена колекция.

Нека започнем с определяне на компонента на високо ниво:

public class CustomerService {
    private final CustomerDao customerDao;
    
    // standard constructor / getter
    
    public Optional<Customer> findById(int id) {
        return customerDao.findById(id);
    }
    
    public List<Customer> findAll() {
        return customerDao.findAll();
    }
}

CustomerService класът прилага методите findById() и findAll() , които донасят клиентите от по нисък слои.

В този случай типът CustomerDao е абстракцията, която CustomerService използва за консумиране на компонента от ниско ниво.

Тъй като това е директно DIP изпълнение, нека определим абстракцията като интерфейс в същия пакет на CustomerService:

public interface CustomerDao {
    Optional<Customer> findById(int id);
    
    List<Customer> findAll();
}

Поставяйки абстракцията в същия пакет на компонента на високо ниво, правим компонента отговорен за притежаването на абстракцията. Този детайл на внедряване е това, което наистина обръща зависимостта между компонента на високо ниво и този на ниско ниво.

Освен това нивото на абстракция на CustomerDao е близо до тази на CustomerService, която се изисква и за добър DIP.

Сега, нека създадем компонента на ниско ниво в различен пакет. В този случай това е просто основно внедряване на CustomerDao:

public class SimpleCustomerDao implements CustomerDao {
    // standard constructor / getter
    
    @Override
    public Optional<Customer> findById(int id) {
        return Optional.ofNullable(customers.get(id));
    }
    
    @Override
    public List<Customer> findAll() {
        return new ArrayList<>(customers.values());
    }
}

Тази реализация позволява класа SimpleCustomerDao да бъде подменян прио промяна на изискването без това да влияе на класа от високо ниво, което улеснява процеса по тестване и внедряване на софтуера.

Last updated