J'essaie de comprendre les principes SOLID de OOP et je suis arrivé à la conclusion que LSP et OCP ont des similitudes (sinon pour en dire plus)) .
le principe ouvert/fermé stipule que "les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes pour extension, mais fermées pour modification".
LSP en termes simples indique que toute instance de Foo
peut être remplacée par n'importe quelle instance de Bar
qui est dérivée de Foo
et le programme fonctionnera de la même manière.
Je ne suis pas un pro OOP programmeur, mais il me semble que LSP n'est possible que si Bar
, dérivé de Foo
n'y change rien mais cela ne fait que l'étendre. Cela signifie que dans un programme particulier, LSP n'est vrai que lorsque OCP est vrai et OCP n'est vrai que si LSP est vrai. Cela signifie qu'ils sont égaux.
Corrige moi si je me trompe. Je veux vraiment comprendre ces idées. Un grand merci pour une réponse.
Mon Dieu, il y a des idées fausses étranges sur ce que OCP et LSP et certains sont dus à l'inadéquation de certaines terminologies et des exemples confus. Les deux principes ne sont "la même chose" que si vous les appliquez 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 loin, mais commençons par plonger dans les principes eux-mêmes:
Selon oncle Bob :
Vous devriez pouvoir étendre un comportement de classe sans le modifier.
Notez que le mot extend dans ce cas ne signifie pas nécessairement que vous devez sous-classer la classe réelle qui a besoin du nouveau comportement. Voir comment j'ai mentionné au premier décalage de terminologie? Le mot clé extend
signifie uniquement le sous-classement 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 la mise en œuvre du Strategy-pattern (car il est le plus facile à montrer du groupe GoF-patterns IMHO):
// 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 voudraient probablement sous-classer la classe afin de l'étendre, mais ici nous ne le faisons pas car cela suppose que son comportement peut être changé via tout ce qui implémente l'interface IBehavior
.
C'est à dire. la classe de contexte est fermée pour modification mais ouverte pour extension. Il suit en fait un autre principe de base car nous mettons le comportement avec la composition d'objet au lieu de l'héritage:
"Favorise ' composition d'objet ' par rapport à ' héritage de classe '." (Gang of Four 1995: 20)
Je vais laisser le lecteur lire ce principe car il sort du cadre de cette question. Pour continuer avec l'exemple, disons que nous avons 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 lors de l'exécution, via la méthode setBehavior
comme 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!"
Donc, chaque fois que vous voulez étendre la classe de contexte "fermée", 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 c'est OCP. LSP ne fait aucune mention à ce sujet non plus.
Il existe d'autres façons de faire l'OCP que le sous-classement. Une façon est de garder vos classes ouvertes pour l'extension en utilisant mixins. Ceci est utile, par exemple dans des langages basés sur des prototypes plutôt que sur des classes. L'idée est de modifier un objet dynamique avec plus de méthodes ou d'attributs selon les besoins, c'est-à-dire des objets qui se mélangent ou se "mélangent" avec d'autres objets.
Voici un exemple javascript d'un mixin qui rend un simple modèle HTML 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 dynamiquement et l'avantage de cela est que les objets peuvent partager des méthodes même s'ils sont 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 le LinkMixin
.
En termes d'OCP, les "mixins" sont des extensions. Dans l'exemple ci-dessus, le YoutubeLink
est notre entité logicielle qui 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 vérification des types est généralement une mauvaise idée et rompt l'idée avec le polymorphisme.
Notez qu'il est possible de faire plusieurs héritages avec cette méthode car la plupart des implémentations extend
peuvent mélanger plusieurs objets:
_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);
La seule chose que vous devez garder à l'esprit est de ne pas heurter les noms, c'est-à-dire que les mixins définissent le même nom de certains attributs ou méthodes car ils seront remplacés. Dans mon humble expérience, ce n'est pas un problème et si cela se produit, c'est une indication de conception défectueuse.
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 l'oncle Bob ne différencie pas les principes car cela rend LSP toujours étroitement lié à OCP par le fait que, dans l'exemple de stratégie ci-dessus, le même supertype est utilisé (IBehavior
). Regardons donc sa définition originale par Barbara Liskov et voyons si nous pouvons trouver autre chose sur ce principe qui ressemble à un théorème mathématique:
Ce que nous voulons ici est quelque chose comme la propriété de substitution suivante: Si pour chaque objet
o1
de typeS
il y a un objeto2
de typeT
de sorte que pour tous les programmesP
définis en termes deT
, le comportement deP
reste inchangé lorsqueo1
remplaceo2
puisS
est un sous-type deT
.
Permet de hausser les épaules pendant un certain temps, notez 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 une classe. Si votre programme a une liste d'au moins quelques objets JavaScript qui:
... alors les objets sont considérés comme ayant le même "type" et cela n'a pas vraiment d'importance pour le programme. Il s'agit essentiellement 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 que la plupart des programmeurs novices font:
Chaque fois que vous ressentez le besoin de vérifier le sous-type d'un objet, vous le faites probablement mal.
D'accord, ce n'est peut-être pas toujours faux, mais si vous avez envie de faire quelques vérification de type avec instanceof
ou des énumérations, vous pourriez faire le programme un peu plus compliqué pour vous-même qu'il ne devrait l'être. Mais ce n'est pas toujours le cas; des hacks rapides et sales pour faire fonctionner les choses est une concession acceptable à faire dans mon esprit si la solution est assez petite, et si vous pratiquez refactoring impitoyable , cela peut s'améliorer une fois que les changements l'exigent.
Il existe des moyens de contourner cette "erreur de conception", en fonction du problème réel:
Ces deux erreurs sont des "erreurs" de conception de code courantes. Il existe plusieurs refactorisations différentes, telles que méthode de pull-up , ou refactoriser un modèle tel que modèle de visiteur .
En fait, j'aime beaucoup le modèle Visitor car il peut prendre en charge les grands spaghettis d'instructions if et il est plus simple à mettre en œuvre que ce que vous pensez 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 l'instruction if peuvent être traduits dans 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 s'il est d'un certain type. Étant donné que les classes Visitor ont une méthode booléenne canDo
, 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 deux des choses différentes à leur sujet. Alors, à quoi ressemble le code s'il viole l'un des principes?
Il existe des moyens de briser l'un des principes, mais il faut toujours suivre l'autre. Les exemples ci-dessous semblent artificiels, pour une bonne raison, mais j'ai en fait vu ces derniers apparaître dans le code de production (et même pire):
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 méthode GetPersons
du contexte, nous aurons un tas de personnes avec toutes leurs propres implémentations. Cela signifie que IPerson est fermé pour modification, mais ouvert pour extension. Cependant, les choses tournent au noir 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 une vérification de type et une conversion de type! Rappelez-vous comment j'ai mentionné ci-dessus à quel point la vérification de type est une mauvaise chose? Oh non! Mais n'ayez crainte, comme mentionné ci-dessus, effectuez une refonte du pull-up ou implémentez un modèle de visiteur. Dans ce cas, nous pouvons simplement faire un refactoring pull up après avoir ajouté 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 est maintenant que vous n'avez plus besoin de connaître le type exact, suivant 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();
}
Regardons un code qui suit LSP mais pas OCP, il est en quelque sorte artificiel mais avec moi, 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 fait LSP parce que le contexte peut utiliser LiskovBase sans connaître le type réel. Vous penseriez que ce code suit également OCP mais regardez attentivement, la classe est-elle vraiment fermée? Et si la méthode doStuff
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 ' re nécessaire pour remplacer le code complètement avec autre chose. Cela ouvre la boîte de copier-coller des vers car vous devez copier le code de la classe de base pour que les choses fonctionnent. La méthode doStuff
est certes ouverte pour l'extension, mais elle n'a pas été complètement fermée pour modification.
Nous pouvons appliquer le modèle de méthode de modèle à ce sujet. Le modèle de méthode de modèle est si courant dans les frameworks que vous l'avez peut-être utilisé sans le savoir (par exemple Java composants swing, formulaires et composants c #, etc.). Voici une façon de fermer la méthode doStuff
pour la modification et en vous assurant qu'elle reste fermée en la marquant avec le mot clé final
de Java. Ce mot clé empêche quiconque de sous-classer la classe davantage (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 cela à plus grande échelle avec plus de code à gérer. Je continue de voir du code déployé en production où les sous-classes remplacent complètement tout et le code surchargé est principalement coupé-collé entre les implémentations. Cela fonctionne, mais comme pour toute duplication de code, il s'agit également d'une configuration pour les cauchemars de maintenance.
J'espère que tout cela clarifie certaines questions concernant OCP et LSP et les différences/similitudes entre eux. Il est facile de les rejeter comme les mêmes, mais les exemples ci-dessus devraient montrer qu'ils ne le sont pas.
Notez que, en collectant à partir de l'exemple de code ci-dessus:
OCP consiste à verrouiller le code de travail, mais à le garder ouvert d'une manière ou d'une autre avec une sorte de points d'extension.
Cela permet d'éviter la duplication de code en encapsulant le code qui change comme avec l'exemple de modèle de méthode de modèle. Il permet également d'échouer rapidement car les changements de rupture sont douloureux (c'est-à-dire changer un endroit, le casser partout ailleurs). Pour des raisons de maintenance, le concept d'encapsulation du changement est une bonne chose, car les changements toujours se produisent.
LSP consiste à laisser l'utilisateur gérer différents objets qui implémentent un supertype sans vérifier de quel type il s'agit. C'est intrinsèquement de quoi parle polymorphisme.
Ce principe offre une alternative à la vérification de type et à la conversion de type, qui peut devenir incontrôlable au fur et à mesure que le nombre de types augmente, et peut être atteint par une refactorisation pull-up ou l'application de modèles tels que Visitor.
C'est quelque chose qui crée beaucoup de confusion. Je préfère considérer ces principes un peu philosophiquement, car il existe de nombreux exemples différents pour eux, et parfois des exemples concrets ne saisissent pas vraiment toute leur essence.
Disons que nous devons ajouter des fonctionnalités à un programme donné. La manière la plus simple de procéder, en particulier pour les personnes formées à la réflexion procédurale, consiste à ajouter une clause if chaque fois que cela est nécessaire, ou quelque chose de similaire.
Les problèmes avec cela sont
Vous pouvez le faire en ajoutant un champ supplémentaire à tous les livres nommés "is_on_sale", puis vous pouvez vérifier ce champ lors de l'impression du prix d'un livre, ou alternativement , vous pouvez instancier des livres en vente à partir de la base de données en utilisant un type différent, qui imprime "(ON SALE)" dans la chaîne de prix (ce n'est pas un design parfait, mais il fournit le point de départ).
Le problème avec la première solution procédurale est un champ supplémentaire pour chaque livre et une complexité redondante supplémentaire dans de nombreux cas. La deuxième solution force uniquement la logique là où elle est réellement requise.
Considérez maintenant le fait qu'il pourrait y avoir de nombreux cas où des données et une logique différentes sont requises, et vous verrez pourquoi garder OCP à l'esprit lors de la conception de vos classes ou réagir aux changements des exigences est une bonne idée.
Vous devriez maintenant avoir l'idée principale: essayez de vous mettre dans une situation où le nouveau code peut être implémenté sous forme d'extensions polymorphes, et non de modifications procédurales.
Mais n'ayez pas peur d'analyser le contexte et de voir si les inconvénients l'emportent sur les avantages, car même un principe tel que l'OCP peut faire un gâchis de 20 classes à partir d'un programme de 20 lignes, s'il n'est pas traité avec soin.
Nous aimons tous la réutilisation du code. Une maladie qui suit est que de nombreux programmes ne le comprennent pas complètement, au point où ils factorisent aveuglément des lignes de code communes uniquement pour créer des complexités illisibles et un couplage serré redondant entre les modules qui, à part quelques lignes de code, n'ont rien de commun en ce qui concerne le travail conceptuel à faire.
Le plus grand exemple de ceci est réutilisation de l'interface. Vous en avez probablement été témoin vous-même; une classe implémente une interface, non pas parce que c'est une implémentation logique de celle-ci (ou une extension dans le cas de classes de base concrètes), mais parce que les méthodes qu'elle déclare à ce moment-là ont les bonnes signatures en ce qui la concerne.
Mais alors vous rencontrez un problème. Si les classes implémentent des interfaces uniquement en considérant les signatures des méthodes qu'elles déclarent, vous vous retrouvez en mesure de passer des instances de classes d'une fonctionnalité conceptuelle à des endroits qui nécessitent des fonctionnalités complètement différentes, qui ne dépendent que de signatures similaires.
Ce n'est pas si horrible, mais cela crée beaucoup de confusion, et nous avons la technologie pour nous empêcher de faire des erreurs comme celles-ci. Ce que nous devons faire est de traiter les interfaces comme API + Protocol. L'API est apparente dans les déclarations et le protocole est apparent dans les utilisations existantes de l'interface. Si nous avons 2 protocoles conceptuels qui partagent la même API, ils doivent être représentés comme 2 interfaces différentes. Sinon, nous sommes pris dans le DRY dogmatisme et, ironiquement, nous ne faisons que créer du code plus difficile à maintenir.
Vous devriez maintenant être en mesure de comprendre parfaitement la définition. LSP dit: N'héritez pas d'une classe de base et n'implémentez pas de fonctionnalités dans les sous-classes qui, à d'autres endroits, qui dépendent de la classe de base, ne s'entendront pas.
De ma compréhension:
OCP dit: "Si vous ajoutez une nouvelle fonction, créez une nouvelle classe étendant une classe existante, plutôt que de la changer."
LSP dit: "Si vous créez une nouvelle classe étendant une classe existante, assurez-vous qu'elle est complètement interchangeable avec sa base."
Je pense donc qu'ils se complètent, mais ils ne sont pas égaux.
S'il est vrai qu'OCP et LSP ont tous deux à voir avec la modification, le type de modification dont parle OCP n'est pas celui dont parle LSP.
La modification par rapport à l'OCP est l'action physique d'un développeur écriture de code dans une classe existante.
LSP traite de la modification de comportement apportée par une classe dérivée par rapport à sa classe de base et de la modification runtime de l'exécution du programme qui peut être provoquée par l'utilisation de la sous-classe au lieu de la super-classe.
Ainsi, bien qu'ils puissent ressembler à distance OCP! = LSP. En fait, je pense qu'ils peuvent être les 2 seuls principes SOLID qui ne peuvent pas être compris les uns par rapport aux autres.
LSP en termes simples indique que toute instance de Foo peut être remplacée par n'importe quelle instance de Bar dérivée de Foo sans aucune perte de fonctionnalité du programme.
C'est faux. LSP indique que la classe Bar ne doit pas introduire de comportement, ce qui n'est pas prévu lorsque le code utilise Foo, lorsque Bar est dérivé de Foo. Cela n'a rien à voir avec la perte de fonctionnalité. Vous pouvez supprimer des fonctionnalités, mais uniquement lorsque le code utilisant Foo ne dépend pas de cette fonctionnalité.
Mais au final, cela est généralement difficile à réaliser, car la plupart du temps, le code utilisant Foo dépend de tout son comportement. Le supprimer viole donc LSP. Mais la simplifier comme ça n'est qu'une partie du LSP.
LSP et OCP ne sont pas identiques.
LSP parle de l'exactitude du programme tel quel. Si une instance d'un sous-type rompt l'exactitude du programme lorsqu'elle est substituée dans le code pour les types d'ancêtre, alors vous avez démontré une violation de LSP. Vous devrez peut-être simuler un test pour le montrer, mais vous n'aurez pas à modifier la base de code sous-jacente. Vous validez le programme lui-même pour voir s'il répond au LSP.
OCP parle de l'exactitude de changements dans le code du programme, le delta d'une version source à une autre. Le comportement ne doit pas être modifié. Il devrait seulement être prolongé. L'exemple classique est l'ajout de champs. Tous les champs existants continuent de fonctionner comme auparavant. Le nouveau champ ajoute simplement des fonctionnalités. Cependant, la suppression d'un champ est généralement une violation d'OCP. Ici, vous validez le delta de version du programme pour voir s'il répond à OCP.
Voilà donc la principale différence entre LSP et OCP. Le premier ne valide que le base de code en l'état, le dernier ne valide que delta de base de code d'une version à la suivante. En tant que tels, ils ne peuvent pas être la même chose, ils sont définis comme validant des choses différentes.
Je vais vous donner une preuve plus formelle: Dire "LSP implique OCP" impliquerait un delta (car OCP en requiert un autre que dans le cas trivial), mais LSP n'en a pas besoin. C'est donc clairement faux. Inversement, nous pouvons réfuter "OCP implique LSP" simplement en disant que OCP est une déclaration sur les deltas, donc il ne dit rien sur une déclaration sur un programme en place. Cela découle du fait que vous pouvez créer TOUT delta en commençant par TOUT programme en place. Ils sont totalement indépendants.
Pour comprendre la différence, vous devez comprendre les sujets des deux principes. Ce n'est pas une partie abstraite du code ou de la situation qui peut violer ou non un principe. Il s'agit toujours d'un composant spécifique - fonction, classe ou module - qui peut violer OCP ou LSP.
On peut vérifier si LSP est cassé uniquement lorsqu'il existe une interface avec un certain contrat et une implémentation de cette interface. Si l'implémentation n'est pas conforme à l'interface ou, d'une manière générale, au contrat, le LSP est rompu.
Exemple le plus simple:
class Container {
// Should add the object to the container.
void addObject(object) {
internalArray.append(object);
}
int size() {
return internalArray.size();
}
}
class CustomContainer extends Container {
@Override void addObject(object) {
System.console.print("Skipping object! Ha-ha!");
}
}
void fillWithRandomNumbers(Container container) {
while (container.size() < 42) {
container.addObject(Randomizer.getNumber())
}
}
Le contrat stipule clairement que addObject
doit ajouter son argument au conteneur. Et CustomContainer
rompt clairement ce contrat. Ainsi, la fonction CustomContainer.addObject
Viole LSP. Ainsi, la classe CustomContainer
viole LSP. La conséquence la plus importante est que CustomContainer
ne peut pas être passé à fillWithRandomNumbers()
. Container
ne peut pas être remplacé par CustomContainer
.
Gardez à l'esprit un point très important. Ce n'est pas tout ce code qui casse LSP, c'est spécifiquement CustomContainer.addObject
Et généralement CustomContainer
qui casse LSP. Lorsque vous déclarez que le LSP est violé, vous devez toujours spécifier deux choses:
C'est ça. Juste un contrat et sa mise en œuvre. Un abaissé dans le code ne dit rien sur la violation du LSP.
On peut vérifier si l'OCP est violé uniquement lorsqu'il existe un ensemble de données limité et un composant qui gère les valeurs de cet ensemble de données. Si les limites de l'ensemble de données peuvent changer avec le temps et que cela nécessite de changer le code source du composant, alors le composant viole OCP.
Cela semble complexe. Essayons un exemple simple:
enum Platform {
iOS,
Android
}
class PlatformDescriber {
String describe(Platform platform) {
switch (platform) {
case iOS: return "iPhone OS, v10.0.1";
case Android: return "Android, v7.1";
}
}
}
L'ensemble de données est l'ensemble des plates-formes prises en charge. PlatformDescriber
est le composant qui gère les valeurs de cet ensemble de données. L'ajout d'une nouvelle plateforme nécessite la mise à jour du code source de PlatformDescriber
. Ainsi, la classe PlatformDescriber
viole OCP.
Un autre exemple:
class Shop {
void sellItemToCustomer(item, customer) {
// some buisiness logic here
...
logger.logItemSold()
}
}
class Logger {
void logItemSold() {
logger.logToStdErr("an item was sold")
logger.logToRemote("an item was sold")
logger.logToDatabase("an item was sold")
}
}
Le "jeu de données" est l'ensemble des canaux où une entrée de journal doit être ajoutée. Logger
est le composant chargé d'ajouter des entrées à tous les canaux. L'ajout de la prise en charge d'une autre méthode de journalisation nécessite la mise à jour du code source de Logger
. Ainsi, la classe Logger
viole OCP.
Notez que dans les deux exemples, l'ensemble de données n'est pas quelque chose de sémantiquement fixe. Cela peut changer avec le temps. Une nouvelle plateforme pourrait voir le jour. Un nouveau canal de journalisation pourrait émerger. Si votre composant doit être mis à jour lorsque cela se produit, il viole l'OCP.
Maintenant, la partie délicate. Comparez les exemples ci-dessus aux suivants:
enum GregorianWeekDay {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
String translateToRussian(GregorianWeekDay weekDay) {
switch (weekDay) {
case Monday: return "Понедельник";
case Tuesday: return "Вторник";
case Wednesday: return "Среда";
case Thursday: return "Четверг";
case Friday: return "Пятница";
case Saturday: return "Суббота";
case Sunday: return "Воскресенье";
}
}
Vous pensez peut-être que translateToRussian
viole OCP. Mais ce n'est pas le cas. GregorianWeekDay
a une limite spécifique d'exactement 7 jours de semaine avec des noms exacts. Et l'important est que ces limites ne peuvent pas sémantiquement changer avec le temps. Il y aura toujours 7 jours dans la semaine grégorienne. Il y aura toujours le lundi, le mardi, etc. Cet ensemble de données est sémantiquement fixe. Il n'est pas possible que le code source de translateToRussian
nécessite des modifications. Ainsi OCP n'est pas violé.
Maintenant, il devrait être clair que l'épuisement de l'instruction switch
n'est pas toujours une indication d'OCP cassé.
Sentez maintenant la différence:
Ces conditions sont complètement orthogonales.
Dans @ Spoike's answer the Violer un principe mais suivre l'autre est totalement faux.
Dans le premier exemple, la partie boucle for
- viole clairement OCP car elle n'est pas extensible sans modification. Mais il n'y a aucune indication de violation du LSP. Et il n'est même pas clair si le contrat Context
permet à getPersons de renvoyer n'importe quoi sauf Boss
ou Peon
. Même en supposant un contrat qui permet de renvoyer n'importe quelle sous-classe IPerson
, aucune classe ne remplace cette condition préalable et ne la viole. De plus, si getPersons retournera une instance d'une troisième classe, la boucle for
- fera son travail sans aucune défaillance. Mais ce fait n'a rien à voir avec le LSP.
Prochain. Dans le deuxième exemple, ni LSP ni OCP ne sont violés. Encore une fois, la partie Context
n'a tout simplement rien à voir avec le LSP - pas de contrat défini, pas de sous-classement, pas de dépassements de rupture. Ce n'est pas Context
qui doit obéir à LSP, c'est LiskovSub
ne doit pas rompre le contrat de sa base. Concernant OCP, la classe est-elle vraiment fermée? - oui, elle l'est. Aucune modification n'est nécessaire pour l'étendre. Évidemment, le nom du point d'extension indique Faites tout ce que vous voulez, pas de limites . L'exemple n'est pas très utile dans la vraie vie, mais il ne viole clairement pas l'OCP.
Essayons de faire quelques exemples corrects avec une véritable violation d'OCP ou de LSP.
interface Platform {
String name();
String version();
}
class iOS implements Platform {
@Override String name() { return "iOS"; }
@Override String version() { return "10.0.1"; }
}
interface PlatformSerializer {
String toJson(Platform platform);
}
class HumanReadablePlatformSerializer implements PlatformSerializer {
String toJson(Platform platform) {
return platform.name() + ", v" + platform.version();
}
}
Ici, HumanReadablePlatformSerializer
ne nécessite aucune modification lors de l'ajout d'une nouvelle plateforme. Il suit ainsi l'OCP.
Mais le contrat requiert que toJson
retourne un JSON correctement formaté. La classe ne fait pas ça. De ce fait, il ne peut pas être transmis à un composant qui utilise PlatformSerializer
pour formater le corps d'une demande réseau. Ainsi, HumanReadablePlatformSerializer
viole LSP.
Quelques modifications à l'exemple précédent:
class Android implements Platform {
@Override String name() { return "Android"; }
@Override String version() { return "7.1"; }
}
class HumanReadablePlatformSerializer implements PlatformSerializer {
String toJson(Platform platform) {
return "{ "
+ "\"name\": \"" + platform.name() + "\","
+ "\"version\": \"" + platform.version() + "\","
+ "\"most-popular\": " + isMostPopular(platform) + ","
+ "}"
}
boolean isMostPopular(Platform platform) {
return (platform instanceof Android)
}
}
Le sérialiseur renvoie une chaîne JSON correctement formatée. Donc, aucune violation LSP ici.
Mais il est nécessaire que si la plate-forme est la plus largement utilisée, il doit y avoir une indication correspondante dans JSON. Dans cet exemple, OCP est violé par la fonction HumanReadablePlatformSerializer.isMostPopular
Car un jour iOS deviendra la plate-forme la plus populaire. Formellement, cela signifie que l'ensemble des plates-formes les plus utilisées est défini comme "Android" pour l'instant, et isMostPopular
ne gère pas correctement cet ensemble de données. L'ensemble de données n'est pas sémantiquement fixe et peut changer librement au fil du temps. Le code source de HumanReadablePlatformSerializer
doit être mis à jour en cas de changement.
Vous pouvez également remarquer une violation de la responsabilité unique dans cet exemple. Je l'ai fait intentionnellement pour pouvoir démontrer les deux principes sur la même entité sujet. Pour corriger SRP, vous pouvez extraire la fonction isMostPopular
dans un Helper
externe et ajouter un paramètre à PlatformSerializer.toJson
. Mais c'est une autre histoire.