Esistono diversi tipi di polimorfismo, quello di interesse è solitamente il polimorfismo di runtime / dispatch dinamico.
Una descrizione di alto 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à è per iniezione di dipendenza , ad es. in modo che io possa passare tra diverse implementazioni o iniettare oggetti finti per il test. Se so in anticipo che ci sarà solo un numero limitato di scelte possibili, 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 consiste nell'introdurre un'interfaccia per tale operazione e scrivere un'implementazione normale e un'implementazione fittizia di tale interfaccia e "iniettare" l'implementazione desiderata in fase di runtime. "Iniezione di dipendenza" è un termine complicato per "passare l'oggetto corretto come argomento".
Come esempio del mondo reale, attualmente sto lavorando a un problema di apprendimento automatico della macchina. Ho un algoritmo che richiede un modello di predizione. Ma voglio provare diversi algoritmi di apprendimento automatico. Così ho definito un'interfaccia. Di cosa ho bisogno dal mio modello di previsione? Dati alcuni esempi di input, la previsione e i relativi errori:
interface Model {
def predict(sample) -> (prediction: float, std: float);
}
Il mio algoritmo utilizza una funzione di fabbrica che allena 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 l'una con l'altra. 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 modelli specifici in anticipo,
- Posso facilmente sostituire i modelli e
- Ho molta flessibilità nell'implementazione dei miei modelli.
Un caso d'uso classico del polimorfismo è nelle GUI. In una struttura GUI come Java AWT / Swing / ... ci sono diversi componenti . L'interfaccia componente / classe base descrive azioni come dipingersi sullo schermo o reagire ai clic del mouse. Molti componenti sono contenitori che gestiscono sottocomponenti. In che modo un simile contenitore potrebbe disegnarsi da solo?
void paint(Graphics g) {
super.paint(g);
for (Component child : this.subComponents)
child.paint(g);
}
Qui, il contenitore non ha bisogno di sapere in anticipo i tipi esatti dei sottocomponenti - finché si conformano all'interfaccia Component
il contenitore può semplicemente chiamare il metodo polimorfo paint()
. Questo mi dà la libertà di estendere la gerarchia delle classi 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 di soluzioni-problema sono denominate modelli di progettazione e alcune di esse 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 ognuno e renderli intercambiabili". L'esempio Java-AWT in cui un componente può contenere sotto-componenti è un esempio di composito .
Ma non tutti i progetti devono utilizzare il polimorfismo (oltre a consentire l'iniezione di dipendenza per il test delle unità, che è un ottimo caso d'uso). La maggior parte dei problemi sono altrimenti molto statici. Di conseguenza, le classi e i metodi spesso non vengono utilizzati per il polimorfismo, ma semplicemente come spazi dei nomi convenienti e per la sintassi di chiamata del metodo piuttosto. Per esempio. molti sviluppatori preferiscono le chiamate di metodo come account.getBalance()
su una funzione in gran parte chiamata Account_getBalance(account)
. Questo è un approccio perfetto, è solo che molte chiamate "metodo" non hanno nulla a che fare con il polimorfismo.