Что означает разделение двух классов на уровне интерфейса?

Допустим, у нас есть класс A в пакете A и класс B в пакете B. Если объект класса A имеет ссылку на класс B, то говорят, что два класса имеют связь между собой.

Чтобы устранить связь, рекомендуется определить интерфейс в пакете A, который реализуется классом в пакете B. Тогда объект класса A может ссылаться на интерфейс в пакете A. Это часто является примером «инверсии зависимости».

Является ли это примером «разделения двух классов на уровне интерфейса». Если да, то как он удаляет связь между классами и сохраняет ту же функциональность, когда два класса были связаны?


person Sarit Adhikari    schedule 11.06.2015    source источник
comment
Я настоятельно рекомендую публикации Роберта С. Мартина о правилах SOLID в объектно-ориентированном программировании. Вот некоторые подробности о вашей проблеме: objectmentor.com/resources/articles/dip.pdf   -  person Maciej Baranowski    schedule 11.06.2015


Ответы (4)


Давайте создадим воображаемый пример двух классов A и B.

Класс A в пакете packageA:

package packageA;

import packageB.B;

public class A {
    private B myB;
    
    public A() {
        this.myB = new B();
    }
    
    public void doSomethingThatUsesB() {
        System.out.println("Doing things with myB");
        this.myB.doSomething();
    }
}

Класс B в пакете packageB:

package packageB;

public class B {
    public void doSomething() {
        System.out.println("B did something.");
    }
}

Как видим, A зависит от B. Без B нельзя использовать A. Мы говорим, что A тесно связано с B. Что, если в будущем мы захотим заменить B на BetterB? Для этого мы создаем Интерфейс Inter внутри packageA:

package packageA;

public interface Inter {
    public void doSomething();
}

Чтобы использовать этот интерфейс, мы

  • import packageA.Inter; и пусть B implements Inter в B и
  • Замените все вхождения B в пределах A на Inter.

Результатом является эта модифицированная версия A:

package packageA;

public class A {
    private Inter myInter;
    
    public A() {
        this.myInter = ???; // What to do here?
    }
    
    public void doSomethingThatUsesInter() {
        System.out.println("Doing things with myInter");
        this.myInter.doSomething();
    }
}

Мы уже видим, что зависимость от A до B исчезла: import packageB.B; больше не нужен. Есть только одна проблема: мы не можем создать экземпляр интерфейса. Но на помощь приходит инверсия управления: вместо создания экземпляра типа Inter в конструкторе A, конструктор потребует что-то, что implements Inter в качестве параметра:

package packageA;

public class A {
    private Inter myInter;
    
    public A(Inter myInter) {
        this.myInter = myInter;
    }
    
    public void doSomethingThatUsesInter() {
        System.out.println("Doing things with myInter");
        this.myInter.doSomething();
    }
}

При таком подходе теперь мы можем изменить конкретную реализацию Inter в A по желанию. Предположим, мы пишем новый класс BetterB:

package packageB;

import packageA.Inter;

public class BetterB implements Inter {
    @Override
    public void doSomething() {
        System.out.println("BetterB did something.");
    }
}

Теперь мы можем создавать экземпляры A с различными реализациями Inter:

Inter b = new B();
A aWithB = new A(b);
aWithB.doSomethingThatUsesInter();

Inter betterB = new BetterB();
A aWithBetterB = new A(betterB);
aWithBetterB.doSomethingThatUsesInter();

И нам не пришлось ничего менять внутри A. Код теперь отделен, и мы можем изменить конкретную реализацию Inter по своему желанию, если контракт(ы) Inter выполняется(ются). В частности, мы можем поддерживать код, который будет написан в будущем и реализует Inter.


Приложение

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

В литературе этот подход известен как принцип разделения интерфейса и относится к SOLID-принципы. На YouTube есть хорошее выступление дяди Боба (самое интересное длится около 15 минут) показывая, как можно использовать полиморфизм и интерфейсы, чтобы позволить точке зависимости времени компиляции противостоять потоку управления (рекомендуется на усмотрение зрителя, дядя Боб будет мягко разглагольствовать о Java). Это, в свою очередь, означает, что реализации высокого уровня не нужно знать о реализациях более низкого уровня, когда они разделены через интерфейсы. Таким образом, нижние уровни можно менять местами по желанию, как мы показали выше.

person Turing85    schedule 11.06.2015
comment
+1 На помощь приходит инверсия управления. Это лучшая развязка и инверсия отношения управления, которую я когда-либо видел. И я читал книги на эту тему. - person AFP_555; 29.04.2016
comment
Но разве класс А теперь не зависит от интерфейса? Вы только что отделили его от B и соединили с интерфейсом, верно? Как это лучше? - person 11m0; 16.08.2017
comment
Любой разработчик может реализовать интерфейс и внедрить его через инверсию управления без необходимости изменять приведенный выше код. Это правда, что Aзависит от Inter, поэтому изменения в Inter могут нарушить работу всего кода, реализующего Inter. Вот почему интерфейсы должны быть разработаны тщательно. Есть некоторые возможности для добавления методов к интерфейсу позже, например. через методы по умолчанию. - person Turing85; 16.08.2017
comment
@ Turing85 Спасибо. Хорошо объяснил. - person Muhammad Maqsood; 15.10.2017
comment
Большое спасибо. Это одно из самых четких объяснений этой концепции в Интернете. - person Zeecitizen; 08.06.2018

Представьте, что функциональность B заключается в записи журнала в какую-то базу данных. Класс B зависит от функциональности класса DB и предоставляет некоторый интерфейс для ведения журналов другим классам.

Класс A нуждается в функциях ведения журнала B, но ему все равно, куда записывается журнал. Ему все равно на DB, но поскольку он зависит от B, он также зависит и от DB. Это не очень желательно.

Итак, что вы можете сделать, так это разделить класс B на два класса: абстрактный класс L, описывающий функциональность ведения журнала (и не зависящий от DB), и реализацию, зависящую от DB.

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

Так как A теперь зависит только от худого L, вы можете легко использовать его с другим механизмом регистрации, не зависящим от DB. Например. вы можете создать простой консольный регистратор, реализующий интерфейс, определенный в L.

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

person Gregor Raýman    schedule 11.06.2015

Описанная вами ситуация устраняет зависимость класса A от конкретной реализации класса B и заменяет ее интерфейсом. Теперь класс A может принимать любой объект типа, реализующего интерфейс, а не только класс B. Дизайн сохраняет ту же функциональность, поскольку класс B создан для реализации этого интерфейса.

person Bill the Lizard    schedule 11.06.2015

Именно здесь фреймворки DI (внедрение зависимостей) действительно проявляют себя.

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

Например...

Ваш ServiceA будет строить свою логику вокруг интерфейса ServiceB, и ему не придется беспокоиться о том, что происходит под капотом ServiceB.

Это позволяет создавать несколько реализаций ServiceB без изменения какой-либо логики в ServiceA.

Для примера

interface ServiceB { void doMethod() }

Вы можете взаимодействовать с ServiceB в ServiceA, не зная, что скрывается за ServiceB.

class ServiceAImpl {

    private final ServiceB serviceB;
    
    public ServiceAImpl(ServiceBImpl serviceBImpl) {
        this.serviceB = serviceBImpl
    }

    public void doSomething() {
        serviceB.doMethod(); // calls ServiceB interface method.
    }

}

Теперь, поскольку вы построили ServiceA с использованием контракта, указанного в ServiceB, вы можете изменить реализацию по своему усмотрению.

Вы можете смоделировать службу, создать другую логику подключения к разным базам данных, создать другую логику времени выполнения. Все это может измениться и никак не повлияет на то, как ServiceA взаимодействует с ServiceB.

Таким образом, слабая связь достигается с помощью IoC (инверсия управления). Теперь у вас есть модульная и целенаправленная кодовая база.

person shinjw    schedule 03.09.2020