Sostituzione di Liskov per vuoti e precondizioni indebolite

3

Sto imparando molto su questo principio (anche grazie a due risposte che ho ricevuto qui) e vorrei approfondire su un altro punto che qualcuno ha menzionato.

1) La seguente è una violazione di LSP?

class Base
{
   public virtual void UpdateUI()
   { 
     //documented: immediately redraws UI completely
   }  
}

class Component: Base
{
  public override UpdateUI
  {
    if (Time.Seconds % 10 ==0)  //updates only every ten seconds
    {
      //drawing logic
    }
  }
}

A mio avviso, la descrizione del codice nella classe base rappresenta il contratto, il comportamento previsto, che viene violato nel sottotipo.

2) Perché la rottura del comportamento non ha importanza per la precondizione indebolimento?

class Human
{
  public virtual void DoSomething(int age)
  {
     //precondition - age < 100
  {

}

class Cyborg : Human
{
  public virtual void DoSomething(int age)
  {
     //precondition - age < 200
  {
}

La classe dei Cyborg ha indebolito la precondizione, che è consentita. Per argomenti validi, la sostituzione funziona bene. Ogni volta che ho un oggetto Umano, può essere usato un oggetto Cyborg. Ma cosa succede se ho un test come questo: Human (110) - deve fallire, l'argomento deve essere < 100

Quando sostituisco con Cyborg, il test passerà. Cioè il comportamento è cambiato. Perché è permesso?

    
posta John V 20.01.2018 - 11:38
fonte

3 risposte

3

Alla risposta molto interessante di CandleOrange, vorrei aggiungere che:

  • I precondizionamenti non possono essere rafforzati significa ciò che può essere fatto con il supertipo può essere fatto con il sottotipo
  • Le postcondizioni non possono essere indebolite e gli invarianti devono essere conservati significa ciò che è garantito dal supertipo è garantito dal sottotipo

Non sono d'accordo tuttavia con le sue conclusioni per il caso 1.

Caso 2: va bene!

Il seguente codice funziona per il supertipo. Se il tuo codice è conforme a LSP, tutto ciò che funziona per il supertipo funziona per il sottotipo. Quindi stai bene:

 // suppose x is a Human or a Cyborg
 x.DoSomething (50);  // if it works for a Human, it should work for a cyborg

Attenzione: se le precondizioni del supertipo non vengono soddisfatte, non puoi dire nulla sul fatto che debba funzionare o meno per il sottotipo. Questo non è detto da LSP ma è la pura conseguenza della relazione implicita (cioè da p implica q , puoi dedurre che non q implica p , ma non puoi dedurre nulla da non p ):

// suppose x is a Human or a Cyborg
x.DoSomething (150);   // Id doesn't even work for a Human,
                       // so we don't care if it works for a Cyborg

Caso 2: potresti essere confuso?

Ora le cose sarebbero completamente diverse, se ti aspetteresti qualche eccezione o un fallimento per gli umani oltre una certa età. Ma poi non stai più pensando alle precondizioni, ma alle post-condizioni. E il sottotipo non può indebolirlo:

class Human
{
   public virtual void DoSomething(int age)
   {
      // post condition:  exception thrown if age >= 100
   {
  }

class Cyborg : Human
{
  public virtual void DoSomething(int age)
  {
      // post condition:  exception thrown if age >= 200  (!!! INVALID IN LSP)
  {
}

Caso 1: non è ok, o è?

Il caso 1 è interessante, perché dal punto di vista del codice, tutto sembra conforme a LSP e molti umani sono tolleranti a un aggiornamento più lento, quindi non sembra importare molto.

Ma dal punto di vista del contratto non è affatto ok: il ridisegno immediato è un postcondition e il sottotipo non può indebolirlo! Quindi questo non è compatibile con LSP.

Per illustrare il problema. Immagina che Base e Component facciano parte di un robot chirurgico e che Base sia un braccio robotico e Component un braccio dotato di un bisturi. Se la garanzia fornita al chirurgo è che l'interfaccia utente con l'immagine della telecamera ottiene un aggiornamento in tempo reale di ogni movimento, Component potrebbe comportare la morte di un paziente. Quindi è meglio prendere in seria considerazione le postcondizioni!

    
risposta data 20.01.2018 - 13:33
fonte
3

Risponderò alle tue domande in ordine inverso. Da 2)

When I substitute with Cyborg, the test will pass. I.e. the behaviour changed. Why is that allowed?

Perché il principio di sostituzione non richiede che il comportamento sia invariato.

La sostituzione di Liskov ha quattro condizioni comportamentali

  • Preconditions cannot be strengthened in a subtype.
  • Postconditions cannot be weakened in a subtype.
  • Invariants of the supertype must be preserved in a subtype.
  • History constraint (the "history rule"). Objects are regarded as being modifiable only through their methods (encapsulation). Because subtypes may introduce methods that are not present in the supertype, the introduction of these methods may allow state changes in the subtype that are not permissible in the supertype. The history constraint prohibits this. It was the novel element introduced by Liskov and Wing. A violation of this constraint can be exemplified by defining a mutable point as a subtype of an immutable point. This is a violation of the history constraint, because in the history of the immutable point, the state is always the same after creation, so it cannot include the history of a mutable point in general. Fields added to the subtype may however be safely modified because they are not observable through the supertype methods. Thus, one can derive a circle with fixed center but mutable radius from immutable point without violating LSP.

Wikipedia: Liskov substitution principle

L'obiettivo di queste quattro condizioni comportamentali è che il codice client che utilizza questi oggetti non deve essere modificato a causa della sostituzione. L'oggetto sostituito deve essere proprio come, se non di più, tollerante di ciò che il codice client si aspetta, non meno. Ciò non significa che l'oggetto sostituito non possa fare ciò che vuole. Non è possibile forzare il codice client a gestire nuovi problemi. Il codice cliente deve essere in grado di non sapere o preoccuparsi della sostituzione.

Ecco perché direi anche che 1) non è una violazione. Non c'è nulla di nuovo qui che il codice cliente dovrebbe preoccuparsi.

    
risposta data 20.01.2018 - 12:07
fonte
-3

Liskov si applica alla semplice ereditarietà, non al polimorfismo. Liskov afferma che le proprietà e i metodi esistenti in una classe base dovrebbero essere ancora disponibili in qualsiasi discendente.

Con il polimorfismo (il tuo metodo virtuale o astratto), il comportamento sarà e dovrebbe deviare. Questo è il punto. Logicamente anche se non c'è alcun problema: il metodo soddisfa lo stesso scopo in un contesto diverso. Qualsiasi test dovrebbe verificare se lo schermo è stato ridisegnato in modo appropriato, non se le azioni eseguite sono esattamente le stesse in entrambi i casi.

    
risposta data 20.01.2018 - 15:18
fonte