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).

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ą.


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.


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.


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.


-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 {
    .
    .
    .
}

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,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.