Ответ 1
Давайте создадим вымышленный пример.
Класс 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
Но что, если мы хотим заменить 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
wihtin 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
-implementations:
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
.
Adendum
Я написал этот ответ более двух лет назад. Будучи в целом удовлетворенным ответом, я всегда думал, что чего-то не хватает, и я, наконец, знаю, что это было. Следующее не обязательно, чтобы понять ответ, но призвано вызвать интерес у читателя, а также предоставить некоторые ресурсы для дальнейшего самообразования.
В литературе этот подход известен как принцип разделения интерфейсов и относится к SOLID -principles. На YouTube есть хорошая беседа от дяди Боба (интересный бит длиной около 15 минут), показывающий, как полиморфизм и интерфейсы можно использовать, чтобы позволить зависимости времени компиляции указывать на поток управления (рекомендуется на усмотрение зрителя, дядя Боб будет мягко рассуждаю про Java). Это, в свою очередь, означает, что реализация высокого уровня не должна знать о реализациях более низкого уровня, когда они сегрегируются через интерфейсы. Таким образом, более низкие уровни можно менять по желанию, как мы показали выше.