LSP vs OCP / Liskov Substitution VS Open Close

48

J'essaie de comprendre les principes SOLID de la programmation orientée objet et je suis parvenu à la conclusion que LSP et OCP présentaient certaines similitudes (sinon pour en dire plus).

les états de principe ouvert / fermé "les entités logicielles (classes, modules, fonctions, etc.) devraient être ouvertes à extension, mais fermées à modification".

LSP en mots simples stipule que toute instance de Foopeut être remplacée par une instance Bardont le résultat est dérivé Fooet que le programme fonctionnera de la même manière.

Je ne suis pas un programmeur pro de la POO, mais il me semble que le LSP n'est possible que si Bar, dérivé de, Foone change rien, mais le prolonge. Cela signifie que, en particulier, le programme LSP n’est vrai que lorsque OCP est vrai et OCP 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. Grand merci pour une réponse.

Kolyunya
la source
4
C'est une interprétation très étroite des deux concepts. Ouvert / fermé peut être maintenu tout en violant le LSP. Les exemples Rectangle / Carré ou Ellipse / Cercle sont de bonnes illustrations. Les deux adhèrent à OCP, mais violent tous les deux le LSP.
Joel Etherton
1
Le monde (ou du moins Internet) est confus à ce sujet. kirkk.com/modularity/2009/12/solid-principles-of-class-design . Ce gars dit que la violation de LSP est également une violation OCP. Et ensuite, dans le livre "Conception du génie logiciel: théorie et pratique" à la page 156, l'auteur donne un exemple de quelque chose qui adhère à OCP mais viole le LSP. J'ai abandonné cela.
Manoj R
@JoelEtherton Ces paires ne violent le LSP que si elles sont mutables. Dans le cas immuable, dériver Squarede Rectanglene viole pas le LSP. (Mais il est sans doute encore une mauvaise conception dans le cas immuable puisque vous pouvez avoir carrés Rectangles qui ne sont pas un Squarequi ne correspond pas les mathématiques)
CodesInChaos
Analogie simple (du point de vue d'un auteur rédacteur de bibliothèque). LSP est comme vendre un produit (une bibliothèque) qui prétend implémenter 100% de ce qu'il dit (sur l'interface ou le manuel de l'utilisateur), mais ne le fait pas (ou ne correspond pas à ce qui est dit). OCP, c'est comme vendre un produit (une bibliothèque) avec la promesse qu'il peut être mis à niveau (étendu) lorsque de nouvelles fonctionnalités apparaissent (comme un microprogramme), mais ne peut en fait être mis à niveau sans un service fourni en usine.
Rwong

Réponses:

119

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é extendne 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 Contextest 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' IBehaviorinterface.

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 setBehaviormé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, YoutubeLinknotre 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 extendimplé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 o1de type, Sil existe un objet o2de type Ttel que, pour tous les programmes Pdéfinis en termes de T, le comportement de Pest inchangé, quand o1est remplacé par o2alors Sun 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 instanceofou 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 canDomé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 GetPersonsmé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 doStuffmé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 doStuffmé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 doStuffméthode de modification et de s’assurer qu’elle reste fermée en la marquant avec le finalmot - clé java . Ce mot-clé empêche quiconque de sous-classer davantage la classe (en C #, vous pouvez utiliser sealedpour 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.

Spoike
la source
7
C'est une bonne explication, car cela ne simplifie pas excessivement l'OCP en impliquant que cela signifie toujours une implémentation par héritage. C’est cette simplification excessive qui unit OCP et SRP dans l’esprit de certaines personnes, alors qu’il s’agit en réalité de deux concepts complètement distincts.
Eric King
5
C'est l'une des meilleures réponses d'échange de pile que j'ai jamais vues. Je souhaite pouvoir le voter 10 fois. Bravo et merci pour cette excellente explication.
Bob Horn
Là-bas, j’ai ajouté un texte de référence sur Javascript qui n’est pas un langage de programmation basé sur les classes mais qui peut toujours suivre LSP et a modifié le texte afin que celui-ci puisse être lu plus facilement. Phew!
Spoike
Bien que votre citation d'Oncle Bob de LSP soit correcte (identique à celle de son site Web), ne devriez-vous pas l'inverse? Ne devrait-il pas indiquer que "les classes de base devraient être substituables à leurs classes dérivées"? Sur LSP, le test de "compatibilité" est effectué par rapport à la classe dérivée et non à la classe de base. Malgré tout, je ne parle pas anglais et je pense que certains éléments de la phrase pourraient me manquer.
Alpha
@ Alpha: C'est une bonne question. La classe de base est toujours substituable avec ses classes dérivées sinon l'héritage ne fonctionnerait pas. Le compilateur (au moins en Java et en C #) se plaindra si vous omettez un membre (méthode ou attribut / champ) de la classe étendue à implémenter. LSP est conçu pour vous empêcher d'ajouter des méthodes qui ne sont disponibles que localement sur les classes dérivées, car cela oblige l'utilisateur de ces classes dérivées à en prendre connaissance. À mesure que le code se développe, de telles méthodes seraient difficiles à maintenir.
Spoike
15

C'est quelque chose qui cause beaucoup de confusion. Je préfère considérer ces principes de manière quelque peu philosophique, car ils ont de nombreux exemples différents, et parfois des exemples concrets ne rendent pas vraiment compte de leur essence.

Ce que OCP essaie de réparer

Supposons que nous devions ajouter des fonctionnalités à un programme donné. La façon la plus simple de s'y prendre, en particulier pour les personnes formées à la procédure, consiste à ajouter une clause if si nécessaire, ou quelque chose du genre.

Les problèmes avec qui sont

  1. Cela change le flux du code de travail existant.
  2. Il impose un nouveau branchement conditionnel sur chaque cas. Par exemple, supposons que vous ayez une liste de livres et que certains d’entre eux soient en vente. Vous souhaitez les parcourir tous et les imprimer et les imprimer de telle sorte que, s’ils sont en vente, le prix imprimé comprendra la chaîne " (EN SOLDES)".

Vous pouvez le faire en ajoutant un champ supplémentaire à tous les livres nommés « is_on_sale », et vous pouvez vérifier ce champ lors de l' impression du prix de tout livre, ou encore , vous pouvez instancier livres en vente à partir de la base de données en utilisant un autre type, qui imprime "(EN VENTE)" dans la chaîne de prix (pas un design parfait mais il fournit le point à la maison).

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 oblige uniquement la logique là où elle est réellement requise.

Examinons maintenant le fait qu'il peut exister de nombreux cas dans lesquels des données et une logique différentes sont nécessaires et vous comprendrez pourquoi il est judicieux de garder à l'esprit OCP lors de la conception de vos classes ou de réagir aux modifications des exigences.

Vous devriez maintenant avoir l’idée principale: essayez de vous mettre dans une situation où un nouveau code peut être implémenté sous la forme d’extensions polymorphes, et non de modifications procédurales.

Mais n'ayez jamais peur d'analyser le contexte et de voir si les inconvénients l'emportent sur les avantages, car même un principe tel que OCP peut créer un gâchis de 20 classes sur un programme de 20 lignes s'il n'est pas traité avec soin .

Ce que LSP essaie de réparer

Nous aimons tous la réutilisation du code. Une maladie qui s'ensuit est que de nombreux programmes ne le comprennent pas complètement, au point de facturer aveuglément des lignes de code communes uniquement pour créer des complexités illisibles et un couplage étroit redondant entre des modules qui, à part quelques lignes de code, n'ont rien en commun en ce qui concerne le travail conceptuel à accomplir.

Le meilleur exemple en est la réutilisation des interfaces . Vous l'avez probablement déjà vu vous-même. une classe implémente une interface, non pas parce que c'est une implémentation logique (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 le 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 pouvez alors passer des instances de classes d'une fonctionnalité conceptuelle à des emplacements qui exigent 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 disposons de la technologie pour nous empêcher de commettre de telles erreurs. Ce que nous devons faire, c'est traiter les interfaces comme API + Protocole . L'API apparaît dans les déclarations et le protocole dans les utilisations existantes de l'interface. Si nous avons 2 protocoles conceptuels qui partagent la même API, ils devraient être représentés sous la forme de 2 interfaces différentes. Sinon, nous sommes pris dans le dogmatisme DRY et, paradoxalement, nous ne créons que plus de difficulté à maintenir le code.

Maintenant, vous devriez être capable de comprendre parfaitement la définition. Le LSP dit: Ne pas hériter d'une classe de base et implémenter des fonctionnalités dans ces sous-classes avec lesquelles d'autres emplacements, qui dépendent de la classe de base, ne s'entendent pas.

Yam Marcovic
la source
1
Je me suis inscrit juste pour pouvoir voter et les réponses de Spoike - excellent travail.
David Culp
7

De ma compréhension:

OCP indique: "Si vous souhaitez ajouter de nouvelles fonctionnalités, créez une nouvelle classe qui étend une classe existante, plutôt que de la modifier."

LSP indique: "Si vous créez une nouvelle classe étendant une classe existante, assurez-vous qu'elle est parfaitement interchangeable avec sa base."

Je pense donc qu'ils se complètent mais qu'ils ne sont pas égaux.

henginy
la source
4

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 concernant OCP est l'action physique d'un développeur qui écrit du code dans une classe existante.

LSP traite de la modification du comportement apportée par une classe dérivée par rapport à sa classe de base, ainsi que de la modification à l' exécution de l' exécution du programme pouvant être provoquée par l'utilisation de la sous-classe à la place de la superclasse.

Donc, bien qu'ils puissent paraître similaires à distance, OCP! = LSP. En fait, je pense que ce sont peut-être les deux seuls principes SOLID qui ne peuvent pas être compris l'un par l'autre.

guillaume31
la source
2

LSP en mots simples stipule que toute instance de Foo peut être remplacée par toute instance de Bar dérivée de Foo sans aucune perte de fonctionnalité du programme.

C'est faux. LSP déclare 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 une perte de fonctionnalité. Vous pouvez supprimer des fonctionnalités, mais uniquement lorsque du code utilisant Foo ne dépend pas de cette fonctionnalité.

Mais en fin de compte, cela est généralement difficile à réaliser, car la plupart du temps, le code utilisant Foo dépend de tout son comportement. Donc, l'enlever viole le LSP. Mais simplifier comme ceci n’est qu’une partie du LSP.

Euphorique
la source
Un cas très courant est celui où l'objet substitué supprime les effets secondaires : par exemple. un enregistreur factice qui ne génère rien ou un objet fictif utilisé lors des tests.
Inutile
0

À propos des objets susceptibles de violer

Pour comprendre la différence, vous devez comprendre les sujets des deux principes. Ce n'est pas une partie abstraite du code ou une situation qui peut violer ou non un principe. Ce sont toujours des composants spécifiques - fonction, classe ou module - qui peuvent violer OCP ou LSP.

Qui peut violer le LSP

On peut vérifier si le LSP est cassé uniquement lorsqu'il existe une interface avec un contrat et une implémentation de cette interface. Si la mise en œuvre n'est pas conforme à l'interface ou, en général, au contrat, le LSP est cassé.

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 addObjectdoit annexer son argument au conteneur. Et CustomContainerbrise clairement ce contrat. Ainsi, la CustomContainer.addObjectfonction viole LSP. Ainsi, la CustomContainerclasse viole LSP. La conséquence la plus importante est qu’on CustomContainerne peut pas la transmettre fillWithRandomNumbers(). Containerne peut pas être remplacé par CustomContainer.

Gardez à l'esprit un point très important. Ce n'est pas tout ce code qui casse le LSP, c'est spécifiquement CustomContainer.addObjectet généralement ce CustomContainerqui le casse. Lorsque vous déclarez que LSP est violé, vous devez toujours spécifier deux choses:

  • L'entité qui viole le LSP.
  • Le contrat qui est rompu par l'entité.

C'est ça. Juste un contrat et sa mise en œuvre. Un code négatif dans le code ne dit rien sur la violation de LSP.

Qui peut violer OCP

On peut vérifier si 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 au fil du temps et qu'il faut modifier le code source du composant, celui-ci enfreint 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. PlatformDescriberest le composant qui gère les valeurs de cet ensemble de données. L'ajout d'une nouvelle plate-forme nécessite la mise à jour du code source de PlatformDescriber. Ainsi, la PlatformDescriberclasse 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. Loggerest le composant responsable de l'ajout d'entrées à tous les canaux. L'ajout d'une prise en charge pour un autre mode de journalisation nécessite la mise à jour du code source de Logger. Ainsi, la Loggerclasse viole OCP.

Notez que dans les deux exemples, l'ensemble de données n'est pas quelque chose de fixe sémantiquement. Cela peut changer avec le temps. Une nouvelle plateforme peut émerger. Un nouveau canal de journalisation peut émerger. Si votre composant doit être mis à jour lorsque cela se produit, il viole OCP.

Pousser les limites

Maintenant la partie la plus 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 qu’il translateToRussianenfreint OCP. Mais en réalité ce n'est pas. GregorianWeekDaya une limite spécifique de exactement 7 jours de semaine avec les noms exacts. Et l’important est que ces limites sémantiquement ne puissent pas changer avec le temps. Il y aura toujours 7 jours dans la semaine grégorienne. Il y aura toujours lundi, mardi, etc. Cet ensemble de données est fixé sémantiquement. Il n'est pas possible que ce translateToRussiancode source nécessite des modifications. Ainsi, OCP n'est pas violé.

Maintenant, il devrait être clair qu'une switchdéclaration épuisante n'est pas toujours une indication d'un OCP cassé.

La différence

Maintenant sentez la différence:

  • Le sujet de LSP est "une implémentation d'interface / contrat". Si la mise en œuvre n'est pas conforme au contrat, le processus LSP est rompu. Ce n'est pas important si cette implémentation peut changer avec le temps ou pas, si elle est extensible ou non.
  • Le sujet d'OCP est "une façon de réagir à un changement d'exigences". Si la prise en charge d'un nouveau type de données nécessite la modification du code source du composant qui gère ces données, ce composant rompt OCP. Ce n'est pas important si le composant rompt son contrat ou non.

Ces conditions sont complètement orthogonales.

Exemples

Dans @ la réponse de Spoike le Violer un principe mais après l'autre partie est tout à fait tort.

Dans le premier exemple, la forpartie -loop viole clairement OCP car elle n'est pas extensible sans modification. Mais il n'y a aucune indication de violation de LSP. Et il n'est même pas clair si le Contextcontrat permet à getPersons de renvoyer quoi que ce soit sauf Bossou Peon. Même en supposant qu'un contrat autorisant le IPersonrenvoi de n'importe quelle sous-classe, aucune classe ne remplace cette conditionnalité et ne la viole pas. De plus, si getPersons retourne une instance d'une troisième classe, le for-loop fera son travail sans échec. 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 Contextpartie n’a rien à voir avec le LSP - pas de contrat défini, pas de sous-classement, pas de dérogation prioritaire. Ce n'est pas à Contextqui devrait obéir le LSP, c'est à LiskovSubne pas rompre le contrat de sa base. En ce qui concerne OCP, la classe est-elle vraiment fermée? - Oui, ça 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, sans limite . L'exemple n'est pas très utile dans la vie réelle, mais il ne viole manifestement pas OCP.

Essayons de faire quelques exemples corrects avec une véritable violation de OCP ou LSP.

Suivez OCP mais pas 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, HumanReadablePlatformSerializerne nécessite aucune modification quand une nouvelle plate-forme est ajoutée. Ainsi, il suit OCP.

Mais le contrat exige que vous toJsondevez renvoyer un JSON correctement formaté. La classe ne fait pas ça. De ce fait, il ne peut pas être transmis à un composant qui utilise PlatformSerializerpour formater le corps d'une requête réseau. HumanReadablePlatformSerializerViole ainsi le LSP.

Suivez LSP mais pas OCP

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, pas de violation de LSP ici.

Toutefois, si la plate-forme est le plus largement utilisée, il doit exister une indication correspondante en JSON. Dans cet exemple, OCP ne respecte pas les HumanReadablePlatformSerializer.isMostPopularfonctions car un jour, iOS devient la plate-forme la plus populaire. Formellement, cela signifie que l'ensemble des plates-formes les plus utilisées est défini pour le moment comme "Android" et ne isMostPopulargère pas correctement cet ensemble de données. L'ensemble de données n'est pas sémantiquement fixé et peut changer librement dans le temps. HumanReadablePlatformSerializerLe code source de 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é. Pour corriger SRP, vous pouvez extraire la isMostPopularfonction sur certains externes Helperet ajouter un paramètre à PlatformSerializer.toJson. Mais c'est une autre histoire.

Mekarthedev
la source
0

LSP et OCP ne sont pas identiques.

LSP parle de l'exactitude du programme tel qu'il se présente . Si une instance d'un sous-type pourrait altérer l'exactitude du programme lorsqu'elle est substituée dans le code pour les types ancêtres, vous avez démontré une violation du LSP. Vous devrez peut-être simuler un test pour montrer cela, mais vous ne devrez pas changer la base de code sous-jacente. Vous validez le programme lui-même pour voir s'il est conforme à LSP.

OCP parle de l'exactitude des modifications apportées au code du programme, le delta d'une version source à une autre. Le comportement ne doit pas être modifié. Il devrait seulement être étendu. L'exemple classique est l'ajout de champs. Tous les champs existants continuent à fonctionner comme avant. Le nouveau champ ajoute simplement des fonctionnalités. La suppression d'un champ constitue toutefois 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 la base de code en l'état , le second ne valide que le delta de base de code d'une version à l'autre . En tant que tels, ils ne peuvent pas être la même chose, ils sont définis comme valider des choses différentes.

Je vais vous donner une preuve plus formelle: Dire "LSP implique OCP" signifierait un delta (car OCP en requiert un autre que dans le cas trivial), mais LSP n'en exige pas un. Donc, c'est clairement faux. Inversement, nous pouvons réfuter "OCP implique LSP" en disant simplement qu'OCP est une déclaration sur les deltas et qu'il ne dit donc 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.

Brad Thomas
la source
-1

Je regarderais cela du point de vue du client. si le client utilise les fonctionnalités d'une interface et que cette fonctionnalité a été implémentée en interne par la classe A. Supposons qu'il existe une classe B qui étend la classe A, alors demain si je supprime la classe A de cette interface et mets la classe B, alors la classe B devrait fournir également les mêmes fonctionnalités au client. L'exemple standard est une classe de canard qui nage, et si ToyDuck étend Duck, il doit également nager et ne se plaint pas de l'impossibilité de nager, sinon ToyDuck ne devrait pas avoir prolongé la classe de canard.

AKS
la source
Ce serait très constructif si les gens ajoutaient des commentaires tout en votant à la baisse pour toute réponse. Après tout, nous sommes tous ici pour partager nos connaissances et le simple fait de juger sans motif valable ne servira à rien.
AKS
cela ne semble rien offrir de substantiel sur les points soulevés et expliqués dans les 6 réponses précédentes
gnat
1
On dirait que vous ne faites qu'expliquer l'un des principes, le L, à mon avis. Pour ce que c'est, c'est correct mais la question demandait une comparaison / contraste de deux principes différents. C'est probablement pourquoi quelqu'un a voté contre.
StarWeaver