Esistono diversi tipi di polimorfismo, quello di interesse è solitamente il polimorfismo di runtime / dispacciamento dinamico.
Una descrizione di altissimo livello del polimorfismo di runtime è che una chiamata di metodo fa cose diverse a seconda del tipo di runtime dei suoi argomenti: l'oggetto stesso è responsabile della risoluzione di una chiamata di metodo. Ciò consente un'enorme flessibilità.
Uno dei modi più comuni per utilizzare questa flessibilità è l' iniezione di dipendenza , ad esempio in modo che io possa passare da implementazioni diverse o iniettare oggetti simulati per i test. Se so in anticipo che ci sarà solo un numero limitato di possibili scelte, potrei provare a codificarle con i condizionali, ad esempio:
void foo() {
if (isTesting) {
... // do mock stuff
} else {
... // do normal stuff
}
}
Questo rende il codice difficile da seguire. L'alternativa è introdurre un'interfaccia per quella operazione e scrivere un'implementazione normale e un'implementazione fittizia di quell'interfaccia e "iniettare" l'implementazione desiderata in fase di esecuzione. "Iniezione delle dipendenze" è un termine complicato per "passare l'oggetto corretto come argomento".
Come esempio nel mondo reale, attualmente sto lavorando a un tipo di problema di apprendimento automatico. Ho un algoritmo che richiede un modello di previsione. Ma voglio provare diversi algoritmi di apprendimento automatico. Quindi ho definito un'interfaccia. Di cosa ho bisogno dal mio modello di previsione? Dato un esempio di input, la previsione e i suoi errori:
interface Model {
def predict(sample) -> (prediction: float, std: float);
}
Il mio algoritmo utilizza una funzione di fabbrica che forma un modello:
def my_algorithm(..., train_model: (observations) -> Model, ...) {
...
Model model = train_model(observations);
...
y, std = model.predict(x)
...
}
Ora ho varie implementazioni dell'interfaccia del modello e posso confrontarle tra loro. Una di queste implementazioni prende in realtà altri due modelli e li combina in un modello potenziato. Quindi grazie a questa interfaccia:
- il mio algoritmo non ha bisogno di conoscere in anticipo modelli specifici,
- Posso facilmente scambiare modelli e
- Ho molta flessibilità nell'implementazione dei miei modelli.
Un classico caso d'uso del polimorfismo è nelle GUI. In un framework GUI come Java AWT / Swing / ... ci sono diversi componenti . L'interfaccia componente / la classe base descrive azioni come dipingere se stesso sullo schermo o reagire ai clic del mouse. Molti componenti sono contenitori che gestiscono i sottocomponenti. Come potrebbe attirare un tale contenitore?
void paint(Graphics g) {
super.paint(g);
for (Component child : this.subComponents)
child.paint(g);
}
Qui, il contenitore non ha bisogno di conoscere in anticipo i tipi esatti dei sottocomponenti - fintanto che sono conformi Component
all'interfaccia, il contenitore può semplicemente chiamare il paint()
metodo polimorfico . Questo mi dà la libertà di estendere la gerarchia della classe AWT con nuovi componenti arbitrari.
Ci sono molti problemi ricorrenti durante lo sviluppo del software che possono essere risolti applicando il polimorfismo come tecnica. Queste ricorrenti coppie problema-soluzione sono chiamate modelli di progettazione e alcune sono raccolte nel libro con lo stesso nome. Nei termini di quel libro, il mio modello di apprendimento automatico iniettato sarebbe una strategia che uso per "definire una famiglia di algoritmi, incapsulare ciascuno di essi e renderli intercambiabili". L'esempio Java-AWT in cui un componente può contenere sottocomponenti è un esempio di un composto .
Ma non tutti i progetti devono utilizzare il polimorfismo (oltre a consentire l'iniezione di dipendenza per i test unitari, che è davvero un buon caso d'uso). La maggior parte dei problemi è altrimenti molto statica. Di conseguenza, spesso le classi e i metodi non vengono utilizzati per il polimorfismo, ma semplicemente come spazi dei nomi convenienti e per il grazioso metodo di sintassi delle chiamate. Ad esempio, molti sviluppatori preferiscono le chiamate di metodo come account.getBalance()
su una chiamata di funzione in gran parte equivalente Account_getBalance(account)
. Questo è un approccio perfettamente perfetto, è solo che molte chiamate di "metodo" non hanno nulla a che fare con il polimorfismo.