Es gibt verschiedene Arten von Polymorphismus, wobei derjenige von Interesse normalerweise Laufzeitpolymorphismus / dynamischer Versand ist.
Eine sehr allgemeine Beschreibung des Laufzeitpolymorphismus ist, dass ein Methodenaufruf abhängig vom Laufzeittyp seiner Argumente unterschiedliche Aktionen ausführt: Das Objekt selbst ist für die Auflösung eines Methodenaufrufs verantwortlich. Dies ermöglicht eine enorme Flexibilität.
Eine der gebräuchlichsten Möglichkeiten, diese Flexibilität zu nutzen, ist die Abhängigkeitsinjektion , z. B. um zwischen verschiedenen Implementierungen zu wechseln oder Scheinobjekte zum Testen zu injizieren. Wenn ich im Voraus weiß, dass es nur eine begrenzte Anzahl von Auswahlmöglichkeiten gibt, könnte ich versuchen, sie mit Bedingungen fest zu codieren, zB:
void foo() {
if (isTesting) {
... // do mock stuff
} else {
... // do normal stuff
}
}
Dies macht es schwierig, dem Code zu folgen. Die Alternative besteht darin, eine Schnittstelle für diese Foo-Operation einzuführen und eine normale Implementierung und eine Scheinimplementierung dieser Schnittstelle zu schreiben und zur Laufzeit in die gewünschte Implementierung einzufügen. "Abhängigkeitsinjektion" ist ein komplizierter Begriff für "Übergeben des richtigen Objekts als Argument".
In der Praxis arbeite ich derzeit an einem Problem mit maschinellem Lernen. Ich habe einen Algorithmus, der ein Vorhersagemodell erfordert. Aber ich möchte verschiedene Algorithmen für maschinelles Lernen ausprobieren. Also habe ich eine Schnittstelle definiert. Was brauche ich von meinem Vorhersagemodell? Bei gegebener Eingabestichprobe die Vorhersage und ihre Fehler:
interface Model {
def predict(sample) -> (prediction: float, std: float);
}
Mein Algorithmus verwendet eine Factory-Funktion, die ein Modell trainiert:
def my_algorithm(..., train_model: (observations) -> Model, ...) {
...
Model model = train_model(observations);
...
y, std = model.predict(x)
...
}
Ich habe jetzt verschiedene Implementierungen der Modellschnittstelle und kann sie gegeneinander vergleichen. Eine dieser Implementierungen verwendet tatsächlich zwei andere Modelle und kombiniert sie zu einem verstärkten Modell. Also dank dieser Schnittstelle:
- Mein Algorithmus muss nicht im Voraus über bestimmte Modelle Bescheid wissen.
- Ich kann Modelle leicht austauschen, und
- Ich habe viel Flexibilität bei der Implementierung meiner Modelle.
Ein klassischer Anwendungsfall für Polymorphismus ist die grafische Benutzeroberfläche. In einem GUI-Framework wie Java AWT / Swing /… gibt es verschiedene Komponenten . Die Komponentenschnittstelle / Basisklasse beschreibt Aktionen wie das Malen auf den Bildschirm oder das Reagieren auf Mausklicks. Viele Komponenten sind Container, die Unterkomponenten verwalten. Wie könnte sich ein solcher Container selbst zeichnen?
void paint(Graphics g) {
super.paint(g);
for (Component child : this.subComponents)
child.paint(g);
}
Hier muss der Container nicht im Voraus die genauen Typen der Unterkomponenten kennen - solange sie mit der Component
Schnittstelle übereinstimmen, kann der Container einfach die polymorphe paint()
Methode aufrufen . Dies gibt mir die Freiheit, die AWT-Klassenhierarchie mit beliebigen neuen Komponenten zu erweitern.
Es gibt viele wiederkehrende Probleme in der Softwareentwicklung, die durch die Anwendung des Polymorphismus als Technik gelöst werden können. Diese wiederkehrenden Problem-Lösungs-Paare werden Entwurfsmuster genannt , und einige von ihnen sind im gleichnamigen Buch zusammengefasst. In diesem Buch wäre mein Modell des injizierten maschinellen Lernens eine Strategie , die ich verwende, um „eine Familie von Algorithmen zu definieren, jede einzelne zu kapseln und austauschbar zu machen“. Das Java-AWT-Beispiel, in dem eine Komponente Unterkomponenten enthalten kann, ist ein Beispiel für einen Verbund .
Es muss jedoch nicht für jedes Design Polymorphismus verwendet werden (über die Aktivierung der Abhängigkeitsinjektion für Komponententests hinaus, was ein wirklich guter Anwendungsfall ist). Die meisten Probleme sind ansonsten sehr statisch. Infolgedessen werden Klassen und Methoden häufig nicht für den Polymorphismus verwendet, sondern lediglich als bequeme Namespaces und für die hübsche Syntax von Methodenaufrufen. ZB bevorzugen viele Entwickler Methodenaufrufe account.getBalance()
gegenüber weitgehend gleichwertigen Funktionsaufrufen Account_getBalance(account)
. Das ist ein perfekter Ansatz, es ist nur so, dass viele Methodenaufrufe nichts mit Polymorphismus zu tun haben.