Понимание внедрения зависимости в контексте двух простых классов - PullRequest
0 голосов
/ 23 января 2019

У меня были проблемы с захватом инъекций зависимостей (или позвольте мне сказать, что это полезно). Поэтому я решил написать два простых кода: один без DI, а другой с ним.

Итак, у меня есть класс A

public class A {
    public void foo(){
        B b = new B();
        b.fooB();
    }
}

как видно выше A зависит от B, B, что

public class B {
    public void fooB(){
        Log.e("s", "y");
    }
}

и мы можем использовать A как

public void do(){
    A a = new A();
    a.foo();
}

Но говорят, что A не должен просто инициализировать B, потому что это зависит от него, однако у нас должна быть служба, имеющая какие-то контракты между двумя классами. Например, если я ошибаюсь, пожалуйста, дайте мне знать

Итак, давайте иметь интерфейс BService

public interface BService {
    void fooB();
}

А B становится DiB

public class DiB implements BService {
    @Override
    public void fooB(){
        Log.e("s", "y");
    }
}

И A становится DiA

public class DiA {
    BService bService;

    public DiA(BService bService){
        this.bService = bService;
    }

    public void foo(){
        bService.fooB();
    }
}

и мы можем использовать A как

public void dIdo(){
        BService service = new diB();
        diA a = new diA(service);
        a.foo();
}

Итак, я прочитал о преимуществах DI:

  1. Тестируемые коды: потому что я могу на самом деле протестировать оба кода в JUnit (я не хочу размещать тест здесь, чтобы избежать длинного вопроса)
  2. Развязка: сказано, что если класс B изменится, то A не будет затронут, и я не могу этого понять, потому что, если я изменю fooB() в классе B на fooB2(), мне придется измените метод переопределения в BService, что, в свою очередь, означает, что мне придется изменить его в классе A

Оба кода, кажется, работают нормально, и я не могу понять преимущества одного над другим, только то, что другой более сложный. Поэтому, пожалуйста, не могли бы вы рассказать мне больше о преимуществах в контексте этих простых A и B классов. Что я не получаю?

Ответы [ 2 ]

0 голосов
/ 23 января 2019

Разделение: сказано, что если класс B изменяется, то на A это не должно повлиять, и я не могу этого понять, потому что, если я заменю fooB () в классе B на fooB2 (), мне придется изменить метод переопределения в BService. что, в свою очередь, означает, что мне придется изменить его в классе A

Думаю, как только вы поймете это, вы поймете всю концепцию.

Попытайтесь представить интерфейсы, которые вы предоставляете, как контракты между различными компонентами вашей системы.

Объявляя BService с помощью метода fooB(), вы говорите, что любой компонент, который соблюдает этот контракт (например, реализует интерфейс), способен выполнять объявленную работу по-своему по-своему до тех пор, пока это не нарушает договор.

Компонент A не должен быть интересным в , как BService выполняет свою работу, поскольку A достаточно, чтобы знать, что работа будет выполнена.

Тогда вы сможете создать еще одну реализацию BService, которая могла бы выполнять необходимую работу совершенно по-другому. Вы можете перенастроить ваш IoC, чтобы добавить новую реализацию в A, и это все. Вы не изменили A, но изменили способ его работы.

Давайте рассмотрим другой пример:

Предположим, у вас есть Repository интерфейс, который может сохранять / извлекать что угодно по некоторому строковому идентификатору (для простоты).

interface Repository {
    Object retrieve(String identifier);
    void store(String identifier, Object content);
}

И у вас может быть пара компонентов, которые используют этот репозиторий для манипулирования некоторыми данными:

class DocumentStorage {
    private int seqNo = 1;
    private Repository repository;

    public void saveMyDocuments(Iterable<Document> documents) {
         for (Document document : documents) {
             repository.store("DocumentStorage" + ++seqNo, document);
         }
    }
}

И

class RuntimeMetrics {
   private Repository repository;

   public void saveFreeMemoryAmount() {
       repository.store("MEM", Runtime.getRuntime().freeMemory());
   }
}

Теперь эти компоненты понятия не имеют о том, как хранилище будет сохранять документы, они просто знают, что так и будет.

И вы можете реализовать хранилище в памяти:

class InMemoryRepository implements Repository {
    private final java.util.Map<Integer, Object> mem = new java.util.HashMap<>();
    @Override
    Object retrieve(Integer identifier) {
        return mem.get(identifier);
    }

    @Override
    void store(Integer identifier, Object content) {
        mem.put(identifier, content);
    }
}

и жить с этим.

Теперь в какой-то момент времени вы можете решить, что документы слишком важны для хранения в памяти, и вам придется хранить их в файле, базе данных или в другом месте.

Вы реализуете DatabaseRepository в соответствии с контрактом Repository, перенастройте свой DI-контейнер и BOOM, ваши документы теперь хранятся в базе данных. Вы ничего не изменили в DocumentStorage, RuntimeMetrics, все еще используя InMemoryRepository для управления его данными.

Аналогичным образом вы можете протестировать DocumentStorage, заменив Repository поддельной реализацией вместо запуска всего сервера базы данных.

Это главное преимущество DI.

0 голосов
/ 23 января 2019

Вам не нужно, чтобы созданные вами интерфейсы классифицировались как внедрение зависимостей. Этот класс использует внедрение зависимостей:

public class A {
    private final B b;

    public A(B b) {
        this.b = b;
    }

    public void foo(){
        b.fooB();
    }
}

Не думай об этом. «Внедрение зависимости» звучит как сложная концепция, когда вы ее не понимаете, но название фактически описывает концепцию идеально и кратко. Давайте разберемся с этим:

Зависимость : Вещи, на которые что-то полагается
Инъекция : акт помещения чего-то внешнего внутрь чего-то другого

В приведенном выше примере мы помещаем вещи, на которые полагаемся, в наш класс извне? Да, мы используем инъекцию зависимостей. Реализует ли наш класс интерфейс или нет, не имеет значения.

Существуют веские причины, по которым реализация классов в интерфейсах является хорошей идеей, но это имеет отношение к внедрению зависимостей. Не путайте эти вещи и не думайте, что это требование.


Адресация к тестируемости: да, в вашей версии без внедрения зависимостей мы можем протестировать A и B. Мы не можем проверить A в отрыве от B, но что с того? Кто сказал, что мы бы хотели? Что это даст нам?

Ну, предположим, B не так тривиально. Предположим, что B читает из базы данных и возвращает некоторое значение. Мы не хотим, чтобы наши модульные тесты для A опирались на базу данных, потому что A не заботится о базах данных, он просто заботится о возможности fooB. К сожалению, если за создание B отвечает A, у нас нет способа изменить это поведение. Он может делать только одно, и в нашем производственном коде он нам нужен для создания B, который взаимодействует с базой данных, так что это то, с чем мы застряли.

Однако, если бы мы вводили зависимость, мы могли бы сделать это в нашем реальном коде:

new A(new DatabaseB());

и добавьте 'поддельные' или 'фиктивные' в наших тестах, которые действуют как , они обращаются к базе данных, фактически не делая этого:

new A(mockB);
new A(fakeB);

Это позволяет нам использовать A двумя различными способами: с базой данных и без нее; для производственного кода и для тестового кода. Это дает нам возможность выбора.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...