Існують різні види поліморфізму, що цікавить, як правило, поліморфізм часу виконання / динамічне відправлення.
Описом поліморфізму виконання на високому рівні є те, що виклик методу робить різні речі залежно від типу його аргументів: сам об'єкт відповідає за рішення виклику методу. Це дозволяє забезпечити величезну кількість гнучкості.
Один з найпоширеніших способів використання цієї гнучкості - це введення залежності , наприклад, щоб я міг переходити між різними реалізаціями або вводити макетні об'єкти для тестування. Якщо я заздалегідь знаю, що буде лише обмежена кількість можливих варіантів, я можу спробувати жорстко закодувати їх, наприклад:
void foo() {
if (isTesting) {
... // do mock stuff
} else {
... // do normal stuff
}
}
Це робить код важким для дотримання. Альтернативою є ввести інтерфейс для цієї foo-операції та написати нормальну реалізацію та макетну реалізацію цього інтерфейсу, а також "вводити" бажану реалізацію під час виконання. "Введення залежності" - складний термін для "передачі правильного об'єкта як аргументу".
Як приклад у реальному світі, я зараз працюю над своєрідною проблемою машинного навчання. У мене є алгоритм, який вимагає моделі прогнозування. Але я хочу спробувати різні алгоритми машинного навчання. Тому я визначив інтерфейс. Що мені потрібно від моєї моделі передбачення? З огляду на деякий вхідний зразок, прогноз та його помилки:
interface Model {
def predict(sample) -> (prediction: float, std: float);
}
Мій алгоритм виконує заводську функцію, яка готує модель:
def my_algorithm(..., train_model: (observations) -> Model, ...) {
...
Model model = train_model(observations);
...
y, std = model.predict(x)
...
}
Зараз у мене є різні реалізації модельного інтерфейсу і можу порівняти їх одна проти одної. Одна з цих реалізацій насправді включає дві інші моделі та об'єднує їх у розширену модель. Тож завдяки цьому інтерфейсу:
- моєму алгоритму не потрібно заздалегідь знати про конкретні моделі,
- Я можу легко замінити моделі, і
- Я маю велику гнучкість у реалізації своїх моделей.
Класичний випадок використання поліморфізму є в графічних інтерфейсах. У такій графічній основі, як Java AWT / Swing /…, є різні компоненти . Компонентний інтерфейс / базовий клас описує такі дії, як малювання самого екрана або реагування на клацання миші. Багато компонентів є контейнерами, які керують підкомпонентами. Як може такий контейнер намалювати себе?
void paint(Graphics g) {
super.paint(g);
for (Component child : this.subComponents)
child.paint(g);
}
Тут контейнеру не потрібно заздалегідь знати про точні типи підкомпонентів - якщо вони відповідають Component
інтерфейсу, контейнер може просто викликати поліморфний paint()
метод. Це дає мені свободу розширювати ієрархію класів AWT за допомогою довільних нових компонентів.
У процесі розробки програмного забезпечення існує багато проблем, які можна вирішити, застосовуючи поліморфізм як техніку. Ці повторювані пари проблеми-рішення називаються моделями дизайну , а деякі з них зібрані в однойменній книзі. Згідно з цією книгою, моя модель машинного навчання в машині буде стратегією, яку я використовую, щоб "визначити сімейство алгоритмів, інкапсулювати кожен з них і зробити їх взаємозамінними". Приклад Java-AWT, де компонент може містити підкомпоненти, є прикладом композиту .
Але не кожна конструкція потребує використання поліморфізму (крім включення введення залежності для одиничного тестування, що є справді хорошим випадком використання). Більшість проблем інакше дуже статичні. Як наслідок, класи та методи часто використовуються не для поліморфізму, а просто як зручні простори імен та синтаксис симпатичного методу. Наприклад, багато розробників віддають перевагу викликам методів, як account.getBalance()
над майже еквівалентним викликом функції Account_getBalance(account)
. Це ідеально чудовий підхід, адже багато викликів «методів» не мають нічого спільного з поліморфізмом.