Richtige Problemumgehung für die Mehrfachvererbung in Java (Android)

15

Ich habe ein konzeptionelles Problem mit einer ordnungsgemäßen Implementierung von Code, die scheinbar eine Mehrfachvererbung erfordert. Dies wäre in vielen OO-Sprachen kein Problem, aber da das Projekt für Android ist, gibt es keine Mehrfachvererbung extends.

Ich habe eine Reihe von Aktivitäten, die von verschiedenen Basisklassen abgeleitet ist , wie einfach Activity, TabActivity, ListActivity, ExpandableListActivity, etc. Auch habe ich einige Code - Fragmente , die ich Platz brauche in onStart, onStop, onSaveInstanceState, onRestoreInstanceStateund anderen Standard - Event - Handler in allen Aktivitäten.

Wenn ich für alle Aktivitäten eine einzige Basisklasse habe, platziere ich den Code in einer speziellen abgeleiteten Zwischenklasse und erstelle dann alle Aktivitäten, mit denen er erweitert wird. Dies ist leider nicht der Fall, da es mehrere Basisklassen gibt. Aber die gleichen Teile des Codes in mehrere Zwischenklassen einzuteilen, ist kein Weg, imho.

Ein anderer Ansatz könnte darin bestehen, ein Helferobjekt zu erstellen und alle Aufrufe der oben genannten Ereignisse an den Helfer zu delegieren. Dies erfordert jedoch, dass das Hilfsobjekt eingeschlossen und alle Handler in allen Zwischenklassen neu definiert werden. Es gibt also keinen großen Unterschied zum ersten Ansatz hier - immer noch viele Code-Duplikate.

Wenn eine ähnliche Situation unter Windows auftritt, würde ich die Basisklasse (etwas, das der ActivityKlasse in Android "entspricht") in Unterklassen unterteilen und entsprechende Nachrichten dort abfangen (an einem einzigen Ort).

Was kann man in Java / Android dafür tun? Ich weiß, dass es interessante Tools wie Java-Instrumentierung gibt ( mit einigen echten Beispielen ), aber ich bin kein Java-Guru und nicht sicher, ob es sich lohnt, es in diesem speziellen Fall zu versuchen.

Wenn ich andere anständige Lösungen verpasst habe, erwähnen Sie sie bitte.

AKTUALISIEREN:

Für diejenigen, die das gleiche Problem in Android lösen möchten, habe ich eine einfache Lösung gefunden. Es gibt die Application- Klasse, die unter anderem das Interface ActivityLifecycleCallbacks bereitstellt . Es tut genau das, was ich brauche, damit wir wichtige Ereignisse für alle Aktivitäten abfangen und aufwerten können. Der einzige Nachteil dieser Methode ist, dass sie ab API-Level 14 verfügbar ist, was in vielen Fällen nicht ausreicht (die Unterstützung von API-Level 10 ist heutzutage eine typische Anforderung).

Stan
quelle

Antworten:

6

Ich fürchte, Sie können Ihr Klassensystem nicht ohne Codeduplizierung in Android / Java implementieren.

Sie können die Codeduplizierung jedoch minimieren, wenn Sie eine spezielle abgeleitete Zwischenklasse mit einem zusammengesetzten Hilfsobjekt kombinieren . Dies nennt man das 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 ....
    }

Daher erstellen Sie für jede Basisklasse eine Zwischenbasisklasse, die ihre Aufrufe an den Helfer delegiert. Die intermediären Basisklassen sind identisch mit Ausnahme der Basisklasse, von der sie erben.

k3b
quelle
1
Ja Dankeschön. Ich kenne das decordator pattern. Dies ist ein letzter Ausweg, der tatsächlich zeigt, was ich lieber vermeiden würde - Code-Duplizierung. Ich nehme Ihre Antwort an, wenn keine anderen Ideen auftauchen. Kann ich möglicherweise Generika verwenden, um den Code der "Intermediates" zu verallgemeinern?
Stan
6

Ich denke, Sie versuchen, die falsche Art der Code-Duplizierung zu vermeiden. Ich glaube, Michael Feathers hat einen Artikel darüber geschrieben, aber ich kann ihn leider nicht finden. So wie er es beschreibt, können Sie sich Ihren Code als zweiteilig wie eine Orange vorstellen: Die Schale und das Fruchtfleisch. Die Rinde ist das Zeug wie Methodendeklarationen, Felddeklarationen, Klassendeklarationen usw. Das Fruchtfleisch ist das Zeug in diesen Methoden; die Umsetzung.

Wenn es um TROCKEN geht, möchten Sie vermeiden, Zellstoff zu duplizieren . Aber oft entsteht dabei mehr Rinde. Und das ist in Ordnung.

Hier ist ein Beispiel:

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

Dies kann folgendermaßen überarbeitet werden:

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

Bei diesem Refactoring haben wir viel Schwarte hinzugefügt. Das zweite Beispiel weist jedoch eine wesentlich sauberere Darstellung method()mit weniger DRY-Verstößen auf.

Sie sagten, Sie möchten das Dekorationsmuster vermeiden , da es zu einer Codeduplizierung führt. Wenn Sie sich das Bild in diesem Link ansehen, werden Sie feststellen, dass es nur die operation()Signatur (dh die Rinde) dupliziert . Die operation()Implementierung (Zellstoff) sollte für jede Klasse unterschiedlich sein. Ich denke, Ihr Code wird dadurch sauberer und hat weniger Zellstoffduplikationen.

Daniel Kaplan
quelle
3

Sie sollten die Komposition der Vererbung vorziehen. Ein schönes Beispiel ist das IExtension " pattern " im .NET WCF Framework. Baiscally haben Sie 3 Schnittstellen, IExtension, IExtensibleObject und IExtensionCollection. Anschließend können Sie komponieren die unterschiedlichen Verhaltensweisen von addig IExtension Instanzen es die Erweiterung IExtensionCollection Eigenschaft mit einem IExtensibleObject Objekt. In Java sollte es ungefähr so ​​aussehen, aber Sie müssen keine eigene IExtensioncollection-Implementierung erstellen, die die Methoden attach und detach aufruft, wenn Elemente hinzugefügt / entfernt werden. Beachten Sie auch, dass Sie die Erweiterungspunkte in Ihrer erweiterbaren Klasse definieren müssen. Im Beispiel wird ein ereignisähnlicher Rückrufmechanismus verwendet:

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]);
    }
}

Dieser Ansatz bietet den Vorteil der Wiederverwendung von Erweiterungen, wenn Sie es schaffen, gemeinsame Erweiterungspunkte zwischen Ihren Klassen zu extrahieren.

m0sa
quelle
1
Vielleicht liegt es daran, dass ich .NET nicht verwende, aber ich fand diese Antwort sehr schwer zu verstehen. Können Sie ein Beispiel für die Verwendung dieser drei Schnittstellen hinzufügen?
Daniel Kaplan
Vielen Dank, sehr interessant. Aber wäre es nicht viel einfacher, die Rückrufe der Erweiterung in einem erweiterbaren Objekt zu registrieren? So etwas wie processor.addHandler(console)vorausgesetzt, dass ConsoleProcessordie Callback-Schnittstelle selbst implementiert. Das "Erweiterungsmuster" sieht aus wie ein Mix aus visitorund decorator, aber ist es in diesem Fall notwendig?
Stan
Wenn Sie Erweiterungen direkt im Prozessor registrieren (== ExtensibleObject), besteht eine enge Kopplung. Die Idee hier ist, Erweiterungen zu haben, die zwischen erweiterbaren Objekten mit denselben Erweiterungspunkten wiederverwendet werden können. In der Tat ist das Muster mora wie eine Mixin- Pattern-Simulation.
m0sa
Hm, ich bin mir nicht sicher, ob eine Bindung per Schnittstelle eine enge Kopplung ist. Das Interessanteste ist, dass sowohl das erweiterbare Objekt als auch die Erweiterung durch das Erweiterungsmuster auf der gleichen Worker-Schnittstelle ("Callback") basieren, sodass die Kopplung imho gleich bleibt. Mit anderen Worten, ich kann keine vorhandene Erweiterung in ein neues erweiterbares Objekt einstecken, ohne eine Kodierung für die Unterstützung der Worker-Schnittstelle im "Prozessor". Wenn der "Erweiterungspunkt" tatsächlich die Arbeitsoberfläche bedeutet, sehe ich keinen Unterschied.
Stan
0

Ab Android 3.0 kann dies möglicherweise mithilfe eines Fragments elegant gelöst werden . Fragmente haben ihre eigenen Rückrufe für den Lebenszyklus und können in eine Aktivität eingefügt werden. Ich bin mir jedoch nicht sicher, ob dies bei allen Veranstaltungen funktionieren wird.

Eine weitere Option , die ich auch bin nicht sicher (fehlt in eingehender Android Know-how) könnte einen Decorator in entgegengesetzter Weise k3b zu benutzen , schlägt vor: ein erstellen , ActivityWrapperderen Callback - Methoden enthalten Ihren gemeinsamen Code und dann nach vorne zu einem gewickelten ActivityGegenstand (Ihr tatsächlichen Implementierungsklassen), und lassen Sie dann Android diesen Wrapper starten.

Michael Borgwardt
quelle
-3

Es ist wahr, dass Java Mehrfachvererbung nicht zulässt, aber Sie können es mehr oder weniger simulieren, indem Sie dafür sorgen, dass jede Ihrer SomeActivity-Unterklassen die ursprüngliche Activity-Klasse erweitert.

Sie werden etwas haben wie:

public class TabActivity extends Activity {
    .
    .
    .
}
Samer
quelle
2
Dies ist, was die Klassen bereits tun, aber die Activity-Basisklasse ist Teil der Android-API und kann von Entwicklern nicht geändert werden.
Michael Borgwardt