Ответ 1
Утверждение, что "наследование из базового класса позволяет наследовать BEHAVIOR, тогда как реализация интерфейса позволяет вам указывать INTERACTION" абсолютно верно.
Но что более важно, интерфейсы позволяют статически типизированным языкам продолжать поддерживать полиморфизм. Объектно-ориентированный пурист настаивал бы на том, что язык должен обеспечивать наследование, инкапсуляцию, модульность и полиморфизм, чтобы быть полнофункциональным объектно-ориентированным языком. В динамически типизированных или утиных типизированных языках (например, Smalltalk) полиморфизм тривиален; однако в статически типизированных языках (например, Java или С#) полиморфизм далек от тривиального (на самом деле, на первый взгляд, он не согласен с понятием сильной типизации).
Позвольте мне продемонстрировать:
На динамически типизированном (или утином) языке (например, Smalltalk) все переменные являются ссылками на объекты (не что иное и не более.) Итак, в Smalltalk я могу это сделать:
|anAnimal|
anAnimal := Pig new.
anAnimal makeNoise.
anAnimal := Cow new.
anAnimal makeNoise.
Этот код:
- Объявляет локальную переменную, называемую anAnimal (обратите внимание, что мы НЕ указываем TYPE переменной - все переменные являются ссылками на объект, не более и не менее.)
- Создает новый экземпляр класса с именем "Pig"
- Назначает новый экземпляр Duck переменной anAnimal.
- Отправляет сообщение
makeNoise
свиньи. - Повторяет все это с помощью коровы, но присваивает ей ту же самую точную переменную, что и Pig.
Тот же Java-код будет выглядеть примерно так (делая предположение, что Duck and Cow являются подклассами Animal:
Animal anAnimal = new Pig();
duck.makeNoise();
anAnimal = new Cow();
cow.makeNoise();
Это все хорошо и хорошо, пока мы не представим класс Vegetable. Овощи имеют то же поведение, что и животное, но не все. Например, как животные, так и овощи могут расти, но, очевидно, овощи не производят шума, и животные не могут быть собраны.
В Smalltalk мы можем написать это:
|aFarmObject|
aFarmObject := Cow new.
aFarmObject grow.
aFarmObject makeNoise.
aFarmObject := Corn new.
aFarmObject grow.
aFarmObject harvest.
Это хорошо работает в Smalltalk, потому что он утиный (если он ходит как утка, а quacks - как утка - это утка.) В этом случае, когда сообщение отправляется объекту, поиск выполняется в списке методов приемника, и если найден метод сопоставления, он вызывается. Если нет, то возникает какое-то исключение NoSuchMethodError, но все это делается во время выполнения.
Но на Java, статически типизированный язык, какой тип мы можем назначить нашей переменной? Кукуруза должна унаследовать от Vegetable, чтобы поддержать рост, но не может унаследовать от Animal, потому что она не шумит. Корова должна наследовать от Animal для поддержки makeNoise, но не может наследовать от Vegetable, потому что она не должна внедрять урожай. Похоже, нам нужно многократное наследование - способность наследовать более чем один класс. Но это оказывается довольно сложной функцией языка из-за всех появляющихся красных случаев (что происходит, когда несколько параллельных суперклассов реализуют один и тот же метод?) И т.д.)
Вперед интерфейсы...
Если мы создадим классы животных и овощей, каждый из которых будет реализовывать Growable, мы можем заявить, что наша Корова - это Животное, а наша кукуруза - Овощи. Мы также можем заявить, что как животные, так и овощи растут. Это позволяет нам писать это, чтобы вырастить все:
List<Growable> list = new ArrayList<Growable>();
list.add(new Cow());
list.add(new Corn());
list.add(new Pig());
for(Growable g : list) {
g.grow();
}
И это позволяет нам делать это, чтобы сделать шумы животных:
List<Animal> list = new ArrayList<Animal>();
list.add(new Cow());
list.add(new Pig());
for(Animal a : list) {
a.makeNoise();
}
Самым большим преимуществом для языка с утиным языком является то, что вы получаете действительно хороший полиморфизм: все, что должен сделать класс, чтобы обеспечить поведение, - это предоставить метод (есть и другие компромиссы, но это большой вопрос при обсуждении набора текста). Пока все играют хорошо, и только отправляет сообщения, которые соответствуют определенным методам, все хорошо. Недостатком является то, что вид ошибки ниже не доходит до времени выполнения:
|aFarmObject|
aFarmObject := Corn new.
aFarmObject makeNoise. // No compiler error - not checked until runtime.
Статически типизированные языки обеспечивают гораздо лучшее "программирование по контракту", потому что они поймают два типа ошибок ниже во время компиляции:
Animal farmObject = new Corn(); // Compiler error: Corn cannot be cast to Animal.
farmObject makeNoise();
-
Animal farmObject = new Cow();
farmObject.harvest(); // Compiler error: Animal doesn't have the harvest message.
Итак... чтобы подвести итог:
-
Реализация интерфейса позволяет вам указать, какие объекты могут выполнять объекты (взаимодействие) и наследование классов позволяет вам указать, как это должно быть сделано (реализация).
-
Интерфейсы дают нам много преимуществ "истинного" полиморфизма, не жертвуя проверкой типа компилятора.