Solución alternativa para la herencia múltiple en Java (Android)

15

Tengo un problema conceptual con una implementación adecuada del código que parece requerir una herencia múltiple, eso no sería un problema en muchos lenguajes OO, pero como el proyecto es para Android, no existe tal cosa como múltiples extends.

Tengo un montón de actividades, derivadas de diferentes clases de base, tales como sencilla Activity, TabActivity, ListActivity, ExpandableListActivity, etc. También tengo algunos fragmentos de código que I necesidad de colocar en onStart, onStop, onSaveInstanceState, onRestoreInstanceStatey otros controladores de eventos estándar en todas las actividades.

Si tengo una sola clase base para todas las actividades, colocaría el código en una clase derivada intermedia especial y luego crearía todas las actividades extendiéndola. Desafortunadamente, este no es el caso, porque hay múltiples clases base. Pero colocar las mismas porciones de código en varias clases intermedias no es un camino a seguir, en mi humilde opinión.

Otro enfoque podría ser crear un objeto auxiliar y delegar todas las llamadas de los eventos mencionados anteriormente al auxiliar. Pero esto requiere que se incluya el objeto auxiliar y que todos los controladores se redefinan en todas las clases intermedias. Entonces, no hay mucha diferencia con el primer enfoque aquí: todavía hay muchos duplicados de código.

Si ocurriera una situación similar en Windows, subclase la clase base (algo que "corresponde" a la Activityclase en Android) y atrapa los mensajes apropiados allí (en un solo lugar).

¿Qué se puede hacer en Java / Android para esto? Sé que hay herramientas interesantes como la instrumentación de Java ( con algunos ejemplos reales ), pero no soy un gurú de Java, y no estoy seguro de si vale la pena intentarlo en este caso específico.

Si me perdí algunas otras soluciones decentes, por favor, menciónelas.

ACTUALIZAR:

Para aquellos que puedan estar interesados ​​en resolver el mismo problema en Android, he encontrado una solución simple. Existe la clase de aplicación , que proporciona, entre otras cosas, la interfaz ActivityLifecycleCallbacks . Hace exactamente lo que necesito, permitiéndonos interceptar y agregar algo de valor en eventos importantes para todas las actividades. El único inconveniente de este método es que está disponible a partir del nivel 14 de API, que en muchos casos no es suficiente (la compatibilidad con el nivel 10 de API es un requisito típico hoy en día).

Stan
fuente

Respuestas:

6

Me temo que no puede implementar su sistema de clases sin duplicación de código en Android / Java.

Sin embargo, puede minimizar la duplicación de código si combina una clase derivada intermedia especial con un objeto auxiliar compuesto . Esto se llama 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 ....
    }

así que cada clase base crea una clase base intermedia que delega sus llamadas al ayudante. Las clases base intermedias son idénticas, excepto la baseclase de donde heredan.

k3b
fuente
1
Si, gracias. Yo se el decordator pattern. Este es un último recurso, que aunque en realidad demuestra lo que preferiría evitar: la duplicación de código. Aceptaré tu respuesta, si no hay otras ideas interesantes. ¿Puedo usar genéricos para generalizar el código de los "intermedios"?
Stan
6

Creo que estás tratando de evitar el tipo incorrecto de duplicación de código. Creo que Michael Feathers escribió un artículo sobre esto, pero desafortunadamente no puedo encontrarlo. La forma en que lo describe es que puede pensar que su código tiene dos partes como una naranja: la corteza y la pulpa. La corteza es la materia como declaraciones de métodos, declaraciones de campo, declaraciones de clase, etc. La pulpa es la materia dentro de estos métodos; la implementación.

Cuando se trata de SECO, debes evitar duplicar la pulpa . Pero a menudo en el proceso creas más corteza. Y eso esta bien.

Aquí hay un ejemplo:

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

Esto se puede refactorizar en esto:

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

Agregamos mucha corteza en esta refactorización. Pero, el segundo ejemplo tiene mucho más limpio method()con menos violaciones DRY.

Dijiste que te gustaría evitar el patrón decorador porque conduce a la duplicación de código. Si observa la imagen en ese enlace, verá que solo duplicará la operation()firma (es decir, cáscara). La operation()implementación (pulpa) debe ser diferente para cada clase. Creo que su código terminará más limpio como resultado y tendrá menos duplicación de pulpa.

Daniel Kaplan
fuente
3

Debes preferir la composición sobre la herencia. Un buen ejemplo es el " patrón " de IExtension en el marco .NET WCF. Básicamente tiene 3 interfaces, IExtension, IExtensibleObject e IExtensionCollection. Luego puede componer los diferentes comportamientos con un objeto IExtensibleObject agregando instancias IExtension a su propiedad Extension IExtensionCollection. En java debería verse algo así, sin embargo, no tiene que crear su propia implementación de IExtensioncollection que llama a los métodos adjuntar y desconectar cuando se agregan / eliminan elementos. También tenga en cuenta que depende de usted definir los puntos de extensión en su clase extensible. El ejemplo utiliza un mecanismo de devolución de llamada similar 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]);
    }
}

Este enfoque tiene el beneficio de la reutilización de extensiones si logra extraer puntos de extensión comunes entre sus clases.

m0sa
fuente
1
Tal vez sea porque no uso .NET, pero encontré esta respuesta muy difícil de entender. ¿Puedes agregar un ejemplo del uso de estas tres interfaces?
Daniel Kaplan
Gracias muy interesante. ¿Pero no sería mucho más simple registrar las devoluciones de llamada de extensión en un objeto extensible? Algo así como processor.addHandler(console)provisto que ConsoleProcessorimplementaría la interfaz de devolución de llamada en sí. El patrón de "extensión" parece una combinación de visitory decorator, pero ¿es necesario en este caso?
Stan
Si registra extensiones directamente en el Procesador (== ExtensibleObject), tiene un ajuste estrecho. La idea aquí es tener extensiones que puedan reutilizarse entre objetos extensibles con los mismos puntos de extensión. De hecho, el patrón es mora como un mixin simulación patrón.
m0sa
Hm, no estoy seguro de que un enlace por interfaz sea un acoplamiento estrecho. Lo más interesante es que incluso por el patrón de extensión, tanto el objeto extensible como la extensión dependen de la misma interfaz de trabajo ("devolución de llamada"), por lo que el acoplamiento sigue siendo el mismo, en mi humilde opinión. En otras palabras, no puedo conectar la extensión existente a un nuevo objeto extensible sin una codificación para el soporte de la interfaz de trabajo en el "procesador". Si el "punto de extensión" significa en realidad la interfaz de trabajo, entonces no veo una diferencia.
Stan
0

A partir de Android 3.0, puede ser posible resolver esto elegantemente usando un Fragmento . Los fragmentos tienen sus propias devoluciones de llamada del ciclo de vida y se pueden colocar dentro de una Actividad. Sin embargo, no estoy seguro de que esto funcione para todos los eventos.

Otra opción de la que tampoco estoy seguro (que carece de un profundo conocimiento de Android) podría ser usar un Decorador de la manera opuesta que k3b sugiere: crear uno ActivityWrappercuyos métodos de devolución de llamada contengan su código común y luego reenviarlo a un Activityobjeto envuelto (su clases de implementación reales), y luego haga que Android inicie ese contenedor.

Michael Borgwardt
fuente
-3

Es cierto que Java no permite la herencia múltiple, pero puede simularlo más o menos haciendo que cada una de sus subclases SomeActivity extienda la clase de actividad original.

Tendrás algo como:

public class TabActivity extends Activity {
    .
    .
    .
}
Samer
fuente
2
Esto es lo que las clases ya hacen, pero la clase base Actividad es parte de la API de Android y los desarrolladores no pueden cambiarla.
Michael Borgwardt