Утверждение, что «наследование от базового класса позволяет вам наследовать BEHAVIOR, тогда как реализация интерфейса позволяет только указывать ВЗАИМОДЕЙСТВИЕ», абсолютно верно.
Но что более важно, интерфейсы позволяют статически типизированным языкам продолжать поддерживать полиморфизм. Объектно-ориентированный пурист будет настаивать на том, что язык должен обеспечивать наследование, инкапсуляцию, модульность и полиморфизм, чтобы быть полнофункциональным объектно-ориентированным языком. В динамически типизированных - или утиных - языках (таких как Smalltalk) полиморфизм тривиален; однако в статически типизированных языках (таких как Java или C #) полиморфизм далек от тривиальности (фактически, на первый взгляд, он противоречит понятию строгой типизации.)
Позвольте мне продемонстрировать:
В динамически типизированном (или типизируемом) языке (например, Smalltalk) все переменные являются ссылками на объекты (не меньше и не более). Итак, в Smalltalk я могу сделать следующее:
|anAnimal|
anAnimal := Pig new.
anAnimal makeNoise.
anAnimal := Cow new.
anAnimal makeNoise.
Этот код:
- Объявляет локальную переменную с именем anAnimal (обратите внимание, что мы НЕ УКАЗЫВАЕМ ТИП переменной - все переменные являются ссылками на объект, не больше и не меньше.)
- Создает новый экземпляр класса с именем "Свинья"
- Назначает этот новый экземпляр Duck переменной anAnimal.
- Отправляет сообщение
makeNoise
свинье.
- Повторяет все это, используя корову, но присваивая ее той же переменной, что и Свинья.
Тот же код Java будет выглядеть примерно так (при условии, что Duck и Cow являются подклассами Animal:
Animal anAnimal = new Pig();
duck.makeNoise();
anAnimal = new Cow();
cow.makeNoise();
Это все хорошо, пока мы не представим класс Овощной. Овощи имеют то же поведение, что и животные, но не все. Например, могут расти как животные, так и овощи, но овощи явно не шумят, а животных нельзя добывать.
В Smalltalk мы можем написать это:
|aFarmObject|
aFarmObject := Cow new.
aFarmObject grow.
aFarmObject makeNoise.
aFarmObject := Corn new.
aFarmObject grow.
aFarmObject harvest.
Это прекрасно работает в Smalltalk, потому что он имеет тип утки (если он ходит как утка, и крякает как утка - это утка). В этом случае, когда сообщение отправляется объекту, поиск выполняется в списке методов получателя, и, если найден соответствующий метод, он вызывается. Если нет, генерируется какое-то исключение NoSuchMethodError - но все это делается во время выполнения.
Но в Java, статически типизированном языке, какой тип мы можем назначить нашей переменной? Кукуруза должна наследоваться от Овощей, чтобы поддерживать рост, но не может наследоваться от Животных, потому что она не производит шума. Корова должна унаследовать от Animal для поддержки makeNoise, но не может наследовать от Vegetable, потому что она не должна реализовывать урожай. Похоже, нам нужно множественное наследование - возможность наследовать от более чем одного класса. Но это оказывается довольно сложной функцией языка из-за всех всплывающих случаев (что происходит, когда несколько параллельных суперклассов реализуют один и тот же метод? И т. Д.)
Вдоль интерфейсов ...
Если мы создадим классы Animal и Vegetable, каждый из которых реализует Growable, мы можем объявить, что наша Cow - это Animal, а наша Corn - это Vegetable. Мы также можем заявить, что как Животное, так и Овощное растение. Это позволяет нам написать это, чтобы вырастить все:
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.
Итак .... подведем итог:
Реализация интерфейса позволяет вам указывать, что могут делать объекты (взаимодействие), а наследование классов позволяет вам определять, как все должно быть сделано (реализация).
Интерфейсы дают нам много преимуществ «истинного» полиморфизма, не жертвуя проверкой типа компилятора.