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

33

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

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

Basefournit la plupart des fonctionnalités, et j'ai une grande collection d' Baseobjets. Cependant, tous les Baseobjets sont soit un, Sub1soit un Sub2et parfois j'ai besoin de savoir qui ils sont.

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

Donc, je suis venu avec une stratégie pour éviter cela sans casting. Basea maintenant ces méthodes:

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

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

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

Et les Sub2met en œuvre dans le sens opposé.

Malheureusement, maintenant Sub1et Sub2ont ces méthodes dans leur propre API. Donc, je peux 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 seulement un Base, ces méthodes sont non dépréciées 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. Cela me semble élégant d’une certaine manière, mais d’un autre côté, j’abuse un peu des annotations obsolètes comme moyen de "supprimer" des méthodes d’une classe.

Puisqu'une Sub1instance est vraiment une base, il est logique d'utiliser l'héritage plutôt que l'encapsulation. Est-ce que je fais bien? Existe-t-il un meilleur moyen de résoudre ce problème?

décrypteur
la source
12
Les 3 classes doivent maintenant se connaître. Ajouter Sub3 impliquerait beaucoup de changements de code, et ajouter Sub10 serait carrément douloureux
Dan Pichelman
15
Cela vous aiderait beaucoup si vous nous donniez du vrai code. Il y a des situations où il est approprié de prendre des décisions en fonction d'une catégorie particulière de choses, mais il est impossible de dire si vous êtes justifié dans ce que vous faites avec de tels exemples artificiels. Pour ce que ça vaut, ce que vous voulez, c'est un visiteur ou une union étiquetée .
Doval
1
Désolé, je pensais que cela faciliterait la tâche en donnant des exemples simplifiés. Peut-être que je posterai une nouvelle question avec une portée plus large de ce que je veux faire.
Codebreaker
12
Vous ne faites que réimplémenter le casting et instanceof, d'une manière qui nécessite beaucoup de dactylographie, est source d'erreurs et rend difficile l'ajout de plusieurs sous-classes.
user253751
5
Si Sub1et Sub2ne peuvent pas être utilisés de manière interchangeable, alors pourquoi les traitez-vous comme tels? Pourquoi ne pas garder une trace de vos "sandwich-makers" et de vos "alien-contacters" séparément?
Pieter Witvoet

Réponses:

27

Cela n'a pas toujours de sens d'ajouter des fonctions à la classe de base, comme suggéré dans certaines des réponses. L'ajout d'un trop grand nombre de fonctions de cas particulier peut aboutir à la liaison de composants sans lien entre eux.

Par exemple, je pourrais avoir une Animalclasse, avec Catet Dogcomposants. Si je veux pouvoir les imprimer ou les afficher dans l'interface graphique, il pourrait être excessif pour moi d'ajouter renderToGUI(...)et sendToPrinter(...)à la classe de base.

L’approche que vous utilisez, à l’aide des vérifications de type et des transtypages, est fragile - mais au moins gardez les problèmes séparés.

Cependant, si vous vous retrouvez souvent confronté à ce type de vérifications / lancements, une option consiste à implémenter le modèle visiteur / double envoi correspondant. Cela ressemble un peu à ça:

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. La nouvelle classe de visiteurs devient également une cible intéressante pour intégrer des fonctions. Par exemple , il peut être judicieux de se 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 loin en massant et en utilisant les lambdas et la diffusion en continu de Java 8 vous permettraient de

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

Quelle OMI est à la fois aussi soignée et succincte que possible.

Voici un exemple plus complet de Java 8:

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));
Michael Anderson
la source
+1: Ce genre de chose est couramment utilisé dans les langages qui prennent en charge de manière optimale les types de somme et la correspondance de modèle. Il s'agit d'une conception valide en Java, même si sa syntaxe est très laide.
Mankarse
@Mankarse Un peu de sucre syntaxique peut le rendre très loin d'être moche - je l'ai mis à jour avec un exemple.
Michael Anderson
Supposons que le PO change d’avis et décide d’ajouter un Sub3. Le visiteur n'a-t-il pas exactement le même problème que instanceof? Maintenant, vous devez ajouter onSub3au visiteur et cela casse si vous oubliez.
Radiodef
1
@Radiodef Un avantage de l'utilisation du modèle de visiteur par rapport à l'activation directe du type est qu'une fois que vous avez ajouté onSub3l'interface de visiteur, vous obtenez une erreur de compilation à chaque endroit où vous créez un nouveau visiteur qui n'a pas encore été mis à jour. Au contraire, l'activation du type générera au mieux une erreur d'exécution - ce qui peut être plus difficile à déclencher.
Michael Anderson
1
@ FiveNine, si vous souhaitez ajouter cet exemple complet à la fin de la réponse, qui pourrait aider les autres.
Michael Anderson
83

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 catset fish. animalsavoir des propriétés communes à catset fish. Mais cela ne suffit pas: il existe certaines propriétés qui se différencient catdes autres fish, vous devez donc créer une sous-classe.

Maintenant, vous avez le problème, vous avez oublié de modéliser le mouvement . D'accord. C'est relativement facile:

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()
}

moveserait un comportement partagé mis en œuvre différemment par chaque animal.

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

Cela signifie que votre design est cassé.

Ma recommandation: refactor Base, Sub1et Sub2.

Thomas Junk
la source
8
Si vous offriez du code, je pourrais faire des recommandations.
Thomas Junk
9
@codebreaker Si vous êtes d'accord avec votre exemple, je vous recommande d'accepter cette réponse, puis de rédiger une nouvelle question. Ne révisez pas la question pour avoir un exemple différent, sinon les réponses existantes n'auront aucun sens.
Disque Moby
16
@codebreaker Je pense que cela "résout" le problème en répondant à la vraie question. La vraie question étant: comment résoudre mon problème? Refacturez votre code correctement. Refactor afin que vous .move()ou .attack()et bien abstraite thatpartie - au lieu d'avoir .swim(), .fly(), .slide(), .hop(), .jump(), .squirm(), .phone_home(), etc. Comment refactoring-vous correctement? Inconnu sans meilleurs exemples ... mais toujours la bonne réponse - à moins que l'exemple de code et plus de détails ne suggèrent le contraire.
WernerCD
11
@codebreaker Si votre hiérarchie de classe vous conduit à de telles situations, cela n'a aucun sens par définition
Patrick Collins
7
@ codebreaker En lien avec le dernier commentaire de Crisfole, il existe une autre façon de voir les choses qui pourrait aider. Par exemple, avec movevs fly/ run/ etc, cela peut être expliqué en une phrase comme suit: Vous devez indiquer à l’objet quoi faire et non comment le faire. En termes d’accesseurs, comme vous le dites, c’est plus le cas réel dans un commentaire ici, vous devriez poser des questions à l’objet, pas en inspecter l’état.
Izkata
9

Il est un peu difficile d'imaginer une situation dans laquelle vous avez un groupe de choses et que vous voulez qu'elles fassent un sandwich ou contactent des extraterrestres. Dans la plupart des cas où vous trouvez un tel casting, vous utiliserez un type - par exemple, vous filtrez un ensemble de noeuds pour les déclarations où getAsFunction renvoie non-null, plutôt que de faire quelque chose de différent pour chaque noeud de la liste.

Il se peut que vous ayez besoin d'effectuer une séquence d'actions et qu'il ne soit pas pertinent que les objets effectuant 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, faire en sorte que Base implémente une méthode pour renvoyer l'action, ou tout autre moyen permettant de sélectionner l'action dans les occurrences de votre liste de base (puisque vous dites que vous ne pouvez pas utiliser le polymorphisme, donc supposément l'action à effectuer. n'est pas une fonction de la classe mais une autre propriété des bases, sinon vous donneriez à Base la méthode act (Context)

Pete Kirkham
la source
2
Vous comprenez que mes méthodes absurdes sont conçues pour montrer les différences entre ce que les classes peuvent faire une fois leur sous-classe connue (et qu’elles n’ont plus rien à faire entre elles), mais je pense qu’avoir un Contextobjet ne fait qu’aggraver les choses. Je pourrais aussi bien passer Objectaux méthodes, les exprimer et espérer qu’elles sont du bon type.
Codebreaker
1
@codebreaker, vous devez vraiment clarifier votre question à ce moment-là. Dans la plupart des systèmes, il existe une raison valable d'appeler un tas de fonctions dépourvues de sens de ce qu'elles font. par exemple, traiter des règles sur un événement ou effectuer une étape dans une simulation. Ces systèmes ont généralement un contexte dans lequel les actions se produisent. Si les propriétés 'getRandomIngredients' et 'getFrequency' ne sont pas liées, vous ne devriez pas être dans le même objet et vous devez encore adopter une approche différente, telle que les actions capturent la source des ingrédients ou leur fréquence.
Pete Kirkham
1
Vous avez raison, et je ne pense pas pouvoir sauver cette question. J'ai fini par choisir un mauvais exemple, alors je vais probablement en poster un autre. Les méthodes getFrequency () et getIngredients () n'étaient que des espaces réservés. J'aurais probablement dû simplement mettre "..." comme arguments pour rendre plus évident le fait que je ne saurais pas ce qu'ils sont.
Codebreaker
C’est la bonne réponse à l’OMI. Comprenez que cela Contextn’a pas besoin d’être une nouvelle classe, ni même une interface. Habituellement, le contexte n'est que l'appelant, les appels sont donc dans le formulaire action.act(this). Si getFrequency()et getRandomIngredients()sont des méthodes statiques, vous n'aurez peut-être même pas besoin d'un contexte. C’est ainsi que je mettrais en œuvre une file d’emploi, par exemple, qui résume votre problème.
QuestionC
En outre, ceci est essentiellement identique à la solution de modèle de visiteur. La différence est que vous implémentez les méthodes Visitor :: OnSub1 () / Visitor :: OnSub2 () en tant que Sub1 :: act () / Sub2 :: act ()
QuestionC
4

Et si vos sous-classes implémentaient une ou plusieurs interfaces définissant 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 va devenir:

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:

  • pas de casting impliqué
  • chaque sous-classe peut 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 pas penser à quelque chose de plus cool à ce moment précis: D.

Radu Murzea
la source
3
Cela ne résout pas réellement le problème. Vous avez toujours besoin de la distribution dans la boucle for. (Si vous ne le faites pas, vous n'avez pas besoin de la distribution dans l'exemple d'OP).
Taemyr
2

L'approche peut être un bon dans les cas où presque tous les types au sein d' une famille , soit directement utilisable comme une mise en œuvre d' une certaine interface qui répond à certains critères ou peut être utilisé pour créer une implémentation de cette interface. Les types de collection intégrés auraient profité de ce modèle pour IMHO, mais comme ils ne le font pas à des fins d'exemple, je vais inventer une interface de collection BunchOfThings<T>.

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

  1. Fred sait qu'il contient les seules références à cela BunchOfThings, et aucune référence extérieure à BunchOfThingsses composants internes n'existe dans l'univers. Si personne d’autre n’a une référence à BunchOfThingsou à ses éléments internes, personne ne pourra la modifier et la contrainte sera satisfaite.

  2. Ni le BunchOfThings, ni aucun de ses composants internes auxquels il existe des références externes, 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 inconditionnellement tout objet reçu (en traitant de manière récursive les composants imbriqués). Une autre solution serait de tester si un objet reçu promet l'immuabilité et, si ce n'est pas le cas, 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 AsImmutableméthode qui demande à un objet de faire une copie immuable de lui-même (en utilisant AsImmutablesur tout composant imbriqué qui la prend en charge).

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

Notez que, si les asXXméthodes peuvent renvoyer des types légèrement différents, leur véritable rôle est de s’assurer que les objets renvoyés répondent aux besoins du programme.

supercat
la source
0

Ignorant la question de savoir si votre conception est bonne ou non, et si elle est bonne ou au moins acceptable, je voudrais examiner les capacités des sous-classes, pas le type.

Par conséquent, soit:


Déplacez des connaissances sur l'existence de sandwichs et d'aliens 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 générer des exceptions et modifiez 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 ou les deux sous-classes se substituent canMakeASandwich()et un seul implémente chacun des éléments makeASandwich()and contactAliens().


Utilisez des interfaces, et non des sous-classes concrètes, pour détecter les capacités d'un type. Laissez la classe de base seule et modifiez 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 correspond pas à votre style, ou d'ailleurs à tout style Java que vous jugeriez judicieux):

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

Personnellement, je n'aime généralement pas ce dernier type d'attraper une exception à moitié attendue, à cause du risque d'attraper de façon inappropriée une ClassCastExceptionsortie de getRandomIngredientsou makeASandwich, mais YMMV.

Steve Jessop
la source
2
Catching ClassCastException est juste ew. C'est tout le but d'instanceof.
Radiodef
@Radiodef: vrai, mais l'un des objectifs <est d'éviter d'attraper ArrayIndexOutOfBoundsException, et certaines personnes décident de le faire aussi. Pas de prise en compte du goût ;-) En gros, mon "problème" est de travailler en Python, où le style préféré est qu'il est préférable de capturer une exception plutôt que de vérifier à l'avance si l'exception se produira, aussi simple que le test préalable soit. . Peut-être que la possibilité ne vaut tout simplement pas la peine d'être mentionnée en Java.
Steve Jessop
@SteveJessop "Il est plus facile de demander pardon que d'autorisation", tel est le slogan;)
Thomas Junk
0

Nous avons ici un cas intéressant de classe de base qui se réduit à 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, voyons quelles sont les contraintes qui pourraient en résulter:

  1. Nous pouvons couvrir tous les cas de la classe de base.
  2. Aucune classe dérivée étrangère ne devrait ajouter un nouveau cas, jamais.
  3. Nous pouvons vivre avec la politique pour laquelle appel à faire être sous le contrôle de la classe de base.
  4. Mais notre science nous a appris que la politique à suivre dans une classe dérivée pour une méthode n'appartenant pas à 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 directement que nous pouvons énumérer toutes les classes dérivées, ce qui signifie qu'il n'y a pas de classe dérivée en dehors.

Mais voici la chose. Si elles sont toutes à vous, vous pouvez remplacer le if par une abstraction qui est un appel de méthode virtuelle (même si cela n'a aucun sens) et vous débarrasser du if et de l'auto-casting. Par conséquent, ne le fais pas. Un meilleur design est disponible.

Josué
la source
-1

Mettez une méthode abstraite dans la classe de base qui fait une chose dans Sub1 et l'autre dans Sub2, et appelez cette méthode abstraite dans vos boucles mixtes.

class Sub1 : Base {
    @Override void doAThing() {
        this.makeASandwich(getRandomIngredients());
    }
}

class Sub2 : Base {
    @Override void doAThing() {
        this.contactAliens(getFrequency());
    }
}

for (Base base : bases) {
    base.doAThing();
}

Notez que cela peut nécessiter d'autres modifications, en fonction de la définition des méthodes getRandomIngredients et getFrequency. Mais honnêtement, au sein de la classe qui contacte les extraterrestres est probablement le meilleur endroit pour définir getFrequency de toute façon.

Incidemment, vos méthodes asSub1 et asSub2 sont définitivement une mauvaise pratique. Si vous voulez faire cela, faites-le en faisant un casting. Il n'y a rien que ces méthodes vous donnent que le casting ne fait pas.

Au hasard832
la source
2
Votre réponse suggère que vous n'avez pas lu la question complètement: le polymorphisme ne la coupera pas ici. Il y a plusieurs méthodes sur Sub1 et Sub2. Ce ne sont que des exemples et "doAThing" ne veut rien dire en réalité. Cela ne correspond à rien de ce que je ferais. En outre, comme pour votre dernière phrase: quid de la sécurité de type garantie?
Codebreaker
@codebreaker - Cela correspond exactement à ce que vous faites dans la boucle de l'exemple. Si cela n'a pas de sens, n'écrivez pas la boucle. Si cela n'a pas de sens en tant que méthode, cela n'a pas de sens en tant que corps de boucle.
Random832
1
Et vos méthodes "garantissent" la sécurité du type de la même manière que le casting: en lançant une exception si l'objet est du type incorrect.
Random832
Je me rends compte que j'ai choisi un mauvais exemple. Le problème est que la plupart des méthodes des sous-classes ressemblent davantage à des méthodes d’accès que des méthodes de mutation. Ces classes ne font pas les choses elles-mêmes, mais fournissent simplement des données. Le polymorphisme ne m'aidera pas. Je traite avec les producteurs, pas les consommateurs. En outre, le lancement d'une exception est une garantie d'exécution, qui n'est pas aussi efficace que la compilation.
Codebreaker
1
Mon point est que votre code fournit uniquement une garantie d'exécution. Rien n'empêche quelqu'un de ne pas vérifier isSub2 avant d'appeler asSub2.
Random832
-2

Vous pouvez pousser les deux makeASandwichet contactAliensdans la classe de base, puis les implémenter avec des implémentations factices. Étant donné que les types / arguments de retour ne peuvent pas être résolus en une classe de base commune.

class Sub1 extends Base{
    Sandwich makeASandwich(Ingredients i){
        //Normal implementation
    }
    boolean contactAliens(Frequency onFrequency){
        return false;
    }
 }
class Sub2 extends Base{
    Sandwich makeASandwich(Ingredients i){
        return null;
    }
    boolean contactAliens(Frequency onFrequency){
       // normal implementation
    }
 }

Il y a des inconvénients évidents à ce genre de choses, comme ce que cela signifie pour le contrat de la méthode et ce que vous pouvez et ne pouvez pas déduire du contexte du résultat. Vous ne pouvez pas penser depuis que je n'ai pas réussi à faire un sandwich les ingrédients ont été utilisés dans la tentative, etc.

Signe
la source
1
Je pensais le faire, mais cela contrevenait au principe de substitution de Liskov, et il n’est pas aussi agréable de travailler avec, par exemple, lorsque vous tapez "sub1". et l'auto-complétion arrive, vous voyez makeASandwich mais pas contactAliens.
Codebreaker
-5

Ce que vous faites est parfaitement légitime. Ne faites pas attention aux opposants qui ne font que réitérer le dogme parce qu'ils le lisent dans certains livres. Le dogme n'a pas sa place dans l'ingénierie.

J'ai utilisé le même mécanisme à plusieurs reprises, et je peux affirmer avec certitude que le runtime java aurait également pu faire la même chose à au moins un endroit auquel je peux penser, améliorant ainsi les performances, la convivialité et la lisibilité du code. l'utilise.

Prenons par exemple java.lang.reflect.Member, qui est la base de java.lang.reflect.Fieldet java.lang.reflect.Method. (La hiérarchie réelle est un peu plus compliquée que cela, mais ce n'est pas pertinent.) Les champs et les méthodes sont des animaux très différents: l'un a une valeur que vous pouvez obtenir ou définir, tandis que l'autre n'a rien de tel, mais il peut être invoqué avec un certain nombre de paramètres et il peut renvoyer une valeur. Ainsi, les champs et les méthodes sont tous les deux membres, mais les choses que vous pouvez faire avec eux sont aussi différents les uns des autres que de faire des sandwichs ou de contacter des extraterrestres.

Maintenant, quand on écrit un code qui utilise la réflexion, on a très souvent un Memberentre nos mains, et nous savons que c’est un Methodou un Field(ou, rarement, autre chose), et pourtant nous devons faire tout l’ennui instanceofpour comprendre avec précision ce qu’il est et ensuite nous devons le lancer pour obtenir une référence appropriée à celui-ci. (Et ce n'est pas seulement fastidieux, mais cela ne fonctionne pas très bien non plus.) La Methodclasse aurait pu très facilement implémenter le modèle que vous décrivez, facilitant ainsi la vie de milliers de programmeurs.

Bien sûr, cette technique n’est viable que dans de petites hiérarchies bien définies de classes étroitement couplées dont vous avez (et aurez toujours) le contrôle au niveau source: vous ne voulez pas le faire si votre hiérarchie de classes est susceptible d’être étendu par des personnes qui ne sont pas libres de reformer la classe de base.

Voici ce que j'ai fait diffère de ce que vous avez fait:

  • La classe de base fournit une implémentation par défaut pour toute la asDerivedClass()famille de méthodes, chacune étant renvoyée null.

  • Chaque classe dérivée remplace uniquement l'une des asDerivedClass()méthodes et renvoie thisau lieu de null. Il ne remplace aucun des autres, et ne veut rien savoir à leur sujet. Donc, pas de IllegalStateExceptions sont jetés.

  • La classe de base fournit également des finalimplémentations pour toute la isDerivedClass()famille de méthodes, codée comme suit: Ainsi return asDerivedClass() != null; , le nombre de méthodes devant être remplacées par des classes dérivées est réduit au minimum.

  • Je n'ai pas utilisé @Deprecatedce mécanisme parce que je n'y avais pas pensé. Maintenant que vous m'avez donné l'idée, je vais la mettre à profit, merci!

C # a un mécanisme associé intégré via l'utilisation du asmot clé. En C #, vous pouvez dire DerivedClass derivedInstance = baseInstance as DerivedClasset vous obtiendrez une référence à un DerivedClasssi vous étiez baseInstancede cette classe, ounull non. Cela (théoriquement) donne de meilleurs résultats que ceux issuivis par la conversion ( isest le mot-clé C # pour instanceof, bien sûr ), mais le mécanisme personnalisé que nous avons créé manuellement est encore plus performant: la paire d' instanceofopérations de conversion et de conversion de Java, également. en tant asqu'opérateur de C #, n'agissez pas aussi rapidement que l'appel de méthode virtuelle unique de notre approche personnalisée.

J'avance par la présente la proposition selon laquelle cette technique devrait être déclarée être un motif et qu'il faudrait lui trouver un joli nom.

Gee, merci pour les votes négatifs!

Un résumé de la controverse, pour vous éviter la peine de lire les commentaires:

L'objection des gens semble être que la conception d'origine était fausse, ce qui signifie que vous ne devriez jamais avoir de classes très différentes dérivant d'une classe de base commune, ou que même si vous le faites, le code qui utilise une telle hiérarchie ne devrait jamais être dans la position d'avoir une référence de base et besoin de comprendre la classe dérivée. Par conséquent, disent-ils, le mécanisme d'auto-casting proposé par cette question et par ma réponse, qui améliore l'utilisation de la conception d'origine, n'aurait jamais dû être nécessaire. (Ils ne disent vraiment rien sur le mécanisme de l'auto-casting, ils se plaignent seulement de la nature des dessins auxquels le mécanisme est censé s'appliquer.)

Cependant, dans l'exemple ci - dessus j'ai déjà montré que les créateurs du moteur d' exécution Java ont en fait choisir la conception d'une précision telle la java.lang.reflect.Member, Field, la Methodhiérarchie, et dans les commentaires ci - dessous je aussi montrer que les créateurs du C # runtime sont arrivés indépendamment à un équivalent conception de la System.Reflection.MemberInfo, FieldInfo, la MethodInfohiérarchie. Il s’agit donc de deux scénarios réels différents, qui se présentent sous le nez de tout le monde et qui proposent des solutions manifestement exploitables en utilisant précisément ces conceptions.

C'est ce que tous les commentaires suivants résument. Le mécanisme de l'auto-casting est à peine mentionné.

Mike Nakis
la source
13
C'est légitime si vous vous êtes conçu dans une boîte, bien sûr. C'est le problème avec beaucoup de ces types de questions. Les gens prennent des décisions dans d’autres domaines de la conception, ce qui force ce type de solution de piratage lorsque la vraie solution consiste à comprendre pourquoi vous vous êtes retrouvé dans cette situation. Un design décent ne vous mènera pas ici. Je suis en train de regarder une application 200K SLOC et je ne vois nulle part où nous devions le faire. Ce n'est donc pas simplement quelque chose que les gens lisent dans un livre. Ne pas se retrouver dans ce type de situation est simplement le résultat final de l'application de bonnes pratiques.
Dunk
3
@Dunk Je viens de montrer que la nécessité d'un tel mécanisme existe par exemple dans la java.lang.reflection.Memberhiérarchie. Les créateurs de Java Runtime se sont retrouvés dans ce type de situation, je ne suppose pas que vous diriez qu'ils se sont conçus eux-mêmes dans une boîte ou que vous les blâmez de ne pas suivre une sorte de meilleure pratique? Donc, ce que je veux dire ici, c'est qu'une fois que vous avez ce genre de situation, ce mécanisme est un bon moyen d'améliorer les choses.
Mike Nakis
6
@MikeNakis Les créateurs de Java ont pris de nombreuses mauvaises décisions, ce qui en dit moins. Quoi qu'il en soit, utiliser un visiteur serait mieux que de faire un test ad-hoc puis une diffusion.
Doval
3
De plus, l'exemple est imparfait. Il existe une Memberinterface, mais elle ne sert qu'à vérifier les qualificatifs d'accès. Généralement, vous obtenez une liste de méthodes ou de propriétés et vous l'utilisez directement (sans les mélanger, aucune List<Member>apparence n'apparaît), vous n'avez donc pas besoin de transtyper. Notez que Classne fournit pas de getMembers()méthode. Bien sûr, un programmeur désemparés pourrait créer List<Member>, mais cela ferait aussi peu de sens que ce qui en fait une List<Object>et en ajoutant certains Integerou Stringà elle.
SJuan76
5
@ MikeNakis "Beaucoup de gens le font" et "Une personne célèbre le fait" ne sont pas de vrais arguments. Les antipatternes sont à la fois de mauvaises idées et largement utilisés par définition, mais ces excuses justifieraient leur utilisation. Donnez de vraies raisons d'utiliser un design ou un autre. Mon problème avec le casting ad-hoc est que chaque utilisateur de votre API doit le faire correctement à chaque fois qu'il le fait. Un visiteur doit seulement être correct une fois. Cacher le casting derrière certaines méthodes et revenir à la nullplace d'une exception ne le rend pas beaucoup mieux. Toutes choses étant égales par ailleurs, le visiteur est plus en sécurité tout en faisant le même travail.
Doval