Mesdames, il y a des idées fausses étranges sur ce que OCP et LSP et certaines sont dues à l'inadéquation de certaines terminologies et à des exemples déroutants. Les deux principes ne sont que "la même chose" si vous les implémentez de la même manière. Les modèles suivent généralement les principes d’une manière ou d’une autre, à quelques exceptions près.
Les différences seront expliquées plus bas, mais commençons par plonger dans les principes eux-mêmes:
Principe ouvert-fermé (OCP)
Selon l' oncle Bob :
Vous devriez pouvoir étendre un comportement de classes sans le modifier.
Notez que le mot expand dans ce cas ne signifie pas nécessairement que vous devez sous-classer la classe réelle qui nécessite le nouveau comportement. Voyez comment j'ai mentionné à la première discordance de la terminologie? Le mot clé extend
ne signifie que des sous-classes en Java, mais les principes sont plus anciens que Java.
L'original est venu de Bertrand Meyer en 1988:
Les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes pour extension, mais fermées pour modification.
Ici, il est beaucoup plus clair que le principe est appliqué aux entités logicielles . Un mauvais exemple serait de remplacer l'entité logicielle lorsque vous modifiez complètement le code au lieu de fournir un point d'extension. Le comportement de l'entité logicielle elle-même devrait être extensible et un bon exemple de cela est l'implémentation du modèle-stratégie (car c'est le plus facile à montrer du groupe de modèles GoF à mon humble avis):
// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {
// Context is however open for extension through
// this private field
private IBehavior behavior;
// The context calls the behavior in this public
// method. If you want to change this you need
// to implement it in the IBehavior object
public void doStuff() {
if (this.behavior != null)
this.behavior.doStuff();
}
// You can dynamically set a new behavior at will
public void setBehavior(IBehavior behavior) {
this.behavior = behavior;
}
}
// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
public void doStuff();
}
Dans l'exemple ci-dessus, le Context
est verrouillé pour d'autres modifications. La plupart des programmeurs voudront probablement sous-classer la classe afin de l'étendre, mais ce n'est pas le cas ici, car il suppose que son comportement peut être modifié par tout ce qui implémente l' IBehavior
interface.
C'est-à-dire que la classe de contexte est fermée pour modification mais ouverte pour extension . En réalité, il suit un autre principe de base, car nous mettons le comportement avec la composition d'objet au lieu d'un héritage:
"Privilégiez la composition d'objet plutôt que l' héritage de classe ." (Gang of Four 1995: 20)
Je laisserai le lecteur prendre connaissance de ce principe car il sort du cadre de cette question. Pour continuer avec l'exemple, supposons que nous ayons les implémentations suivantes de l'interface IBehavior:
public class HelloWorldBehavior implements IBehavior {
public void doStuff() {
System.println("Hello world!");
}
}
public class GoodByeBehavior implements IBehavior {
public void doStuff() {
System.out.println("Good bye cruel world!");
}
}
En utilisant ce modèle, nous pouvons modifier le comportement du contexte au moment de l'exécution, via la setBehavior
méthode en tant que point d'extension.
// in your main method
Context c = new Context();
c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"
c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"
Ainsi, chaque fois que vous souhaitez étendre la classe de contexte "fermé", faites-le en sous-classant sa dépendance de collaboration "ouverte". Ce n'est clairement pas la même chose que de sous-classer le contexte lui-même mais il s'agit d'OCP. LSP n'en fait aucune mention.
Extension avec Mixins au lieu de l'héritage
Il y a d'autres façons de faire OCP que le sous-classement. Une solution consiste à garder vos classes ouvertes à l'extension grâce à l'utilisation de mixins . Cela est utile, par exemple, dans les langages basés sur des prototypes plutôt que sur des classes. L'idée est d'amender un objet dynamique avec plus de méthodes ou d'attributs en fonction des besoins, autrement dit des objets qui se fondent ou se "mélangent" avec d'autres objets.
Voici un exemple javascript de mixin qui rend un modèle HTML simple pour les ancres:
// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
render: function() {
return '<a href="' + this.link +'">'
+ this.content
+ '</a>;
}
}
// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
this.content = content;
this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
setLink: function(youtubeid) {
this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
}
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);
// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");
console.log(ytLink.render());
// will output:
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>
L'idée est d'étendre les objets de manière dynamique et l'avantage est que les objets peuvent partager des méthodes même s'ils se trouvent dans des domaines complètement différents. Dans le cas ci-dessus, vous pouvez facilement créer d’autres types d’ancres HTML en étendant votre implémentation spécifique avec LinkMixin
.
En termes d'OCP, les "mixins" sont des extensions. Dans l'exemple ci-dessus, YoutubeLink
notre entité logicielle est fermée pour modification, mais ouverte pour les extensions via l'utilisation de mixins. La hiérarchie des objets est aplatie, ce qui rend impossible la vérification des types. Cependant, ce n'est pas vraiment une mauvaise chose, et j'expliquerai plus loin que la recherche de types est généralement une mauvaise idée et qu'elle rompt avec le polymorphisme.
Notez qu'il est possible d'effectuer plusieurs héritages avec cette méthode car la plupart des extend
implémentations peuvent mélanger plusieurs objets:
_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);
La seule chose que vous devez garder à l’esprit est de ne pas entrer en collision les noms, c’est-à-dire que les mixins définissent le même nom pour certains attributs ou méthodes lorsqu’ils seront remplacés. Dans mon humble expérience, ceci n’est pas un problème et si cela se produit, c’est une indication d’une conception défectueuse.
Principe de substitution de Liskov (LSP)
Oncle Bob le définit simplement par:
Les classes dérivées doivent être substituables à leurs classes de base.
Ce principe est ancien. En fait, la définition de Oncle Bob ne différencie pas les principes, car le LSP est toujours étroitement lié à OCP par le fait que, dans l'exemple de stratégie ci-dessus, le même supertype est utilisé ( IBehavior
). Voyons donc sa définition originale de Barbara Liskov et voyons si nous pouvons trouver autre chose à propos de ce principe qui ressemble à un théorème mathématique:
Ce qui est recherché ici ressemble à la propriété de substitution suivante: Si, pour chaque objet o1
de type, S
il existe un objet o2
de type T
tel que, pour tous les programmes P
définis en termes de T
, le comportement de P
est inchangé, quand o1
est remplacé par o2
alors S
un sous-type de T
.
Permet de hausser les épaules pendant un moment, remarquez qu'il ne mentionne pas du tout les cours. En JavaScript, vous pouvez réellement suivre LSP même s'il n'est pas explicitement basé sur les classes. Si votre programme contient une liste d'au moins deux objets JavaScript qui:
- doit être calculé de la même manière,
- avoir le même comportement, et
- sont autrement d'une manière complètement différente
... alors les objets sont considérés comme ayant le même "type" et cela n'a pas vraiment d'importance pour le programme. C'est essentiellement du polymorphisme . Au sens générique; vous ne devriez pas avoir besoin de connaître le sous-type réel si vous utilisez son interface. OCP ne dit rien d’explicite à ce sujet. Il identifie également une erreur de conception commise par la plupart des programmeurs débutants:
Chaque fois que vous ressentez le besoin de vérifier le sous-type d'un objet, vous le faites probablement FAUX.
Bien, cela n’est peut-être pas toujours faux, mais si vous avez l’envie de faire une vérification de type avec instanceof
ou enums, vous ferez peut-être le programme un peu plus compliqué que nécessaire. Mais ce n'est pas toujours le cas. Des astuces rapides et sales pour faire fonctionner les choses sont une concession acceptable à faire dans mon esprit si la solution est suffisamment petite et si vous pratiquez une refactorisation sans merci , elle peut être améliorée une fois que les changements l'exigent.
Selon le problème, il existe des moyens de contourner cette "erreur de conception":
- La super classe n'appelle pas les conditions préalables, ce qui oblige l'appelant à le faire.
- Il manque à la super classe une méthode générique dont l'appelant a besoin.
Ces deux erreurs sont des "erreurs" de conception de code courantes. Vous pouvez effectuer plusieurs refactorisations différentes, telles que la méthode d'extraction ou la refactorisation d'un modèle tel que le modèle Visiteur .
En fait, j'aime beaucoup le modèle de visiteur car il peut traiter de gros spaghettis avec une instruction if et il est plus simple à mettre en œuvre que ce que l'on pourrait penser du code existant. Disons que nous avons le contexte suivant:
public class Context {
public void doStuff(string query) {
// outcome no. 1
if (query.Equals("Hello")) {
System.out.println("Hello world!");
}
// outcome no. 2
else if (query.Equals("Bye")) {
System.out.println("Good bye cruel world!");
}
// a change request may require another outcome...
}
}
// usage:
Context c = new Context();
c.doStuff("Hello");
// prints "Hello world"
c.doStuff("Bye");
// prints "Bye"
Les résultats de la déclaration if peuvent être traduits en leurs propres visiteurs car chacun dépend d'une décision et d'un code à exécuter. Nous pouvons les extraire comme ceci:
public interface IVisitor {
public bool canDo(string query);
public void doStuff();
}
// outcome 1
public class HelloVisitor implements IVisitor {
public bool canDo(string query) {
return query.Equals("Hello");
}
public void doStuff() {
System.out.println("Hello World");
}
}
// outcome 2
public class ByeVisitor implements IVisitor {
public bool canDo(string query) {
return query.Equals("Bye");
}
public void doStuff() {
System.out.println("Good bye cruel world");
}
}
À ce stade, si le programmeur ne connaissait pas le modèle Visitor, il implémenterait plutôt la classe Context pour vérifier si elle est d'un certain type. Etant donné que les classes Visitor ont une canDo
méthode booléenne , l'implémenteur peut utiliser cet appel de méthode pour déterminer s'il s'agit du bon objet pour effectuer le travail. La classe de contexte peut utiliser tous les visiteurs (et en ajouter de nouveaux) comme ceci:
public class Context {
private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();
public Context() {
visitors.add(new HelloVisitor());
visitors.add(new ByeVisitor());
}
// instead of if-statements, go through all visitors
// and use the canDo method to determine if the
// visitor object is the right one to "visit"
public void doStuff(string query) {
for(IVisitor visitor : visitors) {
if (visitor.canDo(query)) {
visitor.doStuff();
break;
// or return... it depends if you have logic
// after this foreach loop
}
}
}
// dynamically adds new visitors
public void addVisitor(IVisitor visitor) {
if (visitor != null)
visitors.add(visitor);
}
}
Les deux modèles suivent OCP et LSP, mais ils indiquent tous les deux des choses différentes. Alors, à quoi ressemble le code s'il viole l'un des principes?
Violer un principe mais suivre l'autre
Il existe des moyens de casser l'un des principes tout en laissant suivre l'autre. Les exemples ci-dessous semblent artificiels, pour une bonne raison, mais je les ai effectivement vus apparaître dans le code de production (et même pire):
Suit OCP mais pas LSP
Disons que nous avons le code donné:
public interface IPerson {}
public class Boss implements IPerson {
public void doBossStuff() { ... }
}
public class Peon implements IPerson {
public void doPeonStuff() { ... }
}
public class Context {
public Collection<IPerson> getPersons() { ... }
}
Ce morceau de code suit le principe ouvert-fermé. Si nous appelons la GetPersons
méthode du contexte , nous aurons un groupe de personnes ayant toutes leurs propres implémentations. Cela signifie qu'IPerson est fermé pour modification, mais ouvert pour extension. Cependant, les choses prennent une tournure sombre lorsque nous devons l’utiliser:
// in some routine that needs to do stuff with
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
// now we have to check the type... :-P
if (person instanceof Boss) {
((Boss) person).doBossStuff();
}
else if (person instanceof Peon) {
((Peon) person).doPeonStuff();
}
}
Vous devez faire la vérification de type et la conversion de type! Rappelez-vous comment j'ai mentionné ci-dessus comment la vérification de type est une mauvaise chose ? Oh non! Mais ne craignez rien, comme mentionné également ci-dessus, soit procéder à une refactorisation en amont, soit mettre en œuvre un modèle de visiteur. Dans ce cas, nous pouvons simplement faire une refactorisation en ajoutant une méthode générale:
public class Boss implements IPerson {
// we're adding this general method
public void doStuff() {
// that does the call instead
this.doBossStuff();
}
public void doBossStuff() { ... }
}
public interface IPerson {
// pulled up method from Boss
public void doStuff();
}
// do the same for Peon
L'avantage à présent est que vous n'avez plus besoin de connaître le type exact après le LSP:
// in some routine that needs to do stuff with
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
// yay, no type checking!
person.doStuff();
}
Suit le LSP mais pas l'OCP
Regardons un code qui suit LSP mais pas OCP, il est un peu artificiel mais supportez-moi sur celui-ci c'est une erreur très subtile:
public class LiskovBase {
public void doStuff() {
System.out.println("My name is Liskov");
}
}
public class LiskovSub extends LiskovBase {
public void doStuff() {
System.out.println("I'm a sub Liskov!");
}
}
public class Context {
private LiskovBase base;
// the good stuff
public void doLiskovyStuff() {
base.doStuff();
}
public void setBase(LiskovBase base) { this.base = base }
}
Le code utilise LSP car le contexte peut utiliser LiskovBase sans connaître le type réel. Vous penseriez que ce code suit également OCP, mais regardez bien, la classe est-elle vraiment fermée ? Et si la doStuff
méthode faisait plus que simplement imprimer une ligne?
La réponse si elle suit OCP est simplement: NON , ce n'est pas parce que dans cette conception d'objet, nous devons remplacer le code complètement par autre chose. Cela ouvre la boîte de vers et coupe-coller, car vous devez copier le code de la classe de base pour que tout fonctionne. La doStuff
méthode sure est ouverte à l'extension, mais elle n'a pas été complètement fermée pour modification.
Nous pouvons appliquer le modèle de méthode Template à ce sujet. Le modèle de méthode template est si courant dans les frameworks que vous l'avez peut-être utilisé sans le savoir (par exemple, composants java swing, formes et composants c #, etc.). Voici une façon de fermer la doStuff
méthode de modification et de s’assurer qu’elle reste fermée en la marquant avec le final
mot - clé java . Ce mot-clé empêche quiconque de sous-classer davantage la classe (en C #, vous pouvez utiliser sealed
pour faire la même chose).
public class LiskovBase {
// this is now a template method
// the code that was duplicated
public final void doStuff() {
System.out.println(getStuffString());
}
// extension point, the code that "varies"
// in LiskovBase and it's subclasses
// called by the template method above
// we expect it to be virtual and overridden
public string getStuffString() {
return "My name is Liskov";
}
}
public class LiskovSub extends LiskovBase {
// the extension overridden
// the actual code that varied
public string getStuffString() {
return "I'm sub Liskov!";
}
}
Cet exemple suit OCP et semble idiot, ce qui est le cas, mais imaginez-le à plus grande échelle avec plus de code à gérer. Je continue à voir le code déployé en production où les sous-classes écrasent complètement tout et où le code substitué est généralement coupé-collé entre les implémentations. Cela fonctionne, mais comme pour tout code, la duplication est aussi une configuration pour les cauchemars de maintenance.
Conclusion
J'espère que tout cela efface certaines questions concernant OCP et LSP et les différences / similitudes entre eux. Il est facile de les rejeter comme identiques, mais les exemples ci-dessus doivent montrer qu'ils ne le sont pas.
Notez que, recueillant à partir de l'exemple de code ci-dessus:
OCP consiste à verrouiller le code de travail tout en le maintenant ouvert d'une manière ou d'une autre avec une sorte de point d'extension.
Cela permet d'éviter la duplication de code en encapsulant le code qui change comme dans l'exemple de modèle de modèle. Cela permet également d’échouer rapidement car briser les changements est douloureux (c’est-à-dire changer d’un endroit, le casser partout ailleurs). Pour des raisons de maintenance, le concept d'encapsulation du changement est une bonne chose, car les changements se produisent toujours .
LSP consiste à laisser l'utilisateur gérer différents objets qui implémentent un supertype sans vérifier leur type réel. C’est ce qui est fondamentalement le polymorphisme .
Ce principe offre une alternative à la vérification de type et à la conversion de type, qui peut devenir incontrôlable à mesure que le nombre de types augmente, et peut être obtenu par une refactorisation ou une application de modèles tels que Visiteur.