web-dev-qa-db-fra.com

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

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.

90
pencilCake

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

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

Vous ne pouvez par exemple 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 jette ArgumentNullException si un argument est manquant et que la sous-classe permet à l'argument d'être nul, également une violation LSP.

Voici un exemple d'une 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 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 casserait le principe Open/Closed et doit être implémenté partout (et donc génère toujours du code instable).

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

Mise à jour

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

La solution avec l'activation du canard à l'intérieur de la méthode Swim peut avoir des effets secondaires lorsque vous travaillez avec l'implémentation réelle (ElectricDuck). Mais cela peut être résolu en utilisant un implémentation d'interface explicite . à mon humble avis, il est plus probable que vous rencontriez des problèmes en ne l'activant PAS dans Swim car il est prévu qu'il nagera lors de l'utilisation de l'interface IDuck

Mise à jour 2

Reformulé certaines parties pour le rendre plus clair.

125
jgauffin

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 nous avons 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 AUX CLIENTS HYPOTHÉCAIRES:

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 CLIENTS 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épendance pour qu'elle reste simple car elle devient déjà compliquée 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 CustomerServiceManagerclass appellera uniquement la méthode GetCustomerDetails, passera le nom de famille et ne devrait pas se soucier de la manière et de l'origine des données.

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

8
Yawar Murtaza