Prawidłowe obejście dla wielokrotnego dziedziczenia w Javie (Android)

15

Mam problem koncepcyjny z poprawną implementacją kodu, który wydaje się wymagać wielokrotnego dziedziczenia, co nie byłoby problemem w wielu językach OO, ale ponieważ projekt dotyczy Androida, nie ma czegoś takiego jak wielokrotność extends.

Mam kilka działań, pochodzący z różnych klas bazowych, takich jak proste Activity, TabActivity, ListActivity, ExpandableListActivity, itd. Również mam pewne fragmenty kodu które należy umieścić w onStart, onStop, onSaveInstanceState, onRestoreInstanceStatei innych standardowych procedur obsługi zdarzeń we wszystkich działaniach.

Jeśli mam jedną klasę bazową dla wszystkich działań, umieściłem kod w specjalnej klasie pochodnej pośredniej, a następnie utworzę wszystkie działania rozszerzające go. Niestety tak nie jest, ponieważ istnieje wiele klas podstawowych. Ale umieszczenie tych samych części kodu w kilku klasach pośrednich nie jest dobrym rozwiązaniem, imho.

Innym podejściem może być utworzenie obiektu pomocnika i przekazanie pomocnikowi wszystkich wywołań wyżej wymienionych zdarzeń. Wymaga to jednak uwzględnienia obiektu pomocnika i ponownego zdefiniowania wszystkich procedur obsługi we wszystkich klasach pośrednich. Tak więc nie ma większej różnicy w stosunku do pierwszego podejścia tutaj - wciąż dużo duplikatów kodu.

Gdyby podobna sytuacja miała miejsce w systemie Windows, podklasę klasę podstawową (coś, co „odpowiada” Activityklasie w systemie Android) i wychwytywanie tam odpowiednich komunikatów (w jednym miejscu).

Co można w tym celu zrobić w Javie / Androidzie? Wiem, że istnieją ciekawe narzędzia, takie jak instrumentacja Java ( z kilkoma prawdziwymi przykładami ), ale nie jestem guru Java i nie jestem pewien, czy warto spróbować w tym konkretnym przypadku.

Jeśli przegapiłem jakieś inne przyzwoite rozwiązania, proszę je wymienić.

AKTUALIZACJA:

Dla tych, którzy mogą być zainteresowani rozwiązaniem tego samego problemu w Androidzie, znalazłem proste obejście. Istnieje klasa aplikacji , która zapewnia między innymi interfejs ActivityLifecycleCallbacks . Robi dokładnie to, czego potrzebuję, pozwalając nam przechwytywać i dodawać wartości do ważnych wydarzeń dla wszystkich działań. Jedyną wadą tej metody jest to, że jest ona dostępna od poziomu API 14, co w wielu przypadkach nie wystarcza (obsługa interfejsu API na poziomie 10 jest dziś typowym wymogiem).

Stan
źródło

Odpowiedzi:

6

Obawiam się, że nie możesz wdrożyć systemu klasy bez kodowania w Androidzie / Javie.

Można jednak zminimalizować nakładanie kodu, łącząc specjalną pośrednią klasę pochodną z kompozytowym obiektem pomocnika . Nazywa się to 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 ....
    }

więc każda klasa podstawowa tworzy pośrednią klasę podstawową, która deleguje swoje wywołania do pomocnika. Pośrednie klasy podstawowe są identyczne, z wyjątkiem podstawowej, z której dziedziczą.

k3b
źródło
1
Tak dziękuję. Ja wiem decordator pattern. Jest to ostatnia deska ratunku, która faktycznie pokazuje, czego wolałbym unikać - powielania kodu. Przyjmę twoją odpowiedź, jeśli nie pojawią się inne inspirujące pomysły. Czy mogę użyć generycznych do uogólnienia kodu „półproduktów”?
Stan
6

Myślę, że próbujesz uniknąć niewłaściwego typu duplikacji kodu. Wierzę, że Michael Feathers napisał o tym artykuł, ale niestety nie mogę go znaleźć. Opisuje to w następujący sposób: możesz myśleć o kodzie, który składa się z dwóch części podobnych do pomarańczy: skórki i miazgi. Skórka to rzeczy takie jak deklaracje metod, deklaracje pól, deklaracje klas itp. Miąższ jest materiałem wewnątrz tych metod; implementacja.

Jeśli chodzi o DRY, chcesz uniknąć powielania miazgi . Ale często w procesie tworzysz więcej skórki. I to jest w porządku.

Oto przykład:

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

Można to zmienić na następujące:

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

Dodaliśmy dużo skórki w tym refaktoryzacji. Ale drugi przykład ma znacznie czystsze method()i mniej naruszeń DRY.

Powiedziałeś, że chcesz uniknąć wzorca dekoratora, ponieważ prowadzi to do powielania kodu. Jeśli spojrzysz na obraz w tym linku, zobaczysz, że będzie on tylko duplikował operation()podpis (tj. Skórkę). operation()Realizacja (masa) powinien być różny dla każdej klasy. Myślę, że w rezultacie twój kod będzie czystszy i będzie mniej duplikacji pulpy.

Daniel Kaplan
źródło
3

Wolisz kompozycję niż dziedziczenie. Dobrym przykładem jest „ wzorzec ” IExtension w frameworku .NET WCF. Baiscally masz 3 interfejsy, IExtension, IExtensibleObject i IExtensionCollection. Następnie można komponować różne zachowania za pomocą obiektu IExtensibleObject, dodając instancje IExtension do jego właściwości Extension IExtensionCollection. W Javie powinno to wyglądać mniej więcej tak, ale nie trzeba tworzyć własnej implementacji IExtensioncollection, która wywołuje metody dołączania i odłączania podczas dodawania / usuwania elementów. Należy również pamiętać, że to do Ciebie należy zdefiniowanie punktów rozszerzenia w swojej rozszerzalnej klasie. W przykładzie zastosowano mechanizm wywołania zwrotnego podobny do zdarzenia:

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

Zaletą tego podejścia jest ponowne użycie rozszerzenia, jeśli uda się wyodrębnić wspólne punkty rozszerzenia między klasami.

m0sa
źródło
1
Może dlatego, że nie używam .NET, ale odpowiedź ta była bardzo trudna do zrozumienia. Czy możesz dodać przykład użycia tych trzech interfejsów?
Daniel Kaplan
Dziękuję bardzo interesujący. Ale czy nie byłoby o wiele łatwiej po prostu zarejestrować wywołania zwrotne rozszerzenia w rozszerzalnym obiekcie? Pod processor.addHandler(console)warunkiem, że pod warunkiem, że ConsoleProcessorsam implementuje interfejs zwrotny. Wzorzec „rozszerzenia” wygląda jak połączenie visitori decorator, ale czy w tym przypadku jest to konieczne?
Stan
Jeśli zarejestrujesz rozszerzenia bezpośrednio w procesorze (== ExtensibleObject), będziesz mieć ciasne cupling. Chodzi o to, aby mieć rozszerzenia, które mogą być ponownie użyte między obiektami rozszerzalnymi z tymi samymi punktami rozszerzenia. W rzeczywistości wzór jest bardziej podobny do symulacji wzoru mixin .
m0sa
Hm, nie jestem pewien, czy powiązanie przez interfejs to ścisłe połączenie. Najciekawsze jest to, że nawet wzorzec rozszerzenia zarówno obiekt rozszerzalny, jak i rozszerzenie zależą od tego samego interfejsu roboczego („callback”), więc sprzężenie pozostaje takie samo, imho. Innymi słowy, nie mogę podłączyć istniejącego rozszerzenia do nowego obiektu rozszerzalnego bez kodowania obsługującego interfejs roboczy w „procesorze”. Jeśli „punkt rozszerzenia” oznacza tak naprawdę interfejs roboczy, to nie widzę różnicy.
Stan
0

Począwszy od Androida 3.0, może być możliwe eleganckie rozwiązanie tego problemu za pomocą Fragmentu . Fragmenty mają własne wywołania zwrotne cyklu życia i mogą być umieszczone w działaniu. Nie jestem jednak pewien, czy to zadziała w przypadku wszystkich wydarzeń.

Inną opcją, której również nie jestem pewien (brakuje dogłębnej wiedzy na temat Androida), może być użycie Dekoratora w odwrotny sposób niż sugeruje to k3b: utwórz ActivityWrappermetody wywołania zwrotnego zawierające wspólny kod, a następnie przekaż do owiniętego Activityobiektu ( rzeczywiste klasy implementacyjne), a następnie Android powinien uruchomić to opakowanie.

Michael Borgwardt
źródło
-3

To prawda, że ​​Java nie zezwala na wielokrotne dziedziczenie, ale można by go mniej więcej symulować, czyniąc każdą z podklas SomeActivity rozszerzeniem oryginalnej klasy Activity.

Będziesz miał coś takiego:

public class TabActivity extends Activity {
    .
    .
    .
}
Samer
źródło
2
To samo robią już klasy, ale podstawowa klasa Activity jest częścią interfejsu API Androida i nie może zostać zmieniona przez programistów.
Michael Borgwardt,