95 votes

Pouvez-vous expliquer le principe de substitution de Liskov avec un bon exemple en C# ?

Pouvez-vous expliquer le principe de substitution de Liskov (le "L" de SOLID) avec un bon exemple en C# couvrant tous les aspects du principe de manière simplifiée ? Si c'est vraiment possible.

10 votes

Voici une façon simplifiée d'y penser en quelques mots : Si je suis LSP, je peux remplacer n'importe quel objet dans mon code par un objet Mock, et rien dans le code appelant ne devra être ajusté ou modifié pour tenir compte de la substitution. LSP est un support fondamental pour le pattern Test by Mock.

0 votes

Il y a d'autres exemples de conformité et de violations dans cette réponse

131voto

jgauffin Points 51913

(Cette réponse a été réécrite le 2013-05-13, lisez la discussion au bas des commentaires)

LSP consiste à suivre le contrat de la classe de base.

Vous pouvez par exemple ne pas lancer de nouvelles exceptions dans les sous-classes car l'utilisateur de la classe de base ne s'y attendrait pas. Il en va de même si la classe de base lance ArgumentNullException si un argument est manquant et que la sous-classe permet à l'argument d'être nul, il y a également une violation de la LSP.

Voici un exemple de structure de classe qui viole la LSP :

public interface IDuck
{
   void Swim();
   // contract says that IsSwimming should be true if Swim has been called.
   bool IsSwimming { get; }
}

public class OrganicDuck : IDuck
{
   public void Swim()
   {
      //do something to swim
   }

   bool IsSwimming { get { /* return if the duck is swimming */ } }
}

public class ElectricDuck : IDuck
{
   bool _isSwimming;

   public void Swim()
   {
      if (!IsTurnedOn)
        return;

      _isSwimming = true;
      //swim logic            
   }

   bool IsSwimming { get { return _isSwimming; } }
}

Et le code d'appel

void MakeDuckSwim(IDuck duck)
{
    duck.Swim();
}

Comme vous pouvez le voir, il y a deux exemples de canards. Un canard biologique et un canard électrique. Le canard électrique ne peut nager que s'il est allumé. Cela brise le principe LSP puisqu'il doit être allumé pour pouvoir nager, comme le fait le canard électrique. IsSwimming (qui fait également partie du contrat) ne sera pas défini comme dans la classe de base.

Vous pouvez bien sûr résoudre ce problème en faisant quelque chose comme ceci

void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();
    duck.Swim();
}

Mais cela briserait le principe d'ouverture/fermeture et devrait être implémenté partout (et donc générer du code instable).

La bonne solution serait d'allumer automatiquement le canard dans le cadre de la Swim et, ce faisant, faire en sorte que le canard électrique se comporte exactement comme défini par la méthode IDuck interface

Mise à jour

Quelqu'un a ajouté un commentaire et l'a supprimé. Il contenait un point valable que j'aimerais aborder :

La solution avec l'activation du canard à l'intérieur du Swim peut avoir des effets secondaires lorsqu'on travaille avec l'implémentation réelle ( ElectricDuck ). Mais cela peut être résolu en utilisant un mise en œuvre explicite de l'interface . il est plus probable que vous rencontriez des problèmes en NE l'allumant PAS en Swim puisqu'on s'attend à ce qu'il nage lors de l'utilisation de l'option IDuck interface

Mise à jour 2

J'ai reformulé certaines parties pour les rendre plus claires.

1 votes

@jgauffin : L'exemple est simple et clair. Mais la solution que vous proposez, tout d'abord : brise le principe d'ouverture et de fermeture et ne correspond pas à la définition de l'Oncle Bob (voir la partie conclusion de son article) qui écrit : "Le principe de substitution de Liskov (aussi appelé conception par contrat) est une caractéristique importante de tous les programmes qui se conforment au principe d'ouverture et de fermeture" voir : objectmentor.com/resources/articles/lsp.pdf

1 votes

Je ne vois pas en quoi la solution rompt l'ouverture/fermeture. Relisez ma réponse si vous faites référence à l'option if duck is ElectricDuck partie. J'ai eu un séminaire sur SOLID jeudi dernier :)

0 votes

Ce n'est pas vraiment le sujet, mais pourriez-vous modifier votre exemple afin de ne pas effectuer deux fois le contrôle de type ? Beaucoup de développeurs ne sont pas au courant de l'existence de l'outil de vérification des types. as ce qui leur évite un grand nombre de vérifications de type. Je pense à quelque chose comme ce qui suit : if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();

8voto

Yawar Murtaza Points 1282

LSP : une approche pratique

Partout où je cherche des exemples C# de LSP, les gens ont utilisé des classes et des interfaces imaginaires. Voici l'implémentation pratique de LSP que j'ai mise en place dans un de nos systèmes.

Scénario : Supposons que nous disposions de 3 bases de données (clients des prêts hypothécaires, clients des comptes courants et clients des comptes d'épargne) qui fournissent des données sur les clients et que nous ayons besoin de détails sur le nom de famille d'un client donné. Il se peut que nous obtenions plus d'un détail sur le client à partir de ces 3 bases de données pour un nom de famille donné.

Mise en œuvre :

COUCHE DE MODÈLE D'ENTREPRISE :

public class Customer
{
    // customer detail properties...
}

COUCHE D'ACCÈS AUX DONNÉES :

public interface IDataAccess
{
    Customer GetDetails(string lastName);
}

L'interface ci-dessus est implémentée par la classe abstraite

public abstract class BaseDataAccess : IDataAccess
{
    /// <summary> Enterprise library data block Database object. </summary>
    public Database Database;

    public Customer GetDetails(string lastName)
    {
        // use the database object to call the stored procedure to retrieve the customer details
    }
}

Cette classe abstraite a une méthode commune "GetDetails" pour les 3 bases de données qui est étendue par chacune des classes de base de données comme indiqué ci-dessous.

L'ACCÈS AUX DONNÉES DES CLIENTS HYPOTHÉCAIRES :

public class MortgageCustomerDataAccess : BaseDataAccess
{
    public MortgageCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetMortgageCustomerDatabase();
    }
}

ACCÈS AUX DONNÉES DU CLIENT DU COMPTE COURANT :

public class CurrentAccountCustomerDataAccess : BaseDataAccess
{
    public CurrentAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetCurrentAccountCustomerDatabase();
    }
}

ACCÈS AUX DONNÉES DES CLIENTS DES COMPTES D'ÉPARGNE :

public class SavingsAccountCustomerDataAccess : BaseDataAccess
{
    public SavingsAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetSavingsAccountCustomerDatabase();
    }
}

Une fois que ces 3 classes d'accès aux données sont définies, nous portons maintenant notre attention sur le client. Dans la couche Business, nous avons la classe CustomerServiceManager qui retourne les détails du client à ses clients.

LA COUCHE COMMERCIALE :

public class CustomerServiceManager : ICustomerServiceManager, BaseServiceManager
{
   public IEnumerable<Customer> GetCustomerDetails(string lastName)
   {
        IEnumerable<IDataAccess> dataAccess = new List<IDataAccess>()
        {
            new MortgageCustomerDataAccess(new DatabaseFactory()), 
            new CurrentAccountCustomerDataAccess(new DatabaseFactory()),
            new SavingsAccountCustomerDataAccess(new DatabaseFactory())
        };

        IList<Customer> customers = new List<Customer>();

       foreach (IDataAccess nextDataAccess in dataAccess)
       {
            Customer customerDetail = nextDataAccess.GetDetails(lastName);
            customers.Add(customerDetail);
       }

        return customers;
   }
}

Je n'ai pas montré l'injection de dépendance pour rester simple car cela devient déjà compliqué maintenant.

Maintenant, si nous avons une nouvelle base de données de détails sur les clients, nous pouvons simplement ajouter une nouvelle classe qui étend BaseDataAccess et fournit son objet de base de données.

Bien sûr, nous avons besoin de procédures stockées identiques dans toutes les bases de données participantes.

Enfin, le client de CustomerServiceManager n'appelle que la méthode GetCustomerDetails, transmet le nom de famille et ne se soucie pas de savoir comment et d'où proviennent les données.

J'espère que cela vous donnera une approche pratique pour comprendre le LSP.

1voto

Voici le code pour appliquer le principe de substitution de Liskov.

public abstract class Fruit
{
    public abstract string GetColor();
}

public class Orange : Fruit
{
    public override string GetColor()
    {
        return "Orange Color";
    }
}

public class Apple : Fruit
{
    public override string GetColor()
    {
        return "Red color";
    }
}

class Program
{
    static void Main(string[] args)
    {
        Fruit fruit = new Orange();

        Console.WriteLine(fruit.GetColor());

        fruit = new Apple();

        Console.WriteLine(fruit.GetColor());
    }
}

Le LSV déclare : "Les classes dérivées doivent être substituables à leurs classes de base (ou interfaces)". & "Les méthodes qui utilisent des références aux classes de base (ou interfaces) doivent pouvoir utiliser les méthodes des classes dérivées sans le savoir ou sans en connaître les détails."

-3voto

Throbbo Points 63
public interface IDuck
{
    bool IsTurnedOn { get; set; }
    void Swim();
    void MakeDuckSwim();
}

public class Duck : IDuck
{
    public bool IsTurnedOn
    {
        get { return true; }
        set { value = true; }
    }

    public void Swim()
    {
        //do something to swim
    }
    private void TurnOn() 
    {
        //do nothing already on
    }
    public void MakeDuckSwim() 
    {
        Swim();
    }
}

public class ElectricDuck : IDuck
{
    public bool IsTurnedOn {get; set;}

    public void Swim()
    {
        if (!IsTurnedOn)
            TurnOn();

        //swim logic  
    }
    private void TurnOn()
    {
        IsTurnedOn = true;
    }
    public void MakeDuckSwim() 
    {
        if (!IsTurnedOn)
            TurnOn();

        Swim();
    }       
}

Alors, cela fonctionnera toujours et suit tous les principes SOLIDES :

    duck.MakeDuckSwim();

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X