Pouvez-vous expliquer le principe de substitution de Liskov avec un bon exemple C #? [fermé]


92

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


9
Voici une façon simplifiée d'y penser en un mot: Si je suis LSP, je peux remplacer n'importe quel objet de mon code par un objet Mock, et le rien dans le code d'appel devrait être ajusté ou changé pour tenir compte de la substitution. LSP est un support fondamental pour le modèle Test by Mock.
kmote

Il y a quelques autres exemples de conformité et de violations dans cette réponse
StuartLC

Réponses:


128

(Cette réponse a été réécrite 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 celle qui utilise la classe de base ne s'y attendrait pas. Il en va de même si la classe de base se lance ArgumentNullExceptionsi un argument est manquant et que la sous-classe autorise l'argument à être nul, également une violation LSP.

Voici un exemple de structure de classe qui viole 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 existe deux exemples de canards. Un canard bio et un canard électrique. Le canard électrique ne peut nager que s'il est allumé. Cela rompt le principe du LSP car il doit être activé pour pouvoir nager car le IsSwimming(qui fait également partie du contrat) ne sera pas défini comme dans la classe de base.

Vous pouvez bien sûr le résoudre en faisant quelque chose comme ça

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

Mais cela briserait le principe Open / Closed et doit être implémenté partout (et donc génère toujours du code instable).

La solution appropriée serait d'allumer automatiquement le canard dans la Swimméthode et, ce faisant, de faire en sorte que le canard électrique se comporte exactement comme défini par l' IDuckinterface

Mettre à jour

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

La solution Swimconsistant à activer le canard à l'intérieur de la méthode peut avoir des effets secondaires lorsque vous travaillez avec l'implémentation réelle ( ElectricDuck). Mais cela peut être résolu en utilisant une implémentation d'interface explicite . à mon humble avis, il est plus probable que vous rencontriez des problèmes en ne l'allumant PAS Swimcar on s'attend à ce qu'il nage lors de l'utilisation de l' IDuckinterface

Mise à jour 2

Reformulé certaines parties pour le rendre plus clair.


1
@jgauffin: L'exemple est simple et clair. Mais la solution que vous proposez, d'abord: rompt le principe ouvert-fermé et ne correspond pas à la définition d'oncle Bob (voir la partie conclusion de son article) qui écrit: «Le principe de substitution de Liskov (AKA Design by Contract) est une caractéristique importante de tous les programmes conformes au principe Ouvert-Fermé. " voir: objectmentor.com/resources/articles/lsp.pdf
PencilCake

1
Je ne vois pas comment la solution ouvre / ferme. Relisez ma réponse si vous faites référence à la if duck is ElectricDuckpièce. J'ai eu un séminaire sur SOLID jeudi dernier :)
jgauffin

Pas vraiment sur le sujet, mais pourriez-vous s'il vous plaît changer votre exemple pour ne pas faire deux fois la vérification de type? De nombreux développeurs ne connaissent pas le asmot - clé, ce qui les évite en fait de nombreuses vérifications de type. Je pense à quelque chose comme ceci:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers

3
@jgauffin - Je suis légèrement confus par l'exemple. Je pensais que le principe de substitution de Liskov serait toujours valable dans ce cas, car Duck et ElectricDuck dérivent tous deux d'IDuck et vous pouvez placer un ElectricDuck ou un Duck partout où IDuck est utilisé. Si ElectricDuck doit s'allumer avant que le canard ne puisse nager, n'est-ce pas la responsabilité d'ElectricDuck ou d'un code instanciant ElectricDuck, puis définissant la propriété IsTurnedOn sur true. Si cela viole LSP, il semble que le LSV serait très difficile à respecter car toutes les interfaces contiendraient une logique différente pour ses méthodes.
Xaisoft

1
@MystereMan: imho LSP est tout au sujet de la correction comportementale. Avec l'exemple rectangle / carré, vous obtenez l'effet secondaire de l'autre propriété définie. Avec le canard, vous obtenez l'effet secondaire de ne pas nager. LSP:if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).
jgauffin

8

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 implémentée dans l'un de nos systèmes.

Scénario: Supposons que nous ayons 3 bases de données (clients hypothécaires, clients des comptes courants et clients des comptes d'épargne) qui fournissent des données client et que nous ayons besoin des détails du client pour le nom de famille du client donné. Maintenant, nous pouvons obtenir plus d'un détail client de ces 3 bases de données par rapport au nom de famille donné.

La mise en oeuvre:

COUCHE DE MODÈLE D'AFFAIRES:

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

ACCÈS AUX DONNÉES DU CLIENT HYPOTHÉCAIRE:

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

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

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

ACCÈS AUX DONNÉES CLIENT DU COMPTE D'ÉPARGNE:

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

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

COUCHE D'AFFAIRES:

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épendances pour rester simple car cela se complique déjà maintenant.

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

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

Enfin, le client pour la CustomerServiceManagerclasse appellera uniquement la méthode GetCustomerDetails, transmettra le lastName et ne devrait pas se soucier de la provenance et de la provenance des données.

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


3
Comment cela peut-il être un exemple de LSP?
somegeek

1
Je ne vois pas non plus l'exemple de LSP là-dedans ... Pourquoi y a-t-il autant de votes positifs?
StaNov

1
@RoshanGhangare IDataAccess a 3 implémentations concrètes qui peuvent être substituées dans la couche métier.
Yawar Murtaza

1
@YawarMurtaza quel que soit l'exemple que vous avez cité, c'est la mise en œuvre typique du modèle de stratégie, c'est tout. Pouvez-vous s'il vous plaît préciser où il brise LSP et comment vous résolvez cette violation de LSP
Yogesh

0

Voici le code pour appliquer le principe de remplacement 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());
    }
}

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 ni connaître les détails . "

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.