Você pode explicar o Princípio de Substituição de Liskov com um bom exemplo C #? [fechadas]


91

Você pode explicar o Princípio de Substituição de Liskov (O 'L' de SOLID) com um bom exemplo C # cobrindo todos os aspectos do princípio de uma forma simplificada? Se for realmente possível.


9
Aqui está uma maneira simplificada de pensar sobre isso em poucas palavras: Se eu seguir o LSP, posso substituir qualquer objeto em meu código por um objeto Mock, e o nada no código de chamada precisaria ser ajustado ou alterado para levar em conta a substituição. O LSP é um suporte fundamental para o padrão Test by Mock.
kmote

Existem mais alguns exemplos de conformidade e violações nesta resposta
StuartLC

Respostas:


128

(Esta resposta foi reescrita em 13/05/2013, leia a discussão na parte inferior dos comentários)

LSP trata de seguir o contrato da classe base.

Você pode, por exemplo, não lançar novas exceções nas subclasses, pois aquele que usa a classe base não esperaria isso. O mesmo é válido se a classe base lançar ArgumentNullExceptionse um argumento estiver faltando e a subclasse permitir que o argumento seja nulo, também uma violação do LSP.

Aqui está um exemplo de uma estrutura de classe que viola o 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; } }
}

E o código de chamada

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

Como você pode ver, existem dois exemplos de patos. Um pato orgânico e um pato elétrico. O pato elétrico só pode nadar se estiver ligado. Isso quebra o princípio LSP, uma vez que deve ser ativado para poder nadar, pois IsSwimming(que também faz parte do contrato) não será definido como na classe base.

Claro, você pode resolvê-lo fazendo algo assim

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

Mas isso quebraria o princípio Aberto / Fechado e tem que ser implementado em todos os lugares (e portanto ainda gera código instável).

A solução adequada seria ligar automaticamente o pato no Swimmétodo e, ao fazer isso, fazer com que o pato elétrico se comporte exatamente como definido pela IDuckinterface

Atualizar

Alguém adicionou um comentário e o removeu. Tinha um ponto válido que eu gostaria de abordar:

A solução com ativar o pato dentro do Swimmétodo pode ter efeitos colaterais ao trabalhar com a implementação real ( ElectricDuck). Mas isso pode ser resolvido usando uma implementação de interface explícita . imho é mais provável que você tenha problemas ao NÃO ligá-lo, Swimpois é esperado que ele nade ao usar a IDuckinterface

Atualização 2

Reformulei algumas partes para ficar mais claro.


1
@jgauffin: o exemplo é simples e claro. Mas a solução que você propõe, primeiro: quebra o Princípio Aberto-Fechado e não se encaixa na definição do Tio Bob (veja a parte de conclusão de seu artigo) que escreve: "O Princípio de Substituição de Liskov (também conhecido como Design por Contrato) é uma característica importante de todos os programas que estão em conformidade com o princípio Aberto-Fechado. " consulte: objectmentor.com/resources/articles/lsp.pdf
pencilCake

1
Não vejo como a solução é aberta / fechada. Leia minha resposta novamente se você estiver se referindo à if duck is ElectricDuckparte. Tive um seminário sobre SOLID na quinta-feira passada :)
jgauffin

Não exatamente no tópico, mas você poderia, por favor, mudar seu exemplo para não fazer a verificação de tipo duas vezes? Muitos desenvolvedores não estão cientes da aspalavra - chave, o que realmente os salva de muitas verificações de tipo. Estou pensando algo como o seguinte:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers

3
@jgauffin - Estou um pouco confuso com o exemplo. Achei que o Princípio de Substituição de Liskov ainda seria válido neste caso, porque Duck e ElectricDuck derivam de IDuck e você pode colocar um ElectricDuck ou Duck em qualquer lugar que IDuck seja usado. Se o ElectricDuck tiver que ligar o antes que o pato possa nadar, isso não é responsabilidade do ElectricDuck ou de algum código que instancie o ElectricDuck e defina a propriedade IsTurnedOn como true. Se isso violar o LSP, parece que seria muito difícil aderir ao LSV, pois todas as interfaces conteriam lógicas diferentes para seus métodos.
Xaisoft

1
@MystereMan: imho LSP tem tudo a ver com correção comportamental. Com o exemplo do retângulo / quadrado, você obtém o efeito colateral da outra propriedade que está sendo definida. Com o pato você tem o efeito colateral de não nadar. 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 uma abordagem prática

Em todos os lugares que procuro por exemplos de C # do LSP, as pessoas usaram classes e interfaces imaginárias. Aqui está a implementação prática do LSP que implementei em um de nossos sistemas.

Cenário: suponha que temos 3 bancos de dados (clientes hipotecários, clientes de contas correntes e clientes de contas poupança) que fornecem dados do cliente e precisamos dos detalhes do cliente para o sobrenome do cliente fornecido. Agora podemos obter mais de 1 detalhe do cliente desses 3 bancos de dados com o sobrenome fornecido.

Implementação:

CAMADA DE MODELO DE NEGÓCIO:

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

CAMADA DE ACESSO DE DADOS:

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

A interface acima é implementada pela classe abstrata

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
    }
}

Esta classe abstrata tem um método comum "GetDetails" para todos os 3 bancos de dados que é estendido por cada uma das classes de banco de dados conforme mostrado abaixo

ACESSO A DADOS DO CLIENTE DE HIPOTECA:

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

ACESSO DE DADOS DO CLIENTE DA CONTA ATUAL:

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

ACESSO A DADOS DO CLIENTE NA CONTA DE ECONOMIA:

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

Uma vez que essas 3 classes de acesso a dados são definidas, agora chamamos nossa atenção para o cliente. Na camada Business, temos a classe CustomerServiceManager que retorna os detalhes do cliente para seus clientes.

CAMADA DE NEGÓCIOS:

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;
   }
}

Não mostrei a injeção de dependência para mantê-la simples, pois já está ficando complicada.

Agora, se tivermos um novo banco de dados de detalhes do cliente, podemos simplesmente adicionar uma nova classe que estende BaseDataAccess e fornece seu objeto de banco de dados.

Claro, precisamos de procedimentos armazenados idênticos em todos os bancos de dados participantes.

Por fim, o cliente da CustomerServiceManagerclasse só chamará o método GetCustomerDetails, passará o lastName e não se preocupará com como e de onde os dados estão vindo.

Espero que isso lhe dê uma abordagem prática para entender o LSP.


3
Como isso pode ser um exemplo de LSP?
somegeek

1
Também não vejo o exemplo do LSP nisso ... Por que tem tantos votos positivos?
StaNov

1
@RoshanGhangare IDataAccess possui 3 implementações concretas que podem ser substituídas na Camada de Negócios.
Yawar Murtaza

1
@YawarMurtaza tudo o que você exemplifica e cita é uma implementação típica de um padrão de estratégia. Você pode deixar claro onde está quebrando o LSP e como você resolve essa violação do LSP
Yogesh

0

Aqui está o código para aplicar o Princípio do Substituto 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 afirma: "As classes derivadas devem ser substituíveis por suas classes básicas (ou interfaces)" & "Métodos que usam referências a classes básicas (ou interfaces) devem ser capazes de usar métodos das classes derivadas sem saber sobre isso ou sem conhecer os detalhes . "

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.