Estensioni simili a Swift in Java usando l'ereditarietà

4

Dopo aver acquisito alcune abilità Swift con Java come linguaggio più strong, una caratteristica di Swift che mi piace molto è la possibilità di aggiungere estensioni a una classe. In Java, un pattern che vedo molto spesso è Utils o Helper classes, in cui aggiungi i tuoi metodi per semplificare qualcosa che stai cercando di realizzare. Questa potrebbe essere una domanda stupida, ma c'è qualche buona ragione per non sottoclasse la classe originale in Java e basta importarla con lo stesso nome?

Un rapido esempio di un'estensione di Data sarebbe qualcosa come questo

extension Date {
    func someUniqueValue() -> Int {
        return self.something * self.somethingElse
    }
}

Quindi un'implementazione sarebbe simile a questa:

let date = Date()
let myThing = date.someUniqueValue()

In Java potresti avere una classe DateHelper, ma ora mi sembra arcaica. Perché non creare una classe con lo stesso nome ed estendere la classe a cui vuoi aggiungere un metodo?

class Date extends java.util.Date {
    int someUniqueValue() {
        return this.something * this.somethingElse;
    }
}

Quindi l'implementazione sarebbe simile a questa:

import com.me.extensions.Date

...

Date date = new Date()
int myThing = date.someUniqueValue()

Quindi, importa semplicemente la tua classe Date che ora funziona come una classe con le estensioni di Swift.

Qualcuno ha avuto successo con questo o ha visto qualche motivo per stare lontano da uno schema come questo?

    
posta Styler 03.01.2017 - 22:42
fonte

1 risposta

8

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.

    
risposta data 04.01.2017 - 15:20
fonte