Давайте создадим воображаемый пример двух классов 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