Il existe différents types de polymorphisme, celui qui nous intéresse est généralement le polymorphisme d'exécution / répartition dynamique.
Une description de très haut niveau du polymorphisme d'exécution est qu'un appel de méthode fait des choses différentes selon le type d'exécution de ses arguments: l'objet lui-même est responsable de la résolution d'un appel de méthode. Cela permet une grande flexibilité.
L'une des façons les plus courantes d'utiliser cette flexibilité est l' injection de dépendances , par exemple pour que je puisse basculer entre différentes implémentations ou pour injecter des objets fictifs à des fins de test. Si je sais à l'avance qu'il n'y aura qu'un nombre limité de choix possibles, je pourrais essayer de les coder en dur avec des conditions, par exemple:
void foo() {
if (isTesting) {
... // do mock stuff
} else {
... // do normal stuff
}
}
Cela rend le code difficile à suivre. L'alternative est d'introduire une interface pour cette foo-opération et d'écrire une implémentation normale et une implémentation simulée de cette interface, et «d'injecter» à l'implémentation souhaitée au moment de l'exécution. «L'injection de dépendance» est un terme compliqué pour «passer le bon objet comme argument».
À titre d'exemple concret, je travaille actuellement sur un genre de problème d'apprentissage automatique. J'ai un algorithme qui nécessite un modèle de prédiction. Mais je veux essayer différents algorithmes d'apprentissage automatique. J'ai donc défini une interface. De quoi ai-je besoin de mon modèle de prédiction? Étant donné un échantillon d'entrée, la prédiction et ses erreurs:
interface Model {
def predict(sample) -> (prediction: float, std: float);
}
Mon algorithme prend une fonction d'usine qui forme un modèle:
def my_algorithm(..., train_model: (observations) -> Model, ...) {
...
Model model = train_model(observations);
...
y, std = model.predict(x)
...
}
J'ai maintenant différentes implémentations de l'interface du modèle et je peux les comparer les unes aux autres. L'une de ces implémentations prend en fait deux autres modèles et les combine en un modèle boosté. Merci donc à cette interface:
- mon algorithme n'a pas besoin de connaître à l'avance des modèles spécifiques,
- Je peux facilement échanger des modèles et
- J'ai beaucoup de flexibilité pour implémenter mes modèles.
Un cas d'utilisation classique du polymorphisme est dans les interfaces graphiques. Dans un framework GUI comme Java AWT / Swing /… il y a différents composants . L'interface de composant / la classe de base décrit des actions telles que se peindre à l'écran ou réagir aux clics de souris. De nombreux composants sont des conteneurs qui gèrent des sous-composants. Comment un tel conteneur pourrait-il se dessiner?
void paint(Graphics g) {
super.paint(g);
for (Component child : this.subComponents)
child.paint(g);
}
Ici, le conteneur n'a pas besoin de connaître à l'avance les types exacts des sous-composants - tant qu'ils sont conformes à l' Component
interface, le conteneur peut simplement appeler la paint()
méthode polymorphe . Cela me donne la liberté d'étendre la hiérarchie des classes AWT avec de nouveaux composants arbitraires.
Il existe de nombreux problèmes récurrents tout au long du développement de logiciels qui peuvent être résolus en appliquant le polymorphisme comme technique. Ces paires récurrentes de problèmes et de solutions sont appelées modèles de conception , et certaines d'entre elles sont rassemblées dans le livre du même nom. Dans les termes de ce livre, mon modèle d'apprentissage automatique injecté serait une stratégie que j'utiliserais pour «définir une famille d'algorithmes, les encapsuler et les rendre interchangeables». L'exemple Java-AWT où un composant peut contenir des sous-composants est un exemple de composite .
Mais toutes les conceptions n'ont pas besoin d'utiliser le polymorphisme (au-delà de l'activation de l'injection de dépendances pour les tests unitaires, ce qui est un très bon cas d'utilisation). La plupart des problèmes sont par ailleurs très statiques. Par conséquent, les classes et les méthodes ne sont souvent pas utilisées pour le polymorphisme, mais simplement comme des espaces de noms pratiques et pour la jolie syntaxe d'appel de méthode. Par exemple, de nombreux développeurs préfèrent les appels de méthode comme account.getBalance()
un appel de fonction largement équivalent Account_getBalance(account)
. C'est une approche parfaitement fine, c'est juste que de nombreux appels de «méthode» n'ont rien à voir avec le polymorphisme.