Solução adequada para herança múltipla em Java (Android)

15

Eu tenho um problema conceitual com uma implementação adequada de código que parece exigir herança múltipla, que não seria um problema em muitas linguagens OO, mas como o projeto é para Android, não existe algo como múltiplo extends.

I têm um grupo de actividades, derivadas a partir de diferentes classes de base, tais como simples Activity, TabActivity, ListActivity, ExpandableListActivity, etc. Também I tem alguns fragmentos de código que eu necessidade de colocar em onStart, onStop, onSaveInstanceState, onRestoreInstanceStatee outros processadores de eventos normais em todas as actividades.

Se eu tiver uma única classe base para todas as atividades, colocaria o código em uma classe derivada intermediária especial e criaria todas as atividades estendendo-o. Infelizmente, esse não é o caso, porque existem várias classes base. Mas colocar as mesmas partes do código em várias classes intermediárias não é um caminho, imho.

Outra abordagem poderia ser criar um objeto auxiliar e delegar todas as chamadas dos eventos mencionados acima ao auxiliar. Mas isso requer que o objeto auxiliar seja incluído e que todos os manipuladores sejam redefinidos em todas as classes intermediárias. Portanto, não há muita diferença para a primeira abordagem aqui - ainda há muitas duplicatas de código.

Se uma situação semelhante ocorresse no Windows, eu subclasseia a classe base (algo que "corresponde" à Activityclasse no Android) e capturaria as mensagens apropriadas (em um único local).

O que pode ser feito em Java / Android para isso? Eu sei que existem ferramentas interessantes, como instrumentação Java ( com alguns exemplos reais ), mas não sou um guru de Java e não tenho certeza se vale a pena tentar neste caso específico.

Se eu perdi outras soluções decentes, mencione-as.

ATUALIZAR:

Para aqueles que podem estar interessados ​​em resolver o mesmo problema no Android, encontrei uma solução simples. Existe a classe Application , que fornece, entre outras coisas, a interface ActivityLifecycleCallbacks . Faz exatamente o que eu preciso, permitindo-nos interceptar e agregar algum valor em eventos importantes para todas as atividades. A única desvantagem desse método é que ele está disponível a partir do nível 14 da API, o que não é suficiente em muitos casos (o suporte ao nível 10 da API é um requisito típico hoje).

Stan
fonte

Respostas:

6

Receio que você não possa implementar seu sistema de classes sem duplicação de código no android / java.

No entanto, você pode minimizar a duplicação de código se combinar classe derivada intermediária especial com objeto auxiliar composto . Isso é chamado de 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 ....
    }

para cada classe base, você cria uma classe básica intermediária que delega suas chamadas ao auxiliar. As classes de base intermediárias são idênticas, exceto a base de onde são herdadas.

k3b
fonte
1
Sim obrigado. Eu conheço o decordator pattern. Este é um último recurso, que, na verdade, demonstra o que eu preferiria evitar - duplicação de código. Aceitarei sua resposta, se nenhuma outra ideia interessante aparecer. Posso usar genéricos para generalizar o código dos "intermediários"?
Stan
6

Eu acho que você está tentando evitar o tipo errado de duplicação de código. Acredito que Michael Feathers escreveu um artigo sobre isso, mas infelizmente não consigo encontrá-lo. A maneira como ele descreve é ​​que você pode pensar no seu código como tendo duas partes como uma laranja: a casca e a polpa. A casca é o material como declarações de método, declarações de campo, declarações de classe, etc. A polpa é o material contido nesses métodos; a implementação.

Quando se trata de DRY, você deseja evitar duplicar a polpa . Mas frequentemente no processo você cria mais casca. E tudo bem.

Aqui está um exemplo:

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

Isso pode ser refatorado para isso:

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

Adicionamos muita casca nessa refatoração. Mas, o segundo exemplo tem um limpador muito mais limpo method()com menos violações DRY.

Você disse que gostaria de evitar o padrão do decorador porque leva à duplicação de código. Se você olhar a imagem nesse link, verá que ela duplicará apenas a operation()assinatura (por exemplo: casca). A operation()implementação (pasta) deve ser diferente para cada classe. Acho que seu código ficará mais limpo como resultado e terá menos duplicação de pasta.

Daniel Kaplan
fonte
3

Você deve preferir composição a herança. Um bom exemplo é o " padrão " IExtension na estrutura do .NET WCF. Além disso, você tem 3 interfaces, IExtension, IExtensibleObject e IExtensionCollection. Você pode então compor os diferentes comportamentos com um objeto IExtensibleObject adicionando instâncias de IExtension à sua propriedade Extension IExtensionCollection. Em java, deve ser algo parecido com isso, não que você precise criar sua própria implementação IExtensioncollection que chama os métodos de anexar e desanexar quando itens estão sendo adicionados / removidos. Observe também que cabe a você definir os pontos de extensão na sua classe extensível. O exemplo usa um mecanismo de retorno de chamada do tipo 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]);
    }
}

Essa abordagem tem o benefício da reutilização de extensão, se você conseguir extrair pontos de extensão comuns entre suas classes.

m0sa
fonte
1
Talvez seja porque eu não uso o .NET, mas achei esta resposta muito difícil de entender. Você pode adicionar um exemplo do uso dessas três interfaces?
Daniel Kaplan
Obrigado, muito interessante. Mas não seria muito mais simples registrar os retornos de chamada de extensões em objetos extensíveis? Algo como processor.addHandler(console)fornecido que ConsoleProcessorimplementaria a própria interface de retorno de chamada. O padrão de "extensão" parece uma combinação de visitore decorator, mas é necessário neste caso?
Stan
Se você registrar extensões diretamente no Processador (== ExtensibleObject), terá um acoplamento apertado. A idéia aqui é ter extensões que possam ser reutilizadas entre objetos extensíveis com os mesmos pontos de extensão. De fato, o padrão é mais como uma simulação de padrão mixin .
m0sa
Hum, não tenho certeza se uma ligação por interface é um acoplamento rígido. O mais interessante é que, mesmo pelo padrão de extensão, o objeto extensível e a extensão dependem da mesma interface de trabalho ("retorno de chamada"), para que o acoplamento permaneça o mesmo, imho. Em outras palavras, não consigo conectar a extensão existente a um novo objeto extensível sem uma codificação para suporte da interface do trabalhador no "processador". Se o "ponto de extensão" significa realmente a interface do trabalhador, não vejo diferença.
Stan
0

A partir do Android 3.0, pode ser possível resolver isso de maneira elegante usando um fragmento . Os fragmentos têm seus próprios retornos de chamada do ciclo de vida e podem ser colocados dentro de uma Atividade. Não tenho certeza se isso funcionará para todos os eventos.

Outra opção que também não tenho certeza (com falta de profundo conhecimento do Android) pode ser usar um Decorator da maneira oposta que o k3b sugere: crie um ActivityWrappercujos métodos de retorno de chamada contenham seu código comum e depois encaminhe para um Activityobjeto empacotado (seu classes de implementação reais) e faça com que o Android inicie esse wrapper.

Michael Borgwardt
fonte
-3

É verdade que o Java não permite herança múltipla, mas você pode simulá-la mais ou menos fazendo com que cada uma das subclasses SomeActivity estenda a classe Activity original.

Você terá algo como:

public class TabActivity extends Activity {
    .
    .
    .
}
Samer
fonte
2
É isso que as classes já fazem, mas a classe base Activity faz parte da API do Android e não pode ser alterada pelos desenvolvedores.
Michael Borgwardt