No, la sottoclasse non ha lo stesso effetto delle estensioni di Swift. Il tuo com.me.extensions.Date
e java.util.Date
sono classi diverse. Un'istanza java.util.Date
esistente non avrà i metodi definiti nella sottoclasse. Al contrario, extension Date
non crea una nuova classe, ma aggiunge metodi a una classe esistente. Questi metodi saranno disponibili su tutte le istanze. Le estensioni sono un po 'come le patch delle scimmie nei linguaggi dinamici (Python, Ruby, Perl, JavaScript).
Questa differenza è particolarmente importante quando l'istanza Date viene creata in un altro pacchetto che non conosce la tua estensione. Creerà un java.util.Date
che non fornisce il tuo metodo someUniqueValue()
.
Le tecniche utilizzate da Swift, Go, Rust, Scala, Haskell, ... per consentire l'aggiunta di funzionalità a un tipo esistente possono essere utilizzate anche in Java, ma la lingua non ci aiuterà - dobbiamo fare tutto manualmente . In particolare, la maggior parte delle API Java non sono progettate in modo da supportare l'estensibilità necessaria.
In Java, possiamo avere lo stesso effetto di aggiungere un nuovo metodo finale a un tipo dichiarando un metodo statico che prende l'istanza come parametro. Ecco come funzionano i metodi di estensione di C #. Nel tuo caso:
static int someUniqueValue(Date self) {
return self.something * self.somethingElse;
}
Invece di date.someUniqueValue()
questo sarà chiamato come someUniqueValue(date)
, ma questa è solo sintassi. Java ti consente di import static
se desideri utilizzare tale funzione in più file.
La grande limitazione qui è che i metodi statici non funzionano insieme a dispatch dinamico (cioè metodi virtuali). Puoi definire nuovi metodi che utilizzi, ma non puoi ignorare i metodi esistenti in modo che tutti utilizzino la tua versione.
Nel caso in cui desideri estendere una classe con metodi per implementare un'interfaccia / protocollo, possiamo usare il modello dell'adattatore dell'oggetto.
interface UniquelyValued { int someUniqueValue(); }
// extension Date: UniquelyValued { ... }
class AdaptDateToUniquelyValued implements UniquelyValued {
private final Date self;
AdaptDateToUniquelyValued(Date self) {
this.self = self;
}
public Date getDate() { return self; }
@Override
public int someUniqueValue() {
return self.something * self.somethingElse;
}
}
Ogni volta che abbiamo una data ma vogliamo usarla come oggetto UniquelyValued
, dobbiamo avvolgerla nell'adattatore: new AdaptDateToUniquelyValued(date).someUniqueValue()
. È possibile interpretare il wrapping come upcasting al tipo di interfaccia. Nelle lingue con supporto di prima classe per le estensioni, il wrapping e lo unwrapping avvengono automaticamente.
Funziona bene se il codice con cui si interagisce dipende dalle interfacce, non dalle classi concrete (il principio di inversione delle dipendenze in SOLID). Questo non è necessariamente il caso.
Un wrapper può anche essere una buona scelta se puoi invece usare metodi statici, ma vuoi usare il concatenamento di metodi per fornire un'API fluente.
Il grande svantaggio degli adattatori è che la scrittura richiede molto impegno, a meno che tu non voglia utilizzare riflessioni o annotazioni. Se si adatta una classe a un'interfaccia e la classe fornisce già tutti i metodi necessari, è comunque necessario inoltrare esplicitamente ciascun metodo nell'interfaccia all'oggetto spostato.
I tipi di interfaccia hanno anche un problema generale che eseguono la "cancellazione dei tipi". Se si utilizza un adattatore per trasformare un oggetto in un'istanza di interfaccia, si perde l'informazione che in origine era un qualche tipo di oggetto. Downcasting un'istanza UniquelyValued
per l'adattatore non è sicura. Quando si progetta un'API, tali problemi possono essere minimizzati utilizzando i generici in modo esteso. Per esempio. in un'interfaccia che definisce un'API fluente
interface FluentAddition {
FluentAddition plus(int x);
int result();
}
// class FluentMath implements FluentAddition<FluentMath> { ... }
// new FluentMath().plus(1).plus(2).plus(3).result()
non possiamo suddividere questa interfaccia per aggiungere metodi extra all'API fluente: la prima chiamata a add()
ci vincolerà inutilmente. Con i generici, possiamo semplicemente usare l'interfaccia come un vincolo di tipo:
interface FluentAddition<Self extends FluentAddition<Self>> {
Self plus(int x);
int result();
}
// class FluentMath implements FluentAddition<FluentMath>,
// FluentMultiplication<FluentMath> { ... }
// new FluentMath().plus(1).plus(2).times(3).result()
Una simile interfaccia distrugge molte meno informazioni di tipo e rende più semplice avvolgere temporaneamente alcuni oggetti con un adattatore.
Altre tecniche per progettare sistemi in un modo che consenta loro di essere estese in seguito con un nuovo comportamento (nel senso del principio Open-Closed) incluso il modello dell'adattatore sono state ampiamente discusse nel libro "Modelli di progettazione". Elementi del software orientato agli oggetti riutilizzabile "di Gamma et al.