Violazione del principio di sostituzione di Liskov?

1

Ho una classe di animali

class Animal
{
    public function eat(Food $food);
}

la sottoclasse che l'eredita in realtà non può supportare tutti i tipi di cibo (il gatto può mangiare solo carne):

class Cat extends Animal
{
    public function eat(Food $food)
    {
        if (!$food instanceof Meat) throw new InvalidArgumentException();
    }
}

ovviamente, Meat è una sottoclasse di Food

Quindi questo codice viola LSP (penso che lo faccia)? e come riprogettarlo?

====================================

PS. La descrizione di cui sopra è una versione modificata. la versione originale è come di seguito:

Ho definito un'interfaccia trasformatore di dati

interface TransformerInterface
{
    /**
     * @return mixed
     */
    public function transform($origin); 
}

Come si può vedere, $ origine e il tipo restituito possono essere qualsiasi tipo di dati (io uso PHP), tuttavia, la classe che implementa in realtà non può supportare tutti i tipi di dati, (penso che dovrebbe essere OK se restituisce determinati tipi di dati, non viola LSP):

class TagTransformer implements TransformerInterface
{
    public function transform($origin)
    {
        if (!is_string($origin)) throw new InvalidArgumentException();
        ...
    }
}

Quindi questo codice viola LSP (penso che lo faccia)? e come riprogettarlo?

    
posta chrisyue 08.05.2018 - 02:17
fonte

4 risposte

5

LSP:

Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T.

Che cosa è dimostrabile su Animal ? non fornisci codice o tipo di reso, presumibilmente qualcosa come:

(Animal.Hungry = false AND Food.State = Consumed) OR InvalidArgumentException thrown

È ancora vero per Cat ? Sì. (supponendo che tu aggiunga le modifiche di stato)

Che ne dici di Food e Meat ? Non provi alcun codice, quindi possiamo presumere che l'unica differenza sia il Tipo. Nessuna violazione possibile.

Che dire dell'istruzione di controllo del tipo in Cat ? sembra male, non è una violazione di LSP, ma solleva dubbi sul fatto che Food abbia una violazione in esso. Se così non fosse, non avrei bisogno di controllare il tipo giusto?

In realtà l'odore del codice indica che Food dovrebbe avere una sorta di proprietà FoodType o CanBeEatenBy(Animal animal) , piuttosto che usare Sottotipi per identificare il tipo.

Modifica: sembra che il progetto corretto sia in ambito ...

Userò c #, che ha un "difetto" in quanto non puoi specificare quali eccezioni potrebbero essere lanciate nella definizione della classe. Stando così le cose, e rispettando la normale regola di non usare le eccezioni per la logica, cercherò di evitare tutte le eccezioni.

Ovviamente questo non è l'unico modo per farlo, è solo un modo per evitare il tuo odore di codice e problemi con le eccezioni.

public enum AnimalType
{
    Omnivore,
    Herbivore,
    Carnivore
}

public class Food
{
    public bool IsEdibleBy(Animal a)
    {
        return true;
    }
}

public class EatenFood
{
    public override bool IsEdibleBy(Animal a)
    {
        return false;
    }
}

public class Animal
{
    public readonly AnimalType Type = AnimalType.Omnivore;
    public Food Eat(Food f)
    {
         if(f.IsEdibleBy(this))
         {
              return new EatenFood(f);
         }
         else
         {
              return f;
         }
    }
}

Che cosa è dimostrabile?

Eat always returns a type of Food 
IsEdibleBy always returns true or false
AnimalType is one of the enum values

Ora aggiungiamo Cat:

public class Cat : Animal
{
    public Cat() { this.Type = AnimalType.Carnivore; }
}

Public class Meat : Food
{
   public override bool IsEdibleBy(Animal a)
   {
       return a.Type == AnimalType.Omnivore || 
              a.Type == AnimalType.Carnivore;
   }
}

Ora hai evitato tutte le eccezioni e le domande LSP, puoi estendere i tuoi tipi di cibo a tempo indeterminato, anche se potresti avere problemi se aggiungi altre enumerazioni AnimalType. Sarebbe importante assicurarsi che quell'elenco sia completo all'inizio

    
risposta data 08.05.2018 - 11:27
fonte
7

Note
By the time I wrote my answer, you had changed your example from an interface implementation to a base class inheritance. My answer is still correct; since the LSP applies to interfaces and base classes as per this StackOverflow answer.

You did change more in the example than just inheritance/interface, which does influence the answer. I've addressed this at the bottom of this answer.

Questa non è una violazione dell'LSP.

Sarebbe una violazione se il metodo è completamente inutilizzabile, cioè non dovresti mai chiamare il metodo TagTransformer.transform .

Esempi della differenza.

Scusa la sintassi C #, è passato un po 'di tempo da quando eseguivo PHP.

public interface ICalculation
{
    void SetFirstNumber(int num);
    void SetSecondNumber(int num);
    int CalculateOutputFromNumbers();
}

Implementazione 1:

public class Addition : ICalculation
{
    public void SetFirstNumber(int num)
    {
        this.Number1 = num;
    }

    public void SetSecondNumber(int num)
    {
        this.Number2 = num;
    }

    public int CalculateOutputFromNumbers()
    {
        return this.Number1 + this.Number2;
    }
}

Questo sta chiaramente utilizzando l'interfaccia come previsto. Nessun problema qui.

Implementazione 2:

public class SquareRoot: ICalculation
{
    public void SetFirstNumber(int num)
    {
        this.Number1 = num;
    }

    public void SetSecondNumber(int num)
    {
        throw new Exception("Square roots only take one input value!);
    }

    public int CalculateOutputFromNumbers()
    {
        return Math.Sqrt(this.Number1);
    }
}

Tieni in considerazione il modo in cui mai potrà chiamare il metodo SetSecondNumber da SquareRoot , anche se il metodo fa parte dell'interfaccia ICalculation e SquareRoot implementa% % co_de.

Ciò viola l'LSP. Per calcolare una radice quadrata, la classe ICalculation deve essere trattata in modo diverso rispetto alle altre classi di implementazione di SquareRoot .

Implementazione 3:

public class Division : ICalculation
{
    public void SetFirstNumber(int num)
    {
        this.Number1 = num;
    }

    public void SetSecondNumber(int num)
    {
        if(num == 0)
            throw new Exception("You can't divide by zero!"); 

        this.Number2 = num;
    }

    public int CalculateOutputFromNumbers()
    {
        return this.Number1 / this.Number2;
    }
}

In base alla tua domanda, sembra che ritieni che si tratti di una violazione di LSP. Questo è essenzialmente ciò che sta accadendo nel tuo esempio di codice, un'eccezione specifica viene lanciata per un dato valore non valido.

Questa non è una violazione. Osserva come è permesso chiamare il metodo ICalculation da SetSecondNumber , ma semplicemente non puoi usare un valore impossibile (0).

Non si tratta di dover utilizzare la classe Division in modo diverso da Division -le classi di implementazione; è semplicemente una questione di input errato, nessun output possibile .

Questo è significativamente diverso dall'esempio ICalculation ; almeno in relazione a LSP.

In risposta al tuo nuovo esempio

Il tuo nuovo esempio in un modo invalida la tua domanda originale.

Il tuo vecchio esempio era uno snippet PHP:

public function transform($origin)
{
    if (!is_string($origin)) throw new InvalidArgumentException();
    ...
}

È importante notare che non esiste alcun vincolo di tipo su SquareRoot . Ciò significa che il controllo di un tipo utilizzabile è una conseguenza logica e non un design intrinsecamente sbagliato (poiché il linguaggio consente parametri non tipizzati).

Tuttavia, questo è significativamente diverso nel tuo esempio rivisto:

public function eat(Food $food)
{
    if ($food instanceof Meat) throw new InvalidArgumentException();
    ...
}

È importante notare che esiste un vincolo di tipo su $origin . Non stai più utilizzando un parametro senza tipo. Stai specificando che il parametro è di tipo $food .

A questo punto, diventa una violazione LSP. La validazione dell'input non è più una questione di come funziona la lingua; ma piuttosto una conseguenza del contratto che è specificata dalla tua classe base Food .

Stai provando a creare un Animal che eredita da Cat ma in realtà (parzialmente) completamente rifiuta di implementare Animal . Questa è un'intenzionale esclusione della funzionalità, che ne fa una violazione LSP.

Prenderò in considerazione questa violazione di LSP di Eat(Food) / Meat , più di quanto non sia una violazione LSP di Food / Cat . La carne viene chiaramente trattata in modo diverso rispetto al cibo, il che viola il contratto secondo il quale la carne è un alimento.

    
risposta data 08.05.2018 - 10:41
fonte
2

Se è documentato che non ogni tipo di oggetto è adatto per ogni sottoclasse / implementazione - e che passare un argomento non idoneo può dare come risultato un InvalidArgumentException - allora sei semi-OK. Hai introdotto questa restrizione nella superclasse / interfaccia, in modo da non infrangere le regole applicandola in sottoclassi. Stai forzando una violazione DIP sul chiamante, ma in sostanza hai soddisfatto LSP.

Altrimenti, il fatto che $animal->eat($broccoli) potrebbe interrompersi e non puoi sapere che senza conoscere il sottotipo, significa che stai violando LSP. I metodi hanno esito positivo per impostazione predefinita. (Le eccezioni sono chiamate "eccezioni" per un motivo.) Quindi una dichiarazione di eat sans condizioni di errore è una promessa che $animal->eat($food) avrà almeno successo. Cat non è promettente.

Un modo per risolvere entrambi i problemi è fornire un metodo can_eat su Animal e dichiarare che se $animal->can_eat($food) è falso, allora $animal->eat($food) avrà (probabilmente) esito negativo.

    
risposta data 08.05.2018 - 16:56
fonte
1

L'esempio di animale è una violazione che può essere risolta facilmente. Non menzionare il cibo nella classe base, fallo così: animale astratto - > mangiatore astratto - > astract MeatEater - > Cat

L'esempio del trasformatore è OK. È ovvio che qualsiasi particolare trasformatore non sarà in grado di trasformare qualcosa in qualcos'altro, si limita a dire che può trasformare qualcosa in qualcos'altro, che presenta un metodo di trasformazione (polimorfico).

    
risposta data 09.05.2018 - 07:33
fonte

Leggi altre domande sui tag