web-dev-qa-db-fra.com

La substitution de méthodes concrètes est-elle une odeur de code?

Est-il vrai que remplacer des méthodes concrètes est une odeur de code? Parce que je pense que si vous avez besoin de remplacer des méthodes concrètes:

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

il peut être réécrit comme

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

et si B veut réutiliser a() dans A il peut être réécrit comme:

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

qui ne nécessite pas d'héritage pour remplacer la méthode, est-ce vrai?

31
ggrr

Non, ce n'est pas une odeur de code.

  • Si une classe n'est pas définitive, elle permet d'être sous-classée.
  • Si une méthode n'est pas définitive, elle permet d'être remplacée.

Il incombe à chaque classe d'examiner attentivement si le sous-classement est approprié et quelles méthodes peuvent être remplacées.

La classe peut se définir elle-même ou n'importe quelle méthode comme finale, ou peut imposer des restrictions (modificateurs de visibilité, constructeurs disponibles) sur la façon et l'endroit où elle est sous-classée.

Le cas habituel pour remplacer les méthodes est un implémentation par défaut dans la classe de base qui peut être personnalisé ou optimisé dans la sous-classe (en particulier dans Java 8 avec l'avènement des méthodes par défaut) dans les interfaces).

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

Un alternative pour remplacer le comportement est le Strategy Pattern, où le comportement est résumé sous forme d'interface, et l'implémentation peut être définie dans la classe.

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}
34
Peter Walser

Est-il vrai que remplacer des méthodes concrètes est une odeur de code?

Oui, en général, remplacer les méthodes concrètes est une odeur de code.

Parce que la méthode de base a un comportement associé que les développeurs respectent généralement, la modification qui entraînera des bogues lorsque votre implémentation fera quelque chose de différent. Pire, s'ils modifient le comportement, votre implémentation précédemment correcte peut exacerber le problème. Et en général, il est assez difficile de respecter le Liskov Substitution Principle pour les méthodes concrètes non triviales.

Maintenant, il y a des cas où cela peut être fait et c'est bien. Les méthodes semi-triviales peuvent être remplacées en toute sécurité. Les méthodes de base qui n'existent que comme une sorte de comportement "par défaut sain" qui est censé être écrasé ont de bonnes utilisations.

Par conséquent, cela me semble être une odeur - parfois bonne, généralement mauvaise, jetez un œil pour vous en assurer.

25
Telastyn

si vous avez besoin de remplacer des méthodes [...] concrètes, il peut être réécrit comme

S'il peut être réécrit de cette façon, cela signifie que vous avez le contrôle sur les deux classes. Mais alors vous savez si vous avez conçu la classe A pour être dérivée de cette façon et si vous l'avez fait, ce n'est pas une odeur de code.

Si, d'autre part, vous n'avez pas de contrôle sur la classe de base, vous ne pouvez pas réécrire de cette façon. Donc, soit la classe a été conçue pour être dérivée de cette façon, dans ce cas, allez-y, ce n'est pas une odeur de code, ou ce n'est pas le cas, dans ce cas, vous avez deux options: chercher une autre solution tout à fait, ou continuer quand même , car le travail l'emporte sur la pureté de la conception.

7
Jan Hudec

Tout d'abord, permettez-moi de souligner que cette question est seulement applicable dans certains systèmes de frappe. Par exemple, dans Typage structurel et Typage de canard systèmes - une classe simplement ayant une méthode avec le même nom & signature serait signifie cette classe était type compatible (au moins pour cette méthode particulière, dans le cas de Duck dactylographie).

Ceci est (évidemment) très différent du domaine des langages typés statiquement , tels que Java, C++, etc. De même que la nature de typage fort , tel qu'utilisé dans les deux Java & C++, ainsi que C # et autres.


Je suppose que vous venez d'un arrière-plan Java, où les méthodes doivent être explicitement marquées comme finales si le concepteur de contrat les destine pas à remplacer. Cependant, dans des langages tels que C #, l'inverse est la valeur par défaut. Les méthodes sont (sans décoration, mots-clés spéciaux) "sealed "(la version C # de final), et doit être explicitement déclaré comme virtual s'ils sont autorisés à remplacer.

Si une API ou une bibliothèque a été conçue dans ces langages avec une méthode concrète qui est surchargeable, alors elle doit (au moins dans certains cas (s)) il est logique qu'il soit annulé. Par conséquent, ce n'est pas une odeur de code de remplacer une méthode concrète non scellée/virtuelle/non final(Cela suppose bien sûr que les concepteurs de l'API suivent les conventions et marquent comme virtual (C #), ou marquent comme final (Java) les méthodes appropriées pour ce qu'ils conçoivent. )


Voir aussi

3
Tersosauros

Oui, c'est car cela peut conduire à appeler super anti-pattern. Il peut également dénaturer l'objectif initial de la méthode, introduire des effets secondaires (=> bugs) et faire échouer les tests. Utiliseriez-vous une classe dont les tests unitaires sont rouges (failling)? Je ne le ferai pas.

J'irai encore plus loin en disant que l'odeur initiale du code est quand une méthode n'est ni abstract ni final. Parce qu'il permet de modifier (en redéfinissant) le comportement d'une entité construite (ce comportement peut être perdu ou altéré). Avec abstract et final l'intention est sans ambiguïté:

  • Résumé: vous devez fournir un comportement
  • Final: vous n'êtes pas autorisé à modifier le comportement

La façon la plus sûre d'ajouter un comportement à une classe existante est ce que montre votre dernier exemple: utiliser le motif décorateur.

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}
2
Spotted