Sostituzione LSP vs OCP / Liskov VS Apri Chiudi


48

Sto cercando di capire i principi SOLID di OOP e sono giunto alla conclusione che LSP e OCP hanno alcune somiglianze (se non per dire altro).

il principio aperto / chiuso afferma che "le entità software (classi, moduli, funzioni, ecc.) dovrebbero essere aperte per l'estensione, ma chiuse per modifica".

LSP in parole semplici afferma che qualsiasi istanza di Foopuò essere sostituita con qualsiasi istanza da Barcui deriva Fooe il programma funzionerà allo stesso modo.

Non sono un programmatore OOP professionista, ma mi sembra che LSP sia possibile solo se Bar, derivato da Foo, non cambia nulla in esso ma lo estende. Ciò significa che in particolare il programma LSP è vero solo quando OCP è vero e OCP è vero solo se LSP è vero. Ciò significa che sono uguali.

Correggimi se sbaglio. Voglio davvero capire queste idee. Grazie mille per una risposta.


4
Questa è un'interpretazione molto ristretta di entrambi i concetti. Apertura / chiusura possono essere mantenute ma violano ancora LSP. Gli esempi Rettangolo / Quadrato o Ellisse / Cerchio sono buone illustrazioni. Entrambi aderiscono a OCP, ma entrambi violano LSP.
Joel Etherton,

1
Il mondo (o almeno Internet) è confuso su questo. kirkk.com/modularity/2009/12/solid-principles-of-class-design . Questo ragazzo dice che la violazione di LSP è anche violazione OCP. E poi nel libro "Software Engineering Design: Theory and Practice" a pagina 156 l'autore fornisce un esempio di qualcosa che aderisce a OCP ma viola LSP. Ho rinunciato a questo.
Manoj R,

@JoelEtherton Quelle coppie violano LSP solo se sono mutabili. Nel caso immutabile, derivare Squareda Rectanglenon viola LSP. (Ma probabilmente è ancora un cattivo design nel caso immutabile poiché puoi avere quadrati Rectangles che Squarenon corrispondono a quelli matematici)
CodesInChaos

Analogia semplice (dal punto di vista di uno scrittore-utente di una biblioteca). LSP è come vendere un prodotto (libreria) che afferma di implementare il 100% di ciò che dice (nell'interfaccia o nel manuale dell'utente), ma in realtà non (o non corrisponde a ciò che viene detto). OCP è come vendere un prodotto (libreria) con la promessa che può essere aggiornato (esteso) quando escono nuove funzionalità (come il firmware), ma in realtà non può essere aggiornato senza un servizio di fabbrica.
rwong

Risposte:


119

Accidenti, ci sono alcune strane idee sbagliate su ciò che OCP e LSP e alcune sono dovute alla mancata corrispondenza di alcune terminologie ed esempi confusi. Entrambi i principi sono solo la "stessa cosa" se li implementi allo stesso modo. I modelli di solito seguono i principi in un modo o nell'altro con poche eccezioni.

Le differenze verranno spiegate più in basso, ma prima facciamo un tuffo nei principi stessi:

Principio aperto-chiuso (OCP)

Secondo lo zio Bob :

Dovresti essere in grado di estendere un comportamento delle classi, senza modificarlo.

Si noti che la parola extension in questo caso non significa necessariamente che è necessario sottoclassare la classe effettiva che necessita del nuovo comportamento. Vedi come ho accennato alla prima discrepanza della terminologia? La parola chiave extendsignifica solo sottoclasse in Java, ma i principi sono più vecchi di Java.

L'originale venne da Bertrand Meyer nel 1988:

Le entità software (classi, moduli, funzioni, ecc.) Devono essere aperte per l'estensione, ma chiuse per modifica.

Qui è molto più chiaro che il principio viene applicato alle entità software . Un cattivo esempio potrebbe essere l'override dell'entità software mentre si modifica completamente il codice anziché fornire un punto di estensione. Il comportamento dell'entità software stessa dovrebbe essere estensibile e un buon esempio di ciò è l'implementazione del modello di strategia (perché è il più semplice da mostrare del gruppo IMHO dei modelli di GoF):

// 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();
}

Nell'esempio sopra Contextè bloccato per ulteriori modifiche. La maggior parte dei programmatori vorrebbe probabilmente sottoclassare la classe per estenderla, ma qui non lo facciamo perché presume che il suo comportamento possa essere modificato attraverso qualsiasi cosa che implementi l' IBehaviorinterfaccia.

Vale a dire la classe di contesto è chiusa per modifica ma aperta per estensione . Segue in realtà un altro principio di base perché stiamo mettendo il comportamento con la composizione dell'oggetto invece dell'ereditarietà:

"Favorire la" composizione dell'oggetto "rispetto all'ereditarietà di classe ." (Gang of Four 1995: 20)

Lascerò che il lettore legga questo principio in quanto non rientra nell'ambito di questa domanda. Per continuare con l'esempio, supponiamo di avere le seguenti implementazioni dell'interfaccia 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!");
    }
}

Usando questo modello possiamo modificare il comportamento del contesto in fase di esecuzione, attraverso il setBehaviormetodo come punto di estensione.

// 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!"

Quindi, ogni volta che vuoi estendere la classe di contesto "chiusa", esegui la sottoclasse della sua dipendenza collaborativa "aperta". Questo chiaramente non è la stessa cosa della sottoclasse del contesto stesso, ma è OCP. Anche LSP non fa menzione di questo.

Estensione con Mixin anziché Ereditarietà

Esistono altri modi per eseguire OCP oltre alla sottoclasse. Un modo è mantenere le tue classi aperte per l'estensione attraverso l'uso di mixin . Ciò è utile, ad esempio, in linguaggi basati su prototipi piuttosto che su classi. L'idea è quella di modificare un oggetto dinamico con più metodi o attributi secondo necessità, in altre parole oggetti che si fondono o "si mescolano" con altri oggetti.

Ecco un esempio javascript di un mixin che rende un semplice modello HTML per ancore:

// 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'idea è di estendere gli oggetti in modo dinamico e il vantaggio è che gli oggetti possono condividere metodi anche se si trovano in domini completamente diversi. Nel caso sopra puoi facilmente creare altri tipi di ancore html estendendo la tua implementazione specifica con LinkMixin.

In termini di OCP, i "mixin" sono estensioni. Nell'esempio sopra YoutubeLinkè la nostra entità software che è chiusa per modifica, ma aperta per estensioni attraverso l'uso di mixin. La gerarchia degli oggetti è appiattita, il che rende impossibile verificare la presenza di tipi. Tuttavia, questa non è davvero una brutta cosa, e spiegherò più avanti che il controllo dei tipi è generalmente una cattiva idea e rompe l'idea con il polimorfismo.

Si noti che è possibile eseguire l'ereditarietà multipla con questo metodo poiché la maggior parte delle extendimplementazioni può combinare più oggetti:

_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);

L'unica cosa da tenere a mente è non scontrarsi con i nomi, vale a dire che i mixin definiscono lo stesso nome di alcuni attributi o metodi in quanto verranno sovrascritti. Nella mia umile esperienza questo non è un problema e se succede è un'indicazione di un design difettoso.

Principio di sostituzione di Liskov (LSP)

Lo zio Bob lo definisce semplicemente:

Le classi derivate devono essere sostituibili per le loro classi di base.

Questo principio è vecchio, infatti la definizione di zio Bob non differenzia i principi in quanto ciò rende LSP ancora strettamente correlato a OCP dal fatto che, nell'esempio di strategia sopra, viene utilizzato lo stesso supertipo ( IBehavior). Vediamo quindi la sua definizione originale di Barbara Liskov e vediamo se possiamo scoprire qualcos'altro su questo principio che assomiglia a un teorema matematico:

Ciò che si desidera qui è qualcosa di simile alla seguente proprietà di sostituzione: Se per ogni oggetto o1di tipo Sesiste un oggetto o2di tipo Ttale che per tutti i programmi Pdefiniti in termini di T, il comportamento di Primane invariato quando o1viene sostituito, o2allora Sè un sottotipo di T.

Facciamo spallucce per un po ', notate perché non menziona affatto le lezioni. In JavaScript puoi effettivamente seguire LSP anche se non è esplicitamente basato sulla classe. Se il tuo programma ha un elenco di almeno un paio di oggetti JavaScript che:

  • deve essere calcolato allo stesso modo,
  • hanno lo stesso comportamento e
  • altrimenti sono in qualche modo completamente diversi

... quindi si considera che gli oggetti abbiano lo stesso "tipo" e non importa davvero per il programma. Questo è essenzialmente polimorfismo . In senso generico; non dovresti aver bisogno di conoscere il sottotipo reale se stai usando la sua interfaccia. OCP non dice nulla di esplicito al riguardo. In realtà individua anche un errore di progettazione che la maggior parte dei programmatori principianti fa:

Ogni volta che senti il ​​bisogno di controllare il sottotipo di un oggetto, molto probabilmente lo stai facendo SBAGLIATO.

Va bene, quindi potrebbe non essere sempre sbagliato, ma se hai la voglia di fare qualche tipo di controllo con instanceofo enum, potresti fare il programma un po 'più contorto per te di quanto dovrebbe essere. Ma questo non è sempre il caso; gli hack rapidi e sporchi per far funzionare le cose sono una buona concessione da prendere in considerazione se la soluzione è abbastanza piccola e se pratichi il refactoring senza pietà , potrebbe migliorare una volta che le modifiche lo richiedono.

Esistono modi per aggirare questo "errore di progettazione", a seconda del problema reale:

  • La superclasse non chiama i prerequisiti, costringendo invece il chiamante a farlo.
  • Alla super classe manca un metodo generico di cui il chiamante ha bisogno.

Entrambi sono "errori" comuni nella progettazione del codice. Esistono un paio di diversi refactoring che è possibile eseguire, come il metodo pull-up o il refactoring di uno schema come lo schema Visitatore .

In realtà mi piace molto il modello Visitor in quanto può occuparsi di grandi spaghetti if-statement ed è più semplice da implementare rispetto a quello che penseresti sul codice esistente. Supponiamo che abbiamo il seguente contesto:

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"

I risultati dell'istruzione if possono essere tradotti nei propri visitatori poiché ciascuno dipende da alcune decisioni e da alcuni codici da eseguire. Possiamo estrarre questi in questo modo:

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");
    }
}

A questo punto, se il programmatore non fosse a conoscenza del modello Visitor, implementerebbe invece la classe Context per verificare se è di un certo tipo. Poiché le classi Visitor hanno un canDometodo booleano , l'implementatore può usare quella chiamata al metodo per determinare se è l'oggetto giusto per fare il lavoro. La classe di contesto può utilizzare tutti i visitatori (e aggiungerne di nuovi) in questo modo:

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);
    }
}

Entrambi i modelli seguono OCP e LSP, tuttavia entrambi identificano cose diverse su di essi. Quindi, come appare il codice se viola uno dei principi?

Violare un principio ma seguire l'altro

Ci sono modi per violare uno dei principi ma è comunque necessario seguirne l'altro. Gli esempi seguenti sembrano inventati, per una buona ragione, ma in realtà li ho visti spuntare nel codice di produzione (e anche peggio):

Segue OCP ma non LSP

Diciamo che abbiamo il codice dato:

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() { ... }
}

Questo pezzo di codice segue il principio aperto-chiuso. Se chiamiamo il GetPersonsmetodo del contesto , avremo un sacco di persone tutte con le proprie implementazioni. Ciò significa che IPerson è chiuso per modifica, ma aperto per estensione. Comunque le cose prendono una svolta oscura quando dobbiamo usarlo:

// 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();
    }
}

Devi fare il controllo del tipo e la conversione del tipo! Ricordi come ho detto sopra come il controllo del tipo è una cosa negativa ? Oh no! Ma non temere, come menzionato sopra o eseguire un refactoring pull-up o implementare uno schema Visitatore. In questo caso possiamo semplicemente fare un refactoring pull up dopo aver aggiunto un metodo generale:

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

Il vantaggio ora è che non è più necessario conoscere il tipo esatto, seguendo 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();
}

Segue LSP ma non OCP

Vediamo un codice che segue LSP ma non OCP, è un po 'inventato, ma sopporta con me su questo è un errore molto sottile:

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 }
}

Il codice fa LSP perché il contesto può usare LiskovBase senza conoscere il tipo effettivo. Penseresti che questo codice segua anche OCP ma guardi da vicino, la classe è davvero chiusa ? E se il doStuffmetodo avesse fatto qualcosa di più della semplice stampa di una linea?

La risposta se segue OCP è semplicemente: NO , non è perché in questa progettazione di oggetti siamo tenuti a sostituire completamente il codice con qualcos'altro. Questo apre la lattina tagliata e incolla di worm poiché devi copiare il codice dalla classe base per far funzionare le cose. Il doStuffmetodo è sicuramente aperto per l'estensione, ma non è stato completamente chiuso per la modifica.

Siamo in grado di applicare il metodo di motivo template su questo. Il modello del modello di modello è così comune nei framework che potresti averlo usato senza saperlo (ad esempio componenti java swing, moduli e componenti c #, ecc.). Ecco un modo per chiudere il doStuffmetodo di modifica e assicurarsi che rimanga chiuso contrassegnandolo con la finalparola chiave java . Quella parola chiave impedisce a chiunque di sottoclassare ulteriormente la classe (in C # puoi usare sealedper fare la stessa cosa).

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!";
    }
}

Questo esempio segue OCP e sembra sciocco, ma lo è, ma immagina che sia ingrandito con più codice da gestire. Continuo a vedere il codice distribuito in produzione in cui le sottoclassi hanno la precedenza su tutto e il codice sovrascritto viene principalmente tagliato e incollato tra le implementazioni. Funziona, ma come per tutta la duplicazione del codice è anche predisposto per incubi di manutenzione.

Conclusione

Spero che tutto ciò chiarisca alcune domande riguardanti OCP e LSP e le differenze / somiglianze tra loro. È facile respingerli come gli stessi, ma gli esempi sopra dovrebbero mostrare che non lo sono.

Nota che, raccogliendo dal codice di esempio sopra:

  • OCP consiste nel bloccare il codice di lavoro ma mantenerlo comunque aperto in qualche modo con alcuni punti di estensione.

    Questo per evitare la duplicazione del codice incapsulando il codice che cambia come nell'esempio del modello Metodo modello. Permette anche di fallire rapidamente poiché i cambiamenti di rottura sono dolorosi (cioè cambiare un posto, romperlo ovunque). Per motivi di manutenzione, il concetto di incapsulare il cambiamento è una buona cosa, perché i cambiamenti avvengono sempre .

  • LSP consiste nel consentire all'utente di gestire diversi oggetti che implementano un supertipo senza verificare quale sia il tipo effettivo. Questo è intrinsecamente il polimorfismo .

    Questo principio fornisce un'alternativa al controllo del tipo e alla conversione del tipo, che può sfuggire di mano all'aumentare del numero di tipi e può essere ottenuto attraverso il refactoring pull-up o l'applicazione di schemi come il visitatore.


7
Questa è una buona spiegazione, perché non semplifica eccessivamente l'OCP implicando che ciò significhi sempre implementazione per eredità. È quella semplificazione eccessiva che unisce OCP e SRP nella mente di alcune persone, quando in realtà possono essere due concetti completamente separati.
Eric King,

5
Questa è una delle migliori risposte di scambio di stack che abbia mai visto. Vorrei poterlo votare 10 volte. Ben fatto, e grazie per l'eccellente spiegazione.
Bob Horn,

Lì, ho aggiunto un blurb su Javascript che non è un linguaggio di programmazione basato su classi ma può ancora seguire LSP e ha modificato il testo in modo che si spera possa essere letto in modo più fluido. Accidenti!
Spoike,

Mentre la tua citazione di Zio Bob di LSP è corretta (come il suo sito Web), non dovrebbe essere il contrario? Non dovrebbe affermare che "Le classi di base dovrebbero essere sostituibili per le loro classi derivate"? Su LSP il test di "compatibilità" viene eseguito sulla classe derivata e non su quella di base. Tuttavia, non sono un madrelingua inglese e penso che potrebbero esserci alcuni dettagli sulla frase che mi potrebbe mancare.
Alpha,

@Alpha: Questa è una buona domanda. La classe base è sempre sostituibile con le sue classi derivate altrimenti l'ereditarietà non funzionerebbe. Il compilatore (almeno in Java e C #) si lamenterà se si sta tralasciando un membro (metodo o attributo / campo) dalla classe estesa che deve essere implementata. LSP ha lo scopo di impedirti di aggiungere metodi che sono disponibili solo localmente sulle classi derivate, in quanto ciò richiede all'utente di quelle classi derivate di conoscerle. Man mano che il codice cresce, tali metodi sarebbero difficili da mantenere.
Spoike,

15

Questo è qualcosa che provoca molta confusione. Preferisco considerare questi principi in qualche modo filosoficamente, perché ci sono molti esempi diversi per loro, e talvolta esempi concreti non catturano davvero la loro intera essenza.

Cosa OCP tenta di risolvere

Supponiamo che dobbiamo aggiungere funzionalità a un determinato programma. Il modo più semplice per farlo, specialmente per le persone che sono state addestrate a pensare in modo procedurale, è aggiungere una clausola if ovunque necessario, o qualcosa del genere.

I problemi con questo sono

  1. Cambia il flusso del codice esistente e funzionante.
  2. Costringe una nuova ramificazione condizionale su ogni caso. Ad esempio, supponi di avere un elenco di libri e che alcuni di essi siano in vendita e che desideri iterare su tutti e stampare il loro prezzo, in modo che se sono in vendita, il prezzo stampato includerà la stringa " (IN VENDITA)".

Puoi farlo aggiungendo un campo aggiuntivo a tutti i libri denominati "is_on_sale", quindi puoi controllare quel campo quando stampi il prezzo di un libro o , in alternativa , puoi creare un'istanza di libri in vendita dal database usando un tipo diverso, che stampa "(IN VENDITA)" nella stringa di prezzo (non un design perfetto ma offre il punto a casa).

Il problema con la prima soluzione procedurale è un campo in più per ogni libro e una complessità ridondante in molti casi. La seconda soluzione forza la logica solo dove è effettivamente richiesta.

Ora considera il fatto che potrebbero esserci molti casi in cui sono richiesti dati e logica diversi e vedrai perché tenere a mente OCP durante la progettazione delle classi o la reazione ai cambiamenti dei requisiti, è una buona idea.

Ormai dovresti avere l'idea principale: prova a metterti in una situazione in cui il nuovo codice può essere implementato come estensioni polimorfiche, non modifiche procedurali.

Ma non abbiate mai paura di analizzare il contesto e vedere se gli svantaggi superano di gran lunga i benefici, perché anche un principio come OCP può creare un disordine di 20 classi da un programma di 20 linee, se non trattato con cura .

Cosa cerca di risolvere LSP

Tutti amiamo il riutilizzo del codice. Una malattia che ne consegue è che molti programmi non lo capiscono completamente, al punto che stanno prendendo in considerazione ciecamente linee di codice comuni solo per creare complessità illeggibili e ridondanti accoppiamenti tra moduli che, oltre a poche righe di codice, non hanno nulla in comune per quanto riguarda il lavoro concettuale da fare.

Il più grande esempio di ciò è il riutilizzo dell'interfaccia . Probabilmente l'hai visto tu stesso; una classe implementa un'interfaccia, non perché è un'implementazione logica di essa (o un'estensione nel caso di classi di base concrete), ma perché i metodi che capita di dichiarare a quel punto hanno le firme giuste per quanto riguarda.

Ma poi incontri un problema. Se le classi implementano le interfacce solo considerando le firme dei metodi che dichiarano, allora ti trovi in ​​grado di passare istanze di classi da una funzionalità concettuale a luoghi che richiedono funzionalità completamente diverse, che dipendono solo da firme simili.

Non è così orribile, ma provoca molta confusione e abbiamo la tecnologia per impedirci di commettere errori come questi. Quello che dobbiamo fare è trattare le interfacce come protocollo API + . L'API è evidente nelle dichiarazioni e il protocollo è evidente negli usi esistenti dell'interfaccia. Se abbiamo 2 protocolli concettuali che condividono la stessa API, dovrebbero essere rappresentati come 2 interfacce diverse. Altrimenti ci lasciamo prendere dal dogmatismo SECCO e, ironia della sorte, creiamo solo più difficile mantenere il codice.

Ora dovresti essere in grado di comprendere perfettamente la definizione. LSP dice: non ereditare da una classe base e implementare funzionalità in quelle sottoclassi che, in altri luoghi, che dipendono dalla classe base, non andranno d'accordo.


1
Mi sono iscritto solo per poter votare questo e le risposte di Spoike - ottimo lavoro.
David Culp,

7

Dalla mia comprensione:

OCP dice: "Se aggiungerai nuove funzionalità, crea una nuova classe estendendone una esistente, anziché modificarla".

LSP dice: "Se crei una nuova classe che estende una classe esistente, assicurati che sia completamente intercambiabile con la sua base."

Quindi penso che si completino a vicenda ma non sono uguali.


4

Mentre è vero che OCP e LSP hanno entrambi a che fare con la modifica, il tipo di modifica di cui parla OCP non è quello di cui parla LSP.

La modifica rispetto all'OCP è l'azione fisica di uno sviluppatore che scrive codice in una classe esistente.

LSP si occupa della modifica comportamentale apportata da una classe derivata rispetto alla sua classe base e dell'alterazione di runtime dell'esecuzione del programma che può essere causata utilizzando la sottoclasse anziché la superclasse.

Quindi, sebbene possano sembrare simili a distanza OCP! = LSP. In effetti penso che possano essere gli unici 2 principi SOLIDI che non possono essere compresi l'uno nell'altro.


2

LSP in parole semplici afferma che qualsiasi istanza di Foo può essere sostituita con qualsiasi istanza di Bar derivata da Foo senza alcuna perdita di funzionalità del programma.

Questo è sbagliato. LSP afferma che la classe Bar non dovrebbe introdurre comportamenti, cosa non prevista quando il codice utilizza Foo, quando Bar deriva da Foo. Non ha nulla a che fare con la perdita di funzionalità. Puoi rimuovere la funzionalità, ma solo quando il codice che utilizza Foo non dipende da questa funzionalità.

Ma alla fine, questo è di solito difficile da ottenere, perché la maggior parte delle volte, il codice usando Foo dipende da tutto il suo comportamento. Quindi rimuoverlo viola LSP. Ma semplificarlo in questo modo è solo una parte di LSP.


Un caso molto comune è quello in cui l'oggetto sostituito rimuove gli effetti collaterali : ad es. un logger fittizio che non emette nulla o un oggetto simulato utilizzato nei test.
Inutile

0

Informazioni sugli oggetti che potrebbero violare

Per capire la differenza dovresti capire le materie di entrambi i principi. Non è una parte astratta di codice o situazione che può violare o meno un principio. È sempre un componente specifico - funzione, classe o un modulo - che può violare OCP o LSP.

Chi può violare LSP

Si può verificare se LSP è rotto solo quando esiste un'interfaccia con un contratto e un'implementazione di tale interfaccia. Se l'implementazione non è conforme all'interfaccia o, in generale, al contratto, LSP viene interrotto.

Esempio più semplice:

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())
    }
}

Il contratto afferma chiaramente che addObjectdovrebbe aggiungere la sua argomentazione al container. E CustomContainerrompe chiaramente quel contratto. Pertanto la CustomContainer.addObjectfunzione viola LSP. Pertanto la CustomContainerclasse viola LSP. La conseguenza più importante è che CustomContainernon può essere passato a fillWithRandomNumbers(). Containernon può essere sostituito con CustomContainer.

Tieni presente un punto molto importante. Non è l'intero codice che rompe LSP, è specificamente CustomContainer.addObjecte generalmente CustomContainerche rompe LSP. Quando dichiari che LSP è stato violato, dovresti sempre specificare due cose:

  • L'entità che viola LSP.
  • Il contratto che viene rotto dall'entità.

Questo è tutto. Solo un contratto e la sua attuazione. Un downcast nel codice non dice nulla sulla violazione di LSP.

Chi può violare OCP

È possibile verificare se OCP è stato violato solo quando esiste un set di dati limitato e un componente che gestisce i valori di quel set di dati. Se i limiti del set di dati possono cambiare nel tempo e ciò richiede la modifica del codice sorgente del componente, il componente viola OCP.

Sembra complesso. Proviamo un semplice esempio:

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";
        }
    }
}

Il set di dati è il set di piattaforme supportate. PlatformDescriberè il componente che gestisce i valori di quel set di dati. L'aggiunta di una nuova piattaforma richiede l'aggiornamento del codice sorgente di PlatformDescriber. Pertanto la PlatformDescriberclasse viola OCP.

Un altro esempio:

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")
    }
}

Il "set di dati" è l'insieme di canali in cui aggiungere una voce di registro. Loggerè il componente responsabile dell'aggiunta di voci a tutti i canali. L'aggiunta del supporto per un altro modo di registrazione richiede l'aggiornamento del codice sorgente di Logger. Pertanto la Loggerclasse viola OCP.

Si noti che in entrambi gli esempi il set di dati non è qualcosa di semanticamente corretto. Potrebbe cambiare nel tempo. Potrebbe emergere una nuova piattaforma. Potrebbe emergere un nuovo canale di registrazione. Se il tuo componente deve essere aggiornato quando ciò accade, viola OCP.

Spingendo i limiti

Ora la parte difficile. Confronta gli esempi sopra con i seguenti:

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 "Воскресенье";
    }
}

Potresti pensare che translateToRussianviola OCP. Ma in realtà non lo è. GregorianWeekDayha un limite specifico di esattamente 7 giorni settimanali con nomi esatti. E la cosa importante è che questi limiti semanticamente non possono cambiare nel tempo. Ci saranno sempre 7 giorni nella settimana gregoriana. Ci saranno sempre lunedì, martedì, ecc. Questo set di dati è fisso semanticamente. Non è possibile che translateToRussianil codice sorgente richieda modifiche. Pertanto OCP non viene violato.

Ora dovrebbe essere chiaro che l' switchaffermazione estenuante non è sempre un'indicazione di OCP rotto.

La differenza

Ora senti la differenza:

  • L'argomento di LSP è "un'implementazione dell'interfaccia / contratto". Se l'implementazione non è conforme al contratto, allora interrompe LSP. Non è importante se tale implementazione può cambiare nel tempo o meno, se è estensibile o meno.
  • L'argomento di OCP è "un modo di rispondere a una modifica dei requisiti". Se il supporto per un nuovo tipo di dati richiede la modifica del codice sorgente del componente che gestisce tali dati, tale componente interrompe OCP. Non è importante se il componente interrompe il contratto o meno.

Queste condizioni sono completamente ortogonali.

Esempi

In @ risposta di Spoike la violazione di un principio, ma in seguito l'altra parte è totalmente sbagliato.

Nel primo esempio la forparte -loop sta chiaramente violando l'OCP perché non è estensibile senza modifiche. Ma non vi è alcuna indicazione di violazione di LSP. E non è nemmeno chiaro se il Contextcontratto consenta a getPersons di restituire qualcosa tranne Bosso Peon. Anche ipotizzando un contratto che consenta la IPersonrestituzione di qualsiasi sottoclasse, non esiste alcuna classe che ignori questa post-condizione e la violi. Inoltre, se getPersons restituirà un'istanza di una terza classe, il for-loop farà il suo lavoro senza errori. Ma questo fatto non ha nulla a che fare con LSP.

Il prossimo. Nel secondo esempio non vengono violati né LSP né OCP. Ancora una volta, la Contextparte non ha nulla a che fare con LSP: nessun contratto definito, nessuna sottoclasse, nessuna sostituzione forzata. Non è Contextchi dovrebbe obbedire a LSP, non LiskovSubdovrebbe rompere il contratto della sua base. Per quanto riguarda l'OCP, la classe è davvero chiusa? - sì. Non è necessaria alcuna modifica per estenderlo. Ovviamente il nome degli stati del punto di estensione fa tutto quello che vuoi, senza limiti . L'esempio non è molto utile nella vita reale, ma chiaramente non viola l'OCP.

Proviamo a fare alcuni esempi corretti con una vera violazione di OCP o LSP.

Segui OCP ma non 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();
    }
}

Qui, HumanReadablePlatformSerializernon richiede alcuna modifica quando viene aggiunta una nuova piattaforma. Quindi segue OCP.

Ma il contratto richiede che toJsondebba restituire un JSON formattato correttamente. La classe non lo fa. Per questo motivo non può essere passato a un componente che utilizza PlatformSerializerper formattare il corpo di una richiesta di rete. Quindi HumanReadablePlatformSerializerviola LSP.

Segui LSP ma non OCP

Alcune modifiche all'esempio precedente:

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)
    }
}

Il serializzatore restituisce una stringa JSON formattata correttamente. Quindi, nessuna violazione LSP qui.

Tuttavia, è necessario che se la piattaforma viene utilizzata in gran parte, dovrebbe esserci un'indicazione corrispondente in JSON. In questo esempio OCP viene violato dalla HumanReadablePlatformSerializer.isMostPopularfunzione perché un giorno iOS diventerà la piattaforma più popolare. Formalmente significa che l'insieme delle piattaforme più utilizzate è definito come "Android" per ora e isMostPopulargestisce in modo inadeguato quel set di dati. Il set di dati non è fisso semanticamente e può cambiare liberamente nel tempo. HumanReadablePlatformSerializerIl codice sorgente deve essere aggiornato in caso di modifica.

In questo esempio potresti anche notare una violazione della responsabilità singola. Ho intenzionalmente fatto in modo da poter dimostrare entrambi i principi sulla stessa entità soggetto. Per correggere SRP è possibile estrarre la isMostPopularfunzione da alcuni esterni Helpere aggiungere un parametro a PlatformSerializer.toJson. Ma questa è un'altra storia.


0

LSP e OCP non sono gli stessi.

LSP parla della correttezza del programma così com'è . Se un'istanza di un sottotipo interrompe la correttezza del programma quando viene sostituita nel codice per i tipi di antenati, allora hai dimostrato una violazione di LSP. Potrebbe essere necessario simulare un test per dimostrarlo, ma non è necessario modificare la base di codice sottostante. Stai convalidando il programma stesso per vedere se soddisfa LSP.

OCP parla della correttezza delle modifiche nel codice del programma, il delta da una versione sorgente a un'altra. Il comportamento non deve essere modificato. Dovrebbe essere solo esteso. L'esempio classico è l'aggiunta sul campo. Tutti i campi esistenti continuano a funzionare come prima. Il nuovo campo aggiunge solo funzionalità. L'eliminazione di un campo, tuttavia, è in genere una violazione di OCP. Qui stai convalidando il delta della versione del programma per vedere se soddisfa OCP.

Questa è la differenza chiave tra LSP e OCP. Il primo convalida solo la base di codice così com'è , il secondo convalida solo il delta di base di codice da una versione alla successiva . In quanto tali, non possono essere la stessa cosa, sono definiti come validanti cose diverse.

Ti darò una prova più formale: dire che "LSP implica OCP" significherebbe un delta (perché OCP ne richiede uno diverso rispetto al caso banale), ma LSP non ne ha bisogno. Quindi questo è chiaramente falso. Al contrario, possiamo confutare "OCP implica LSP" semplicemente dicendo che OCP è un'affermazione sui delta, quindi non dice nulla su un'affermazione su un programma in atto. Ciò deriva dal fatto che è possibile creare QUALSIASI delta a partire da QUALSIASI programma in atto. Sono totalmente indipendenti.


-1

Lo guarderei dal punto di vista del cliente. se il client utilizza le funzionalità di un'interfaccia e internamente tale funzionalità è stata implementata dalla classe A. Supponiamo che esista una classe B che estenda la classe A, quindi domani se rimuovo la classe A da tale interfaccia e inserisco la classe B, allora la classe B dovrebbe forniscono anche le stesse funzionalità al client. L'esempio standard è una classe Duck che nuota, e se ToyDuck estende Duck allora dovrebbe anche nuotare e non si lamenta di non poter nuotare, altrimenti ToyDuck non dovrebbe estendere la classe Duck.


Sarebbe molto costruttivo se le persone mettessero commenti anche mentre votavano in giù qualsiasi risposta. Dopotutto siamo tutti qui per condividere la conoscenza, e semplicemente giudicare senza una ragione adeguata non servirà a nessuno scopo.
AKS

questo non sembra offrire nulla di sostanziale rispetto ai punti formulati e spiegati nelle precedenti 6 risposte
moscerino del

1
Sembra che tu stia solo spiegando uno dei principi, quello L penso. Per quello che è va bene, ma la domanda ha richiesto un confronto / contrasto di due diversi principi. Questo è probabilmente il motivo per cui qualcuno l'ha sottovalutato.
StarWeaver,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.