Liskov Principio di sostituzione - condizioni di rafforzamento

7

Sono un po 'confuso riguardo a cosa significhi veramente. Nelle domande correlate ( È una violazione di il Principio di sostituzione di Liskov? ), è stato detto che l'esempio viola chiaramente LSP.

Ma mi chiedo, se non ci fosse una nuova eccezione generata, sarebbe comunque una violazione? Non è quindi semplicemente polimorfismo? Cioè:

public class Task
{
     public Status Status { get; set; }

     public virtual void Close()
     {
         Status = Status.Closed;
     }
}

public class ProjectTask : Task
{
     public override void Close()
     {
          if (Status == Status.Started) 
          {
              base.Close(); 
          }
     }
}
    
posta John V 18.01.2018 - 10:38
fonte

6 risposte

5

Dipende.

Per convalidare l'LSP, è necessario conoscere il contratto preciso della funzione Close . Se il codice è simile a questo

public class Task
{
     // after a call to this method, the status must become "Closed"
     public virtual void Close()
     //...
}

quindi una classe derivata che ignora questo commento viola l'LSP. Se, tuttavia, il codice appare come questo

public class Task
{
     // tries to close the the task 
     // (check Status afterwards to find out if it has worked)
     public virtual void Close()
     //...
}

allora ProjectTask non viola l'LSP.

Tuttavia, nel caso in cui non ci sia un commento, un nome di funzione come Close dà a IMHO un caller un'aspettativa abbastanza chiara di impostare lo stato su "Chiuso", e se la funzione non funziona in questo modo, sarebbe essere almeno una violazione del "principio del minimo stupore".

Nota anche che alcuni linguaggi di programmazione come Eiffel hanno supporti linguistici integrati per i contratti, quindi non è necessario fare affidamento sui commenti. Vedi questo articolo di Wikipedia per un elenco.

    
risposta data 18.01.2018 - 11:58
fonte
20

Non puoi decidere se un codice viola l'LSP dal codice stesso. Devi conoscere il contratto che ciascun metodo deve soddisfare.

Nell'esempio, c'è nessun contratto esplicito dato , quindi dobbiamo indovinare quale potrebbe essere il contratto previsto del metodo Close() .

Osservando l'implementazione della classe base del metodo te Close (), l'unico effetto di tale metodo è che dopo il Status è Status.Closed . La mia ipotesi migliore di un contratto per questo metodo è la seguente:

Do whatever is necessary to make the Status become Status.Closed.

Ma questa è solo una ipotesi plausibile . Nessuno può esserne sicuro se non è scritto esplicitamente.

Diamo per scontato la mia ipotesi.

Anche il metodo Close() sottoposto a override soddisfa quel contratto ? Ci sono due possibilità che dopo aver eseguito questo metodo abbiamo Status.Closed :

  • Abbiamo già avuto Status.Closed prima di chiamare il metodo.
  • Abbiamo avuto Status.Started . Quindi chiamiamo l'implementazione di base, impostando il campo su Status.Closed .
  • In tutti gli altri casi ci ritroviamo con uno stato diverso.

Se Status ha solo i due possibili valori Closed e Started (es. enum a 2 valori), tutto va bene, non c'è violazione LSP, perché otteniamo sempre Status.Closed dopo Close() metodo.

Ma probabilmente ci sono più possibili valori di Status , che finiscono con un Status non essendo Status.Closed , quindi che violano il contratto .

L'OP ha chiesto la famosa frase "ovunque io usi la classe base, la sua classe derivata può essere usata".

Quindi mi piacerebbe approfondire.

L'ho letto come "ovunque io usi la classe base nel suo contratto , la sua classe derivata può essere utilizzata, senza violare quel contratto .

Quindi non si tratta solo di non produrre errori di compilazione o di eseguire senza errori di lancio, si tratta di eseguire ciò che il contratto richiede .

E si applica solo alle situazioni in cui chiedo alla classe di fare qualcosa che rientra nel suo range di operazioni previsto. Quindi non dobbiamo preoccuparci delle situazioni di abuso (ad esempio laddove non sono soddisfatte le condizioni preliminari).

Dopo aver riletto la tua domanda, penso che dovrei aggiungere un paragrafo sul polimorfismo in quel contesto.

Il polimorfismo indica che per istanze di classi diverse, la stessa chiamata al metodo determina l'esecuzione di diverse implementazioni. Pertanto, il tecnoforisma consente tecnicamente di sostituire il nostro metodo Close() con uno che invece ad es. apre un flusso. Tecnicamente, è possibile, ma è un cattivo uso del polimorfismo. E un principio sugli usi buoni e cattivi del polimorfismo è l'LSP.

    
risposta data 18.01.2018 - 12:12
fonte
7

Il principio di sostituzione di Liskov riguarda i contratti . Consiste di precondizioni (condizioni che devono essere vere in modo che il comportamento corrispondente possa essere eseguito), postcondizioni (condizioni che devono essere vere in modo che il comportamento possa essere considerato terminato), invarianti (condizioni che devono essere valide prima, durante e dopo il corrispondente esecuzione del metodo) e vincolo cronologico (secondo me è un sottoinsieme di invarianti, quindi è meglio controllare wikipedia). In una domanda che hai collegato a un contratto implicito della classe Task ha un aspetto simile al seguente:

  • Precondizione: non c'è nessuno
  • Postcondition: Status è closed
  • Invariante: impossibile vedere qualsiasi

Quindi se una delle classi figlie non chiude l'attività, è considerata una violazione di LSP all'interno di un certo contratto .

Ma se postuli esplicitamente il tuo contratto come "Chiudi l'attività solo se è started ", allora stai bene. Puoi farlo nel tuo codice - un esempio di questo è risposta accettata . Ma molto spesso non puoi, quindi potresti usare semplici commenti.

Fondamentalmente, ogni volta che pensi alla violazione di LSP, dovresti già avere familiarità con il contratto. Non esiste una cosa come "violazione LSP", solo "violazione LSP all'interno di un contratto".

    
risposta data 18.01.2018 - 11:34
fonte
1

, ancora una violazione (probabilmente)

Alcuni client di Task si basano su "after Task::Close() , Status è ora Closed " e quindi si interrompe quando incontra un ProjectTask . Potresti attualmente non avere alcun client di questo tipo, ma la postcondizione di Task::Close() dovrebbe essere " Status è in uno stato valido ma non specificato", che è fondamentalmente inutile.

La cosa molto più naturale è che Task::Close() abbia la post-condizione " Status is Closed ", che preclude all'implementazione in ProjectTask di essere valida.

Questo è un grosso problema con i metodi void DoStuff() : tutto ciò che hai sono alcuni effetti collaterali, quindi hai cose che fanno affidamento su quegli effetti collaterali. bool TryClose() ha il significato " Close() se puoi, e parlamene"

    
risposta data 18.01.2018 - 10:47
fonte
1

Come Ralf e altri hanno menzionato, non hai effettivamente implementato o applicato alcun contratto sul tuo codice, se non con la presunta "buonsenso" convenzione che Close() dovrebbe lasciare l'oggetto in uno stato chiuso e diverso dai commenti aggiunti alla sottoclasse.

A mio parere, l'esempio che hai fornito (so che è stato copiato da un post correlato ) ha un difetto di progettazione per dichiarare il metodo Close() come virtuale sulla classe base Task - questo è solo invitare altri sottoclasse Task e cambi il comportamento, anche se hai fornito un'implementazione predefinita che osserva il contratto.

E peggio, dal momento che Status non è incapsulato affatto, lo stato è mutabile pubblicamente, quindi qualsiasi contratto intorno a Close è abbastanza privo di significato in quanto lo stato può essere assegnato casualmente esternamente in ogni caso.

Quindi, se la tua gerarchia di classi non richiede un comportamento polimorfico di Close , rimuoverò semplicemente la parola chiave virtual su Task.Close :

// Encapsulate status, to control state transition
public Status Status { get; private set; }

public void Close()
{
    Status = Status.Closed;
}

(e fai lo stesso per tutte le altre transizioni di stato)

Se tuttavia tu richiedi un comportamento polimorfico (cioè se le sottoclassi devono fornire implementazioni personalizzate di Close ), allora convertirò la tua classe base Task in un'interfaccia, e quindi applicherò le condizioni pre e post attraverso Contratti di codice , come segue:

[ContractClass(typeof(TaskContracts))]
public interface ITask
{
    Status Status { get; } // No externally accessible set

    void Close();
    // Other transition methods here.
}

Con i contratti corrispondenti:

[ContractClassFor(typeof(ITask))]
public class TaskContracts : ITask
{
    public Status Status { get; }

    public void Close()
    {
        Contract.Requires(Status != Status.Closed, "Already Closed!");
        Contract.Ensures(Status == Status.Closed, "Must close Task on Completion!");
    }
}

Il vantaggio di questo approccio è che il contratto di utilizzo dell'interfaccia è chiaro (e applicabile!) e, a differenza del virtual Close() che potrebbe essere ignorato, le sottoclassi possono fornire qualsiasi implementazione a loro piace, purché il contratto sia rispettato.

    
risposta data 18.01.2018 - 12:45
fonte
0

Sì, è ancora una violazione dell'LSP.

Nella classe base Attività , dopo che Chiudi () è stato invocato lo stato è Chiuso .
Nella classe ProjectTask derivata, dopo aver richiamato Close () lo stato può essere o meno chiuso .

Quindi la post-condizione (lo stato è Chiuso ) non è più soddisfatta nella classe ProjectTask .
O in altre parole: un cliente che sa solo di Task può contare sul fatto che Status è Closed dopo aver richiamato Close () . Se gli dai un ProjectTask "mascherato" come un compito (che ti è permesso fare), e invoca Close () il risultato è diverso (lo stato potrebbe non essere Chiuso ).

    
risposta data 18.01.2018 - 11:27
fonte

Leggi altre domande sui tag