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?
virtual
mot - clé pour prendre en charge les remplacements. Ainsi, la question est rendue plus (ou moins) ambiguë par les particularités de la langue.final
modificateur existe exactement pour des situations comme celles-ci. Si le comportement de la méthode est tel qu'elle n'est pas conçue pour être remplacée, marquez-la commefinal
. Sinon, attendez-vous à ce que les développeurs choisissent de le remplacer.Réponses:
Non, ce n'est pas une odeur de code.
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 manière et l'endroit où elle est sous-classée.
Le cas habituel pour remplacer les méthodes est une implémentation par défaut dans la classe de base qui peut être personnalisée ou optimisée dans la sous-classe (en particulier dans Java 8 avec l'avènement des méthodes par défaut dans les interfaces).
Une alternative pour remplacer le comportement est le modèle de stratégie , où le comportement est résumé sous forme d'interface, et l'implémentation peut être définie dans la classe.
la source
A
implémenterDescriptionProvider
?A
pourrait implémenterDescriptionProvider
, et donc être le fournisseur de description par défaut. Mais l'idée ici est la séparation des préoccupations: aA
besoin de la description (d'autres classes pourraient aussi), tandis que d'autres implémentations les fournissent.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 principe de substitution de Liskov 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 qu'en tant que type de comportement "par défaut sain" destiné à être remplacé ont de bonnes utilisations.
Par conséquent, cela me semble être une odeur - parfois bonne, généralement mauvaise, jetez un coup d'œil pour vous en assurer.
la source
Number
classe avec uneincrement()
méthode qui définit la valeur à sa prochaine valeur valide. Si vous le sous-classe dansEvenNumber
oùincrement()
ajoute deux au lieu d'un (et le setter applique la régularité), la première proposition de l'OP nécessite des implémentations en double de chaque méthode de la classe et sa seconde nécessite l'écriture de méthodes wrapper pour appeler les méthodes dans le membre. L'héritage vise en partie à ce que les classes d'enfants expliquent clairement en quoi elles diffèrent des parents en ne fournissant que les méthodes qui doivent être différentes.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'était 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.
la source
abstract
; mais il y a de nombreux cas où cela n'a pas à être. 2) si elle ne doit pas être remplacée, elle doit l'êtrefinal
(non virtuelle). Mais il existe encore de nombreux cas où les méthodes peuvent être remplacées. 3) si le développeur en aval ne lit pas les documents, ils vont se tirer une balle dans le pied, mais ils le feront, quelles que soient les mesures que vous mettez en place.Tout d'abord, permettez-moi de souligner que cette question ne s'applique qu'à certains systèmes de dactylographie. Par exemple, en tapant structurel et frappe de canard systèmes - une classe tout simplement avoir une méthode avec le même nom et la signature ne signifie que la classe était de type compatible (au moins pour cette méthode particulière, dans le cas de la saisie de canard).
Ceci est (évidemment) très différent du domaine des langages typés statiquement , tels que Java, C ++, etc. Ainsi que la nature du typage fort , tel qu'utilisé à la fois en Java et C ++, ainsi qu'en 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 souhaite qu'elles ne soient pas remplacées. 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) " scellées " (version C # de
final
), et doivent être explicitement déclarées commevirtual
si elles pouvaient être remplacées.Si une API ou une bibliothèque a été conçue dans ces langages avec une méthode concrète qui peut être remplacée, alors elle doit (au moins dans certains cas) avoir un sens pour qu'elle soit remplacée. Par conséquent, ce n'est pas une odeur de code de remplacer une
final
méthode non scellée / virtuelle / non concrète. (Cela suppose bien sûr que les concepteurs de l'API suivent les conventions et marquent en tant quevirtual
(C #) ou marquent en tant quefinal
(Java) les méthodes appropriées pour ce qu'ils conçoivent.)Voir également
la source
Oui c'est parce que ça 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 de code initiale est quand une méthode n'est pas
abstract
nifinal
. Parce qu'il permet de modifier (en redéfinissant) le comportement d'une entité construite (ce comportement peut être perdu ou altéré). Avecabstract
etfinal
l'intention est sans ambiguïté: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.
la source
Object.toString()
à Java ... Si c'était le casabstract
, de nombreuses choses par défaut ne fonctionneraient pas, et le code ne serait même pas compilé lorsque quelqu'un n'a pas outrepassé latoString()
méthode! Si c'était le casfinal
, les choses seraient encore pires - aucune possibilité d'autoriser la conversion d'objets en chaîne, à l'exception de la méthode Java. Comme le dit la réponse de @Peter Walser , les implémentations par défaut (telles queObject.toString()
) sont importantes. Surtout dans les méthodes par défaut de Java 8.toString()
c'était le casabstract
oufinal
ce serait une douleur. Mon opinion personnelle est que cette méthode est cassée et il aurait été préférable de ne pas l'avoir du tout (comme equals et hashCode ). Le but initial des valeurs par défaut Java est de permettre l'évolution de l'API avec la rétrocompatibilité.