Інтерфейси дозволяють статично набраним мовам підтримувати поліморфізм. Пурист, орієнтований на об'єкти, наполягає на тому, що мова повинна забезпечувати успадкування, інкапсуляцію, модульність та поліморфізм, щоб бути повноцінною об'єктно-орієнтованою мовою. У мовах, що динамічно набираються - або качками (як Smalltalk), поліморфізм є тривіальним; однак у статично типових мовах (на зразок Java чи C #,) поліморфізм далеко не тривіальний (насправді на поверхні це, здається, суперечить поняттю сильного набору тексту).
Дозвольте продемонструвати:
У мові, що динамічно набирається (або набирається качка) (як Smalltalk), всі змінні є посиланнями на об'єкти (не менше і нічого більше.) Отже, у Smalltalk я можу це зробити:
|anAnimal|
anAnimal := Pig new.
anAnimal makeNoise.
anAnimal := Cow new.
anAnimal makeNoise.
Цей код:
- Заявляє локальну змінну під назвою anAnimal (зауважте, що ми НЕ вказуємо ТИП змінної - всі змінні є посиланнями на об'єкт, не більше і не менше.)
- Створює новий екземпляр класу під назвою "Pig"
- Призначає цей новий екземпляр Pig змінній anAnimal.
- Надсилає повідомлення
makeNoise
свині.
- Повторює всю справу, використовуючи корову, але привласнюючи її до тієї ж точної змінної, що і Свиня.
Той самий код Java виглядатиме приблизно так (припускаючи, що Дак і Корова є підкласами 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, статично набраній мові, який тип можна віднести до нашої змінної? Кукурудзи потрібно успадковувати від Овочів, щоб підтримувати ріст, але не може успадковувати від Тварин, тому що не дає шуму. Корові потрібно успадковувати від тварини для підтримки makeNoise, але не може успадковувати її від 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.
Статистично типізовані мови забезпечують набагато краще "програмування за контрактом", оскільки вони підкажуть два види помилок нижче під час компіляції:
// Compiler error: Corn cannot be cast to Animal.
Animal farmObject = new Corn();
farmObject makeNoise();
-
// Compiler error: Animal doesn't have the harvest message.
Animal farmObject = new Cow();
farmObject.harvest();
Отже .... підсумовуючи:
Реалізація інтерфейсу дозволяє вказати, які види об'єктів можуть робити (взаємодія), а спадкування класу дозволяє вказати, як слід робити (реалізацію).
Інтерфейси дають нам багато переваг "справжнього" поліморфізму, без шкоди для перевірки типу компілятора.