Esposizione interna dei dettagli di implementazione sull'ereditarietà

1

Sto leggendo il libro "Effective Java" che suggerisce di favorire la composizione sull'ereditarietà. L'esempio che mostra mostra qualcosa di simile a questo:

public class InstrumentedHashSet<E> extends HashSet<E> {
     // The number of attempted element insertions 
     private int addCount = 0;

     public InstrumentedHashSet() { }

     public InstrumentedHashSet(int initCap, float loadFactor) {
         super(initCap, loadFactor);
     }

     @Override public boolean add(E e) {
         addCount++;
         return super.add(e);
     }

     @Override public boolean addAll(Collection<? extends E> c) {
         addCount += c.size();
         return super.addAll(c);
     }

     public int getAddCount() {
         return addCount;
    }
}

Questo presenta un problema perché l'implementazione di addAll in HashSet utilizza il metodo add . Pertanto, quando si chiama addAll ogni nuovo elemento viene conteggiato due volte: una volta in addAll e una volta in add . Nel prossimo capitolo viene spiegato che se scegliamo di consentire l'estensione della nostra classe, dovremmo esplicitamente spiegare il funzionamento interno dei nostri metodi che usano metodi sovrascrivibili (nel senso: nella documentazione addAll dovremmo specificare il suo uso di add e commit a questa implementazione per sempre ).

Penso che una migliore pratica sarebbe quella di decidere che ogni metodo che è sia un metodo API overridable, sia usato dall'implementazione interna dovrebbe essere estratto in un metodo interno, privato. Quindi il nostro ideale HashSet avrebbe questi metodi:

public class HashSet<E> implements Set<E> {
     // Ignore irrelevant code


     @Override public boolean add(E e) {
         // The new add implementation only wraps the innerAdd method
         return innerAdd(e);
     }

     @Override public boolean addAll(Collection<? extends E> c) {
        // Do the same logic as before but use innerAdd instead of add
     }

     private boolean innerAdd(E e) {
         // The original add logic will be moved to here
     }
}

In questo modo incapsuliamo l'implementazione interna e permettiamo di estendere la classe senza temere di sovrascrivere un metodo che viene utilizzato dall'implementazione interna. Ovviamente, possiamo usare l'aggiunta originale come prima, perché chiamando super.add(e) di fatto dobbiamo usare innerAdd , quindi non è un problema.

Vorrei sapere se questa potrebbe essere una buona pratica (non per HashSet<E> che è già impegnata per il codice che la usa, ma per le nuove classi che devono essere sostituite)?

    
posta Avi 04.08.2013 - 22:53
fonte

1 risposta

2

Sì, avrebbero potuto farlo, ma penso che tu abbia dimenticato la parte cruciale: il fatto che "addAll" non si basi su "add" deve essere esplicitamente documentato nell'interfaccia pubblica della classe . Il problema è che farlo non sarebbe troppo elegante. Se aggiungi dettagli di implementazione all'interfaccia pubblica, interrompi il principio di astrazione in quella classe.

Guarda questo estratto pertinente da quel particolare capitolo:

"This 'self-use' is an implementation detail, not guaranteed to hold in all implementations of the Java platform and subject to change from release to release".

Secondo me il punto d'appoggio qui è che l'eredità può facilmente interrompere l'incapsulamento e portare a problemi insidiosi come questo, quindi la sua opinione è che le classi Java dovrebbero essere state rese definitive di default. In questo modo dovresti prendere una decisione consapevole quando rendi ereditabile una classe.

    
risposta data 04.08.2013 - 23:45
fonte

Leggi altre domande sui tag