web-dev-qa-db-fra.com

Est-il correct d'avoir des objets qui se castent, même si cela pollue l'API de leurs sous-classes?

J'ai une classe de base, Base. Il a deux sous-classes, Sub1 Et Sub2. Chaque sous-classe a des méthodes supplémentaires. Par exemple, Sub1 A Sandwich makeASandwich(Ingredients... ingredients) et Sub2 A boolean contactAliens(Frequency onFrequency).

Étant donné que ces méthodes prennent différents paramètres et font des choses entièrement différentes, elles sont complètement incompatibles, et je ne peux pas simplement utiliser le polymorphisme pour résoudre ce problème.

Base fournit la plupart des fonctionnalités, et j'ai une grande collection d'objets Base. Cependant, tous les objets Base sont soit un Sub1 Soit un Sub2, Et j'ai parfois besoin de savoir de quoi il s'agit.

Cela semble être une mauvaise idée de faire ce qui suit:

for (Base base : bases) {
    if (base instanceof Sub1) {
        ((Sub1) base).makeASandwich(getRandomIngredients());
        // ... etc.
    } else { // must be Sub2
        ((Sub2) base).contactAliens(getFrequency());
        // ... etc.
    }
}

J'ai donc trouvé une stratégie pour éviter cela sans lancer. Base a maintenant ces méthodes:

boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();

Et bien sûr, Sub1 Implémente ces méthodes comme

boolean isSub1() { return true; }
Sub1 asSub1();   { return this; }
Sub2 asSub2();   { throw new IllegalStateException(); }

Et Sub2 Les implémente de la manière opposée.

Malheureusement, Sub1 Et Sub2 Ont désormais ces méthodes dans leur propre API. Je peux donc le faire, par exemple, sur Sub1.

/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1();   { return this; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2();   { throw new IllegalStateException(); }

De cette façon, si l'objet est connu pour être uniquement un Base, ces méthodes ne sont pas obsolètes et peuvent être utilisées pour se "convertir" en un type différent afin que je puisse invoquer les méthodes de la sous-classe dessus. Cela me semble en quelque sorte élégant, mais d'un autre côté, j'abuse en quelque sorte les annotations obsolètes comme un moyen de "supprimer" des méthodes d'une classe.

Puisqu'une instance de Sub1 Est vraiment est une Base, il est logique d'utiliser l'héritage plutôt que l'encapsulation. Est-ce que je fais du bien? Existe-t-il une meilleure façon de résoudre ce problème?

33
codebreaker

Il n'est pas toujours logique d'ajouter des fonctions à la classe de base, comme suggéré dans certaines des autres réponses. L'ajout d'un trop grand nombre de fonctions de cas particulier peut entraîner la liaison de composants autrement non liés entre eux.

Par exemple, je pourrais avoir une classe Animal, avec des composants Cat et Dog. Si je veux pouvoir les imprimer ou les afficher dans l'interface graphique, il pourrait être exagéré pour moi d'ajouter renderToGUI(...) et sendToPrinter(...) à la classe de base.

L'approche que vous utilisez, à l'aide de vérifications de type et de transtypages, est fragile - mais au moins, les préoccupations sont séparées.

Cependant, si vous vous retrouvez souvent à effectuer ces types de vérifications/lancements, une option consiste à implémenter le modèle de visiteur/double expédition. Cela ressemble un peu à ceci:

public abstract class Base {
  ...
  abstract void visit( BaseVisitor visitor );
}

public class Sub1 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}

public class Sub2 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}

public interface BaseVisitor {
   void onSub1(Sub1 that);
   void onSub2(Sub2 that);
}

Maintenant, votre code devient

public class ActOnBase implements BaseVisitor {
    void onSub1(Sub1 that) {
       that.makeASandwich(getRandomIngredients())
    }

    void onSub2(Sub2 that) {
       that.contactAliens(getFrequency());
    }
}

BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
    base.visit(visitor);
}

Le principal avantage est que si vous ajoutez une sous-classe, vous obtiendrez des erreurs de compilation plutôt que des cas manquants en silence. La nouvelle classe de visiteurs devient également une cible intéressante pour intégrer des fonctions. Par exemple, il peut être judicieux de déplacer getRandomIngredients() dans ActOnBase.

Vous pouvez également extraire la logique de bouclage: par exemple, le fragment ci-dessus peut devenir

BaseVisitor.applyToArray(bases, new ActOnBase() );

Un peu plus de massage et d'utilisation de Java 8 vous permettront d'accéder à

bases.stream()
     .forEach( BaseVisitor.forEach(
       Sub1 that -> that.makeASandwich(getRandomIngredients()),
       Sub2 that -> that.contactAliens(getFrequency())
     ));

Quel IMO est à peu près aussi soigné et succinct que possible.

Voici un exemple Java 8 plus complet:

public static abstract class Base {
    abstract void visit( BaseVisitor visitor );
}

public static class Sub1 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub1(this); }

    void makeASandwich() {
        System.out.println("making a sandwich");
    }
}

public static class Sub2 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub2(this); }

    void contactAliens() {
        System.out.println("contacting aliens");
    }
}

public interface BaseVisitor {
    void onSub1(Sub1 that);
    void onSub2(Sub2 that);

    static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {

        return base -> {
            BaseVisitor baseVisitor = new BaseVisitor() {

                @Override
                public void onSub1(Sub1 that) {
                    sub1.accept(that);
                }

                @Override
                public void onSub2(Sub2 that) {
                    sub2.accept(that);
                }
            };
            base.visit(baseVisitor);
        };
    }
}

Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());

bases.stream()
     .forEach(BaseVisitor.forEach(
             Sub1::makeASandwich,
             Sub2::contactAliens));
27
Michael Anderson

De mon point de vue: votre conception est fausse.

Traduit en langage naturel, vous dites ce qui suit:

Étant donné que nous avons animals, il y a cats et fish. animals ont des propriétés communes à cats et fish. Mais cela ne suffit pas: il existe certaines propriétés qui différencient cat de fish, vous devez donc sous-classer.

Vous avez maintenant le problème que vous avez oublié de modéliser mouvement. D'accord. C'est relativement simple:

for(Animal a : animals){
   if (a instanceof Fish) swim();
   if (a instanceof Cat) walk();
}

Mais c'est une mauvaise conception. La bonne façon serait:

for(Animal a : animals){
    animal.move()
}

move serait un comportement partagé implémenté différemment par chaque animal.

Étant donné que ces méthodes prennent différents paramètres et font des choses entièrement différentes, elles sont complètement incompatibles, et je ne peux pas simplement utiliser le polymorphisme pour résoudre ce problème.

Cela signifie: votre design est cassé.

Ma recommandation: refactor Base, Sub1 et Sub2.

82
Thomas Junk

Il est un peu difficile d'imaginer une situation où vous avez un groupe de choses et que vous voulez qu'ils fassent un sandwich ou contactent des extraterrestres. Dans la plupart des cas, lorsque vous trouvez un tel casting, vous opérerez avec un seul type - par exemple en clang, vous filtrez un ensemble de nœuds pour les déclarations où getAsFunction renvoie non nul, plutôt que de faire quelque chose de différent pour chaque nœud de la liste.

Il se peut que vous ayez besoin d'effectuer une séquence d'actions et qu'il ne soit pas réellement pertinent que les objets exécutant l'action soient liés.

Donc au lieu d'une liste de Base, travaillez sur la liste des actions

for (RandomAction action : actions)
   action.act(context);

interface RandomAction {
    void act(Context context);
} 

interface Context {
    Ingredients getRandomIngredients();
    double getFrequency();
}

Vous pouvez, le cas échéant, demander à Base d'implémenter une méthode pour renvoyer l'action, ou tout autre moyen dont vous avez besoin pour sélectionner l'action dans les instances de votre liste de base (puisque vous dites que vous ne pouvez pas utiliser le polymorphisme, donc probablement l'action à entreprendre n'est pas une fonction de la classe mais une autre propriété des bases; sinon vous donneriez simplement à Base la méthode act (Context))

9
Pete Kirkham

Et si vos sous-classes implémentaient une ou plusieurs interfaces qui définissent ce qu'elles peuvent faire? Quelque chose comme ça:

interface SandwichCook
{
    public void makeASandwich(String[] ingredients);
}

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

Ensuite, vos cours ressembleront à ceci:

class Sub1 extends Base implements SandwichCook
{
    public void makeASandwich(String[] ingredients)
    {
        //some code here
    }
}

class Sub2 extends Base implements AlienRadioSignalAwarable
{
    public void contactAliens(int frequency)
    {
        //some code here
    }
}

Et votre boucle deviendra:

for (Base base : bases) {
    if (base instanceof SandwichCook) {
        base.makeASandwich(getRandomIngredients());
    } else if (base instanceof AlienRadioSignalAwarable) {
        base.contactAliens(getFrequency());
    }
}

Deux avantages majeurs de cette approche:

  • aucun casting impliqué
  • vous pouvez demander à chaque sous-classe d'implémenter autant d'interfaces que vous le souhaitez, ce qui offre une certaine flexibilité pour les modifications futures.

PS: Désolé pour les noms des interfaces, je ne pouvais penser à rien de plus cool à ce moment particulier: D.

4
Radu Murzea

L'approche peut être bonne dans les cas où presque n'importe quel type au sein d'une famille soit sera directement utilisable en tant qu'implémentation d'une interface qui répond à un critère o peut être utilisé pour créer une implémentation de cette interface. Les types de collection intégrés auraient à mon humble avis bénéficié de ce modèle, mais comme ils ne le sont pas à des fins d'exemple, j'inventerai une interface de collection BunchOfThings<T>.

Certaines implémentations de BunchOfThings sont mutables; certains ne le sont pas. Dans de nombreux cas, un objet Fred peut vouloir contenir quelque chose qu'il peut utiliser comme BunchOfThings et savoir que rien d'autre que Fred ne pourra le modifier. Cette exigence peut être satisfaite de deux manières:

  1. Fred sait qu'il contient les seules références à ce BunchOfThings, et aucune référence extérieure à ce BunchOfThings ir ses internes existent nulle part dans l'univers. Si personne d'autre n'a une référence au BunchOfThings ou à ses internes, personne d'autre ne pourra le modifier, donc la contrainte sera satisfaite.

  2. Ni le BunchOfThings, ni aucun de ses éléments internes auxquels des références externes existent, ne peuvent être modifiés par quelque moyen que ce soit. Si absolument personne ne peut modifier un BunchOfThings, alors la contrainte sera satisfaite.

Une façon de satisfaire la contrainte serait de copier sans condition tout objet reçu (en traitant récursivement tous les composants imbriqués). Un autre serait de tester si un objet reçu promet l'immuabilité et, sinon, d'en faire une copie, et de faire de même avec les composants imbriqués. Une alternative, qui est plus propre que la seconde et plus rapide que la première, consiste à proposer une méthode AsImmutable qui demande à un objet de faire une copie immuable de lui-même (en utilisant AsImmutable sur n'importe quel composants imbriqués qui le prennent en charge).

Des méthodes connexes peuvent également être fournies pour asDetached (à utiliser lorsque le code reçoit un objet et ne sait pas s'il veut le muter, auquel cas un objet mutable doit être remplacé par un nouvel objet mutable, mais un objet immuable peut être conservé tel quel), asMutable (dans les cas où un objet sait qu'il contiendra un objet précédemment renvoyé de asDetached, c'est-à-dire soit une référence non partagée à un objet mutable ou une référence partageable à une référence mutable) et asNewMutable (dans les cas où le code reçoit une référence externe et sait qu'il va vouloir muter une copie des données qu'il contient - si les données entrantes sont mutables, il n'y a aucune raison commencer par faire une copie immuable qui va être immédiatement utilisée pour créer une copie mutable puis abandonnée).

Notez que bien que les méthodes asXX puissent renvoyer des types légèrement différents, leur véritable rôle est de s'assurer que les objets retournés satisferont les besoins du programme.

2
supercat

Ignorant la question de savoir si vous avez une bonne conception ou non, et en supposant qu'elle soit bonne ou au moins acceptable, je voudrais considérer les capacités des sous-classes, pas le type.

Par conséquent, soit:


Déplacez quelques connaissances sur l'existence de sandwichs et d'extraterrestres dans la classe de base, même si vous savez que certaines instances de la classe de base ne peuvent pas le faire. Implémentez-le dans la classe de base pour lever des exceptions et changez le code en:

if (base.canMakeASandwich()) {
    base.makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    base.contactAliens(getFrequency());
    // ... etc.
}

Ensuite, une sous-classe ou les deux remplacent canMakeASandwich() et une seule implémente chacune de makeASandwich() et contactAliens().


Utilisez des interfaces, pas des sous-classes concrètes, pour détecter les capacités d'un type. Laissez la classe de base seule et changez le code en:

if (base instanceof SandwichMaker) {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    ((AlienContacter)base).contactAliens(getFrequency());
    // ... etc.
}

ou éventuellement (et n'hésitez pas à ignorer cette option si elle ne convient pas à votre style, ou d'ailleurs n'importe quel Java que vous jugeriez raisonnable):

try {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
} catch (ClassCastException e) {
    ((AlienContacter)base).contactAliens(getFrequency());
}

Personnellement, je n'aime pas généralement comme le dernier style d'attraper une exception à moitié attendue, en raison du risque d'attraper de manière inappropriée un ClassCastException sortant de getRandomIngredients ou makeASandwich, mais YMMV.

0
Steve Jessop

Nous avons ici un cas intéressant d'une classe de base qui se retransmet à ses propres classes dérivées. Nous savons très bien que c'est normalement mauvais, mais si nous voulons dire que nous avons trouvé une bonne raison, regardons et voyons quelles pourraient être les contraintes:

  1. Nous pouvons couvrir tous les cas de la classe de base.
  2. Aucune classe dérivée étrangère n'aurait à ajouter un nouveau cas, jamais.
  3. Nous pouvons vivre avec la politique pour laquelle l'appel à faire est sous le contrôle de la classe de base.
  4. Mais nous savons par notre science que la politique de ce qu'il faut faire dans une classe dérivée pour une méthode qui n'est pas dans la classe de base est celle de la classe dérivée et non de la classe de base.

Si 4 alors nous avons: 5. La politique de la classe dérivée est toujours sous le même contrôle politique que la politique de la classe de base.

2 et 5 impliquent tous deux directement que nous pouvons énumérer toutes les classes dérivées, ce qui signifie qu'il ne doit pas y avoir de classes dérivées extérieures.

Mais voici le truc. S'ils sont tous à vous, vous pouvez remplacer l'if par une abstraction qui est un appel de méthode virtuelle (même si c'est un non-sens) et vous débarrasser de l'if et de l'auto-cast. Par conséquent, ne le faites pas. Une meilleure conception est disponible.

0
Joshua