Soluzione corretta per l'ereditarietà multipla in Java (Android)


15

Ho un problema concettuale con una corretta implementazione del codice che sembra richiedere l'ereditarietà multipla, che non sarebbe un problema in molte lingue OO, ma poiché il progetto è per Android, non esiste nulla come multiplo extends.

Ho un sacco di attività, derivata da diverse classi base, come semplice Activity, TabActivity, ListActivity, ExpandableListActivity, ecc Inoltre ho alcuni frammenti di codice cui devo posto in onStart, onStop, onSaveInstanceState, onRestoreInstanceStatee altri gestori di eventi standard in tutte le attività.

Se ho una singola classe base per tutte le attività, inserisco il codice in una speciale classe derivata intermedia e quindi creo tutte le attività che lo estendono. Sfortunatamente, questo non è il caso, perché ci sono più classi di base. Ma mettere le stesse porzioni di codice in più classi intermedie non è una strada da percorrere, imho.

Un altro approccio potrebbe essere quello di creare un oggetto helper e delegare tutte le chiamate degli eventi sopra citati all'helper. Tuttavia, è necessario includere l'oggetto helper e ridefinire tutti i gestori in tutte le classi intermedie. Quindi non c'è molta differenza nel primo approccio qui - ancora molti duplicati di codice.

Se si verificasse una situazione simile in Windows, sottoclasserei la classe base (qualcosa che "corrisponde" alla Activityclasse in Android) e intrappolerei lì i messaggi appropriati (in un unico posto).

Cosa si può fare in Java / Android per questo? So che ci sono strumenti interessanti come la strumentazione Java ( con alcuni esempi reali ), ma non sono un guru Java e non sono sicuro se valga la pena provare in questo caso specifico.

Se ho perso alcune altre soluzioni decenti, per favore, menzionale.

AGGIORNARE:

Per coloro che potrebbero essere interessati a risolvere lo stesso problema in Android, ho trovato una soluzione semplice. Esiste la classe Application , che fornisce, tra l'altro, l'interfaccia ActivityLifecycleCallbacks . Fa esattamente ciò di cui ho bisogno permettendoci di intercettare e aggiungere un valore in eventi importanti per tutte le attività. L'unico inconveniente di questo metodo è che è disponibile a partire dal livello API 14, che in molti casi non è sufficiente (il supporto per il livello API 10 è un requisito tipico oggi).

Risposte:


6

Temo che tu non possa implementare il tuo sistema di classi senza codeduplicazione in android / java.

Tuttavia, è possibile ridurre al minimo la codeduplicazione combinando una classe derivata intermedia speciale con un oggetto helper composito . Questo si chiama Decorator_pattern :

    class ActivityHelper {
        Activity owner;
        public ActivityHelper(Activity owner){/*...*/}
        onStart(/*...*/){/*...*/}   
    }

    public class MyTabActivityBase extends TabActivity {
        private ActivityHelper helper;
        public MyTabActivityBase(/*...*/) {
            this.helper = new ActivityHelper(this);
        }

        protected void onStart() {
            super.onStart();
            this.helper.onStart();
        }
        // the same for onStop, onSaveInstanceState, onRestoreInstanceState,...
    }

    Public class MySpecialTabActivity extends MyTabActivityBase  {
       // non helper logic goes here ....
    }

così ogni classe di base crei una base di base intermedia che delega le sue chiamate all'helper. I basesclass intermedi sono identici ad eccezione del baseclase da cui ereditano.


1
Si Grazie. Conosco il decordator pattern. Questa è l'ultima risorsa, che in realtà dimostra ciò che preferirei evitare: la duplicazione del codice. Accetterò la tua risposta, se non vi saranno altre idee interessanti. Posso forse usare i generici per generalizzare il codice degli "intermedi"?
Stan,

6

Penso che stai cercando di evitare il tipo sbagliato di duplicazione del codice. Credo che Michael Feathers abbia scritto un articolo su questo, ma sfortunatamente non riesco a trovarlo. Il modo in cui lo descrive è che puoi pensare al tuo codice come avente due parti come un'arancia: la scorza e la polpa. La scorza è roba come dichiarazioni di metodi, dichiarazioni di campo, dichiarazioni di classe, ecc. La polpa è roba dentro questi metodi; l'implemento.

Quando si tratta di SECCO, si desidera evitare la duplicazione della polpa . Ma spesso nel processo crei più scorza. E va bene.

Ecco un esempio:

public void method() { //rind
    boolean foundSword = false;
    for (Item item : items)
        if (item instanceof Sword)
             foundSword = true;
    boolean foundShield = false;
    for (Item item : items)
        if (item instanceof Shield)
             founShield = true;
    if (foundSword && foundShield)
        //...
}  //rind

Questo può essere refactored in questo:

public void method() {  //rind
    if (foundSword(items) && foundShield(items))
        //...
} //rind

public boolean foundSword(items) { //rind
    return containsItemType(items, Sword.class);
} //rind

public boolean foundShield(items) { //rind
    return containsItemType(items, Shield.class);
} //rind

public boolean containsItemType(items, Class<Item> itemClass) { //rind
    for (Item item : items)
        if (item.getClass() == itemClass)
             return true;
    return false;
} //rind

Abbiamo aggiunto molta scorza in questo refactoring. Ma il secondo esempio ha un aspetto molto più pulitomethod() con meno violazioni SECCHI.

Hai detto che vorresti evitare il motivo decorativo perché porta alla duplicazione del codice. Se guardi l'immagine in quel link vedrai che duplicherà solo la operation()firma (cioè: scorza). L' operation()implementazione (pulp) dovrebbe essere diversa per ogni classe. Penso che il tuo codice finirà per essere più pulito di conseguenza e avrà una duplicazione meno pulp.


3

Dovresti preferire la composizione all'eredità. Un bell'esempio è il " pattern " di IExtension nel framework .NET WCF. Baiscally hai 3 interfacce, IExtension, IExtensibleObject e IExtensionCollection. È quindi possibile comporre i diversi comportamenti con un oggetto IExtensibleObject aggiungendo istanze IExtension alla sua proprietà Extension IExtensionCollection. In java dovrebbe apparire qualcosa del genere, non tuttavia è necessario creare la propria implementazione IExtensioncollection che chiama i metodi di collegamento e scollegamento quando gli elementi vengono aggiunti / rimossi. Nota anche che spetta a te definire i punti di estensione nella tua classe estensibile L'esempio usa un meccanismo di callback simile a un evento:

import java.util.*;

interface IExtensionCollection<T> extends List<IExtension<T>> {
    public T getOwner();
}

interface IExtensibleObject<T> {
    IExtensionCollection<T> getExtensions();
}

interface IExtension<T> {
    void attach(T target);
    void detach(T target);
}

class ExtensionCollection<T>
    extends LinkedList<IExtension<T>>
    implements IExtensionCollection<T> {

    private T owner;
    public ExtensionCollection(T owner) { this.owner = owner; }
    public T getOwner() { return owner; }
    public boolean add(IExtension<T> e) {
        boolean result = super.add(e);
        if(result) e.attach(owner);
        return result;
    }
    // TODO override remove handler
}

interface ProcessorCallback {
    void processing(byte[] data);
    void processed(byte[] data);
}

class Processor implements IExtensibleObject<Processor> {
    private ExtensionCollection<Processor> extensions;
    private Vector<ProcessorCallback> processorCallbacks;
    public Processor() {
        extensions = new ExtensionCollection<Processor>(this);
        processorCallbacks = new Vector<ProcessorCallback>();
    }
    public IExtensionCollection<Processor> getExtensions() { return extensions; }
    public void addHandler(ProcessorCallback cb) { processorCallbacks.add(cb); }
    public void removeHandler(ProcessorCallback cb) { processorCallbacks.remove(cb); }

    public void process(byte[] data) {
        onProcessing(data);
        // do the actual processing;
        onProcessed(data);
    }
    protected void onProcessing(byte[] data) {
        for(ProcessorCallback cb : processorCallbacks) cb.processing(data);
    }
    protected void onProcessed(byte[] data) {
        for(ProcessorCallback cb : processorCallbacks) cb.processed(data);
    }
}

class ConsoleProcessor implements IExtension<Processor> {
    public ProcessorCallback console = new ProcessorCallback() {
        public void processing(byte[] data) {

        }
        public void processed(byte[] data) {
            System.out.println("processed " + data.length + " bytes...");
        }
    };
    public void attach(Processor target) {
        target.addHandler(console);
    }
    public void detach(Processor target) {
        target.removeHandler(console);
    }
}

class Main {
    public static void main(String[] args) {
        Processor processor = new Processor();
        IExtension<Processor> console = new ConsoleProcessor();
        processor.getExtensions().add(console);

        processor.process(new byte[8]);
    }
}

Questo approccio ha il vantaggio di riutilizzare l'estensione se riesci a estrarre punti di estensione comuni tra le tue classi.


1
Forse è perché non uso .NET, ma ho trovato questa risposta molto difficile da capire. Puoi aggiungere un esempio dell'uso di queste tre interfacce?
Daniel Kaplan,

Grazie, molto interessante. Ma non sarebbe molto più semplice registrare i callback dell'estensione in un oggetto estensibile? Qualcosa del genere processor.addHandler(console)fornito che ConsoleProcessorimplementerebbe l'interfaccia di callback stessa. Il modello "extension" sembra un mix di visitore decorator, ma è necessario in questo caso?
Stan,

Se si registrano estensioni direttamente nel processore (== ExtensibleObject) si ha un raggruppamento stretto. L'idea qui è di avere estensioni che possono essere riutilizzate tra oggetti estensibili con gli stessi punti di estensione. In effetti il ​​pattern è mora come una simulazione del pattern mixin .
m0sa

Hm, non sono sicuro che un binding per interfaccia sia un accoppiamento stretto. La cosa più interessante è che anche dal modello di estensione sia l'oggetto estensibile che l'estensione si basano sulla stessa interfaccia di lavoro ("callback"), quindi l'accoppiamento rimane lo stesso, imho. In altre parole, non posso collegare l'estensione esistente in un nuovo oggetto estensibile senza una codifica per il supporto dell'interfaccia di lavoro nel "processore". Se il "punto di estensione" significa effettivamente l'interfaccia di lavoro, allora non vedo alcuna differenza.
Stan,

0

A partire da Android 3.0, potrebbe essere possibile risolverlo elegantemente usando un frammento . I frammenti hanno i loro callback del ciclo di vita e possono essere inseriti in un'attività. Non sono sicuro che funzionerà per tutti gli eventi.

Un'altra opzione di cui non sono sicuro (mancanza di un approfondito know-how Android) potrebbe essere quella di utilizzare un Decorator nel modo opposto suggerito da k3b: crea un i ActivityWrappercui metodi di callback contengono il tuo codice comune e quindi inoltra a un Activityoggetto spostato (il tuo classi di implementazione effettive), quindi Android avvia quel wrapper.


-3

È vero che Java non consente l'ereditarietà multipla, ma è possibile simularla più o meno facendo in modo che ciascuna delle sottoclassi SomeActivity estenda la classe Activity originale.

Avrai qualcosa del tipo:

public class TabActivity extends Activity {
    .
    .
    .
}

2
Questo è ciò che fanno già le classi, ma la classe base Activity fa parte dell'API Android e non può essere modificata dagli sviluppatori.
Michael Borgwardt,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.