Solution de contournement appropriée pour l'héritage multiple en Java (Android)

15

J'ai un problème conceptuel avec une implémentation correcte du code qui semble nécessiter un héritage multiple, ce ne serait pas un problème dans de nombreux langages OO, mais comme le projet est pour Android, il n'y a rien de tel que multiple extends.

J'ai un tas d'activités, provenant de différentes classes de base, comme simples Activity, TabActivity, ListActivity, ExpandableListActivity, etc. De plus j'ai quelques fragments de code que je dois lieu dans onStart, onStop, onSaveInstanceState, onRestoreInstanceStateet autres gestionnaires d'événements standard dans toutes les activités.

Si j'ai une seule classe de base pour toutes les activités, je placerais le code dans une classe dérivée intermédiaire spéciale, puis créerais toutes les activités en l'étendant. Malheureusement, ce n'est pas le cas, car il existe plusieurs classes de base. Mais placer les mêmes portions de code dans plusieurs classes intermédiaires n'est pas une solution, à mon humble avis.

Une autre approche pourrait être de créer un objet d'assistance et de déléguer tous les appels des événements susmentionnés à l'aide. Mais cela nécessite que l'objet d'assistance soit inclus et que tous les gestionnaires soient redéfinis dans toutes les classes intermédiaires. Il n'y a donc pas beaucoup de différence avec la première approche ici - encore beaucoup de doublons de code.

Si une situation similaire se produisait sous Windows, je sous-classerais la classe de base (quelque chose qui "correspond" à la Activityclasse dans Android) et y intercepterais les messages appropriés (en un seul endroit).

Que peut-on faire en Java / Android pour cela? Je sais qu'il existe des outils intéressants tels que l'instrumentation Java ( avec de vrais exemples ), mais je ne suis pas un gourou de Java, et je ne sais pas si cela vaut la peine d'essayer dans ce cas spécifique.

Si j'ai raté d'autres solutions décentes, veuillez les mentionner.

MISE À JOUR:

Pour ceux qui pourraient être intéressés à résoudre le même problème dans Android, j'ai trouvé une solution de contournement simple. Il existe la classe Application , qui fournit, entre autres, l'interface ActivityLifecycleCallbacks . Il fait exactement ce dont j'ai besoin, nous permettant d'intercepter et d'ajouter de la valeur à des événements importants pour toutes les activités. Le seul inconvénient de cette méthode est qu'elle est disponible à partir du niveau 14 de l'API, ce qui n'est pas suffisant dans de nombreux cas (la prise en charge du niveau 10 de l'API est aujourd'hui une exigence typique).

Stan
la source

Réponses:

6

Je crains que vous ne puissiez pas implémenter votre système de classe sans duplication de code dans android / java.

Cependant, vous pouvez minimiser la duplication de code si vous combinez une classe dérivée intermédiaire spéciale avec un objet d'assistance composite . C'est ce qu'on appelle le 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 ....
    }

Ainsi, chaque classe de base crée une classe de base intermédiaire qui délègue ses appels à l'assistant. Les classes de base intermédiaires sont identiques à l'exception de la base dont elles héritent.

k3b
la source
1
Oui merci. Je connais le decordator pattern. Il s'agit d'un dernier recours, qui montre bien ce que je préfère éviter: la duplication de code. J'accepterai votre réponse, si aucune autre idée instante n'apparaît. Puis-je éventuellement utiliser des génériques pour généraliser le code des "intermédiaires"?
Stan
6

Je pense que vous essayez d'éviter le mauvais type de duplication de code. Je crois que Michael Feathers a écrit un article à ce sujet mais malheureusement je ne le trouve pas. La façon dont il le décrit est que vous pouvez considérer votre code comme ayant deux parties comme une orange: l'écorce et la pulpe. L'écorce est l'étoffe comme les déclarations de méthode, les déclarations de champ, les déclarations de classe, etc. La pulpe est l'étoffe à l'intérieur de ces méthodes; la mise en oeuvre.

En ce qui concerne SEC, vous voulez éviter la duplication de la pulpe . Mais souvent, dans le processus, vous créez plus de croûte. Et ça va.

Voici un exemple:

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

Cela peut être refactorisé en ceci:

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

Nous avons ajouté beaucoup d'écorce dans ce refactoring. Mais, le deuxième exemple a un beaucoup plus propre method()avec moins de violations DRY.

Vous avez dit que vous aimeriez éviter le modèle de décorateur car il entraîne une duplication de code. Si vous regardez l'image dans ce lien, vous verrez qu'elle ne fera que dupliquer la operation()signature (par exemple: rind). L' operation()implémentation (pulp) doit être différente pour chaque classe. Je pense que votre code finira par être plus propre et qu'il y aura moins de duplication de pulpe.

Daniel Kaplan
la source
3

Vous devriez préférer la composition à l'héritage. Un bon exemple est le " modèle " IExtension dans le framework .NET WCF. En bref, vous avez 3 interfaces, IExtension, IExtensibleObject et IExtensionCollection. Vous pouvez ensuite composer les différents comportements avec un objet IExtensibleObject en ajoutant des instances IExtension à sa propriété Extension IExtensionCollection. En java, cela devrait ressembler à ça, mais pas que vous deviez créer votre propre implémentation IExtensioncollection qui appelle les méthodes attach et detach lorsque des éléments sont ajoutés / supprimés. Notez également qu'il vous appartient de définir les points d'extension dans votre classe extensible. L'exemple utilise un mécanisme de rappel de type événement:

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

Cette approche présente l'avantage de la réutilisation des extensions si vous parvenez à extraire des points d'extension communs entre vos classes.

m0sa
la source
1
C'est peut-être parce que je n'utilise pas .NET, mais j'ai trouvé cette réponse très difficile à comprendre. Pouvez-vous ajouter un exemple d'utilisation de ces trois interfaces?
Daniel Kaplan
Merci, très intéressant. Mais ne serait-il pas beaucoup plus simple d'enregistrer les rappels des extensions dans un objet extensible? Quelque chose comme processor.addHandler(console)fourni qui ConsoleProcessorimplémenterait l'interface de rappel elle-même. Le modèle "d'extension" ressemble à un mélange de visitoret decorator, mais est-il nécessaire dans ce cas?
Stan
Si vous enregistrez des extensions directement dans le processeur (== ExtensibleObject), vous avez un couplage serré. L'idée ici est d'avoir des extensions qui peuvent être réutilisées entre des objets extensibles avec les mêmes points d'extension. En fait, le modèle est mora comme une simulation de modèle mixin .
m0sa
Hm, je ne suis pas sûr qu'une liaison par interface soit un couplage serré. La chose la plus intéressante est que même par le modèle d' extension, l'objet extensible et l'extension s'appuient sur la même interface de travail ("rappel"), de sorte que le couplage reste le même, à mon humble avis. En d'autres termes, je ne peux pas brancher d'extension existante dans un nouvel objet extensible sans codage pour la prise en charge de l'interface de travail dans le "processeur". Si le «point d'extension» signifie en fait l'interface de travail, alors je ne vois pas de différence.
Stan
0

À partir d'Android 3.0, il peut être possible de résoudre ce problème avec élégance en utilisant un fragment . Les fragments ont leurs propres rappels de cycle de vie et peuvent être placés dans une activité. Je ne suis pas sûr que cela fonctionnera pour tous les événements.

Une autre option dont je ne suis pas sûr non plus (manquant d'un savoir-faire Android approfondi) pourrait être d'utiliser un décorateur de la manière opposée suggérée par k3b: créer un ActivityWrapperdont les méthodes de rappel contiennent votre code commun, puis transmettre à un Activityobjet encapsulé (votre classes d'implémentation réelles), puis demandez à Android de démarrer ce wrapper.

Michael Borgwardt
la source
-3

Il est vrai que Java n'autorise pas l'héritage multiple, mais vous pouvez le simuler plus ou moins en faisant que chacune de vos sous-classes SomeActivity étende la classe Activity d'origine.

Vous aurez quelque chose comme:

public class TabActivity extends Activity {
    .
    .
    .
}
Samer
la source
2
C'est ce que font déjà les classes, mais la classe de base Activity fait partie de l'API Android et ne peut pas être modifiée par les développeurs.
Michael Borgwardt