Casos especiais com fallbacks violam o Princípio de Substituição de Liskov?


20

Digamos que eu tenha uma interface FooInterfaceque tenha a seguinte assinatura:

interface FooInterface {
    public function doSomething(SomethingInterface something);
}

E uma classe concreta ConcreteFooque implementa essa interface:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

Eu gostaria ConcreteFoo::doSomething()de fazer algo único se ele passou por um tipo especial de SomethingInterfaceobjeto (digamos que seja chamado SpecialSomething).

Definitivamente, é uma violação do LSP se eu fortalecer as condições prévias do método ou lançar uma nova exceção, mas ainda assim seria uma violação do LSP se eu fiz um caso especial de SpecialSomethingobjetos enquanto fornecia um substituto para SomethingInterfaceobjetos genéricos ? Algo como:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}

Respostas:


19

Ele pode ser uma violação da LSP, dependendo do que mais está no contrato para o doSomethingmétodo. Mas é quase certamente um cheiro de código, mesmo que não viole o LSP.

Por exemplo, se parte do contrato de doSomethingé que ele ligará something.commitUpdates()pelo menos uma vez antes de retornar e, no caso especial que ele chama commitSpecialUpdates(), isso é uma violação do LSP. Mesmo que SpecialSomethingo commitSpecialUpdates()método tenha sido conscientemente projetado para fazer as mesmas coisas commitUpdates(), isso é apenas invasão preventiva à violação do LSP e é exatamente o tipo de hackeamento que não seria necessário fazer se seguisse o LSP de forma consistente. Se algo assim se aplica ao seu caso é algo que você precisará descobrir verificando seu contrato para esse método (explícito ou implícito).

A razão pela qual esse é um cheiro de código é porque a verificação do tipo concreto de um de seus argumentos perde o sentido de definir um tipo de interface / abstrato para ele em primeiro lugar, e porque, em princípio, você não pode mais garantir que o método funcione (imagine se alguém escreve uma subclasse de SpecialSomethingcom a suposição de que commitUpdates()será chamado). Primeiro, tente fazer essas atualizações especiais funcionarem dentro dosSomethingInterface; esse é o melhor resultado possível. Se você realmente tem certeza de que não pode fazer isso, precisa atualizar a interface. Se você não controlar a interface, considere escrever sua própria interface que faça o que você deseja. Se você não consegue nem criar uma interface que funcione para todos eles, talvez deva descartar completamente a interface e ter vários métodos usando diferentes tipos de concreto, ou talvez um refator ainda maior esteja em ordem. Teríamos que saber mais sobre a mágica que você comentou para saber qual delas é apropriada.


Obrigado! Isso ajuda. Hipoteticamente, o objetivo do doSomething()método seria a conversão de tipo para SpecialSomething: se receber SpecialSomething, retornaria o objeto sem modificação, enquanto que, se receber um SomethingInterfaceobjeto genérico , executaria um algoritmo para convertê-lo em um SpecialSomethingobjeto. Como as pré-condições e pós-condições permanecem as mesmas, não acredito que o contrato tenha sido violado.
Evan

1
@Evan Oh uau ... esse é um caso interessante. Isso pode ser completamente sem problemas. A única coisa em que consigo pensar é que, se você está retornando o objeto existente em vez de construir um novo objeto, talvez alguém dependa desse método para retornar um novo objeto ... mas se esse tipo de coisa pode quebrar as pessoas pode depender o idioma. É possível alguém ligar y = doSomething(x)então x.setFoo(3)e descobrir que y.getFoo()retorna 3?
Ixrec 30/12/15

Isso é problemático dependendo do idioma, embora deva ser facilmente contornado, basta retornar uma cópia do SpecialSomethingobjeto. Embora, por uma questão de pureza, eu também pude ver renunciar à otimização de caso especial quando o objeto passou SpecialSomethinge apenas executá-lo através do algoritmo de conversão maior, uma vez que ainda deve funcionar porque também é SomethingInterfaceobjeto.
Evan

1

Não é uma violação do LSP. No entanto, ainda é uma violação da regra "não faça verificações de tipo". Seria melhor projetar o código de forma que o especial surja naturalmente; talvez SomethingInterfaceprecise de outro membro que possa fazer isso, ou talvez você precise injetar uma fábrica abstrata em algum lugar.

No entanto, essa não é uma regra rígida e rápida, portanto, você precisa decidir se a troca vale a pena. No momento, você tem um cheiro de código e um possível obstáculo para aprimoramentos futuros. Livrar-se disso pode significar uma arquitetura consideravelmente mais complicada. Sem mais informações, não sei dizer qual é o melhor.


1

Não, aproveitar o fato de que um determinado argumento não fornece apenas a interface A, mas também A2, não viola o LSP.

Apenas certifique-se de que o caminho especial não tenha pré-condições mais fortes (além das testadas na decisão de adotá-lo), nem pós-condições mais fracas.

Os modelos C ++ geralmente fazem isso para fornecer melhor desempenho, por exemplo, exigindo InputIterators, mas fornecendo garantias adicionais se chamados com RandomAccessIterators.

Se você precisar decidir em tempo de execução (por exemplo, usando a conversão dinâmica), tome cuidado com a decisão de qual caminho seguir consumindo todos os seus ganhos potenciais ou até mais.

Tirar proveito do caso especial geralmente é contra o DRY (não se repita), pois você pode precisar duplicar o código e contra o KISS (Keep it Simple), pois é mais complexo.


0

Há uma troca entre o princípio de "não faça verificações de tipo" e "segregar suas interfaces". Se muitas classes fornecem um meio viável, mas possivelmente ineficiente, de executar algumas tarefas, e algumas delas também podem oferecer um meio melhor, e há uma necessidade de código que possa aceitar qualquer categoria mais ampla de itens que possam executar a tarefa. (talvez ineficientemente), mas, em seguida, execute a tarefa da maneira mais eficiente possível, será necessário que todos os objetos implementem uma interface que inclua um membro para dizer se o método mais eficiente é suportado e outro para usá-lo, se for, ou então faça com que o código que recebe o objeto verifique se ele suporta uma interface estendida e, se for o caso, faça a conversão.

Pessoalmente, sou a favor da abordagem anterior, embora deseje que estruturas orientadas a objetos como o .NET permitam que interfaces especifiquem métodos padrão (tornando interfaces maiores menos dolorosas de se trabalhar). Se a interface comum incluir métodos opcionais, uma única classe de wrapper poderá manipular objetos com muitas combinações diferentes de habilidades, enquanto promete aos consumidores apenas as habilidades presentes no objeto original. Se muitas funções forem divididas em interfaces diferentes, será necessário um objeto wrapper diferente para cada combinação diferente de interfaces que os objetos quebrados possam precisar suportar.


0

O princípio de substituição de Liskov é sobre subtipos que agem de acordo com um contrato de seu supertipo. Portanto, como o Ixrec escreveu, não há informações suficientes para responder se é uma violação do LSP.

O que é violado aqui é um princípio fechado aberto. Se você tem um novo requisito - faça mágica de SpecialSomething - e precisa modificar o código existente, definitivamente está violando o OCP .

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.