O Princípio da Substituição de Liskov é incompatível com Introspecção ou Duck Typing?

11

Entendo corretamente que o Princípio da Substituição de Liskov não pode ser observado em linguagens em que objetos podem se inspecionar, como é habitual em linguagens tipificadas por pato?

Por exemplo, em Ruby, se uma classe Bherda de uma classe A, então para cada objeto xde A, x.classretornará A, mas se xfor um objeto de B, x.classnão retornará A.

Aqui está uma declaração do LSP:

Vamos q (x) ser um provable propriedade sobre objetos x do tipo T . Em seguida, q (Y) deve ser demonstrável para objectos y de tipo S , onde S é um subtipo de T .

Então, em Ruby, por exemplo,

class T; end
class S < T; end

viole o LSP neste formulário, como testemunhado pela propriedade q (x) =x.class.name == 'T'


Adição. Se a resposta for "sim" (LSP incompatível com a introspecção), minha outra pergunta seria: existe alguma forma "fraca" modificada de LSP que pode ser válida para uma linguagem dinâmica, possivelmente sob algumas condições adicionais e apenas com tipos especiais de propriedades .


Atualizar. Para referência, aqui está outra formulação de LSP que encontrei na web:

Funções que usam ponteiros ou referências a classes base devem poder usar objetos de classes derivadas sem conhecê-lo.

E outro:

Se S é um subtipo declarado de T, objetos do tipo S devem se comportar como objetos do tipo T devem se comportar, se forem tratados como objetos do tipo T.

O último é anotado com:

Observe que o LSP tem tudo a ver com o comportamento esperado dos objetos. Só se pode seguir o LSP se estiver claro sobre qual é o comportamento esperado dos objetos.

Isso parece ser mais fraco que o original e pode ser possível observar, mas eu gostaria de vê-lo formalizado, em particular explicado quem decide qual é o comportamento esperado.

O LSP não é uma propriedade de um par de classes em uma linguagem de programação, mas de um par de classes juntamente com um determinado conjunto de propriedades, satisfeito pela classe ancestral? Na prática, isso significaria que, para construir uma subclasse (classe descendente) respeitando o LSP, todos os usos possíveis da classe ancestral devem ser conhecidos? De acordo com o LSP, a classe ancestral deve ser substituída por qualquer classe descendente, certo?


Atualizar. Eu já aceitei a resposta, mas gostaria de adicionar mais um exemplo concreto de Ruby para ilustrar a pergunta. Em Ruby, cada classe é um módulo no sentido de que Classclasse é descendente de Moduleclasse. Contudo:

class C; end
C.is_a?(Module) # => true
C.class # => Class
Class.superclass # => Module

module M; end
M.class # => Module

o = Object.new

o.extend(M) # ok
o.extend(C) # => TypeError: wrong argument type Class (expected Module)
Alexey
fonte
2
Quase todas as linguagens modernas fornecem algum grau de introspecção, então a questão não é realmente específica para Ruby.
Joachim Sauer
Eu entendo, eu dei Ruby apenas como um exemplo. Eu não sei, talvez em algumas outras línguas com introspecção haja algumas "formas fracas" do LSP, mas, se eu entendi o princípio corretamente, é incompatível com introspecção.
Alexey
Eu removi "Ruby" do título.
Alexey
2
A resposta curta é que eles são compatíveis. Aqui está um post de blog que eu maioria deles concorda com: substituição de Liskov para Typing Duck
K.Steff
2
@Alexey As propriedades nesse contexto são invariantes de um objeto. Por exemplo, objetos imutáveis ​​têm a propriedade de que seus valores não são alterados. Se você observar bons testes de unidade, eles deverão testar exatamente essas propriedades.
30912 K 15:40

Respostas:

29

Aqui está o princípio real :

Seja q(x)uma propriedade comprovável sobre objetos xdo tipo T. Em seguida, q(y)deve ser demonstrável para objetos ydo tipo Sonde Sé um subtipo de T.

E o excelente resumo da Wikipedia :

Ele afirma que, em um programa de computador, se S é um subtipo de T, os objetos do tipo T podem ser substituídos por objetos do tipo S (ou seja, objetos do tipo S podem ser substituídos por objetos do tipo T) sem alterar nenhum dos as propriedades desejáveis ​​desse programa (correção, tarefa executada etc.).

E algumas citações relevantes do artigo:

O que é necessário é um requisito mais forte que restrinja o comportamento dos subtipos: as propriedades que podem ser comprovadas usando a especificação do tipo presumido de um objeto devem ser mantidas, mesmo que o objeto seja realmente um membro de um subtipo desse tipo ...

Uma especificação de tipo inclui as seguintes informações:
- O nome do tipo;
- Uma descrição do espaço de valor do tipo;
- Para cada um dos métodos do tipo:
--- Seu nome;
--- Sua assinatura (incluindo exceções sinalizadas);
--- Seu comportamento em termos de pré-condições e pós-condições.

Então, vamos à pergunta:

Entendo corretamente que o Princípio da Substituição de Liskov não pode ser observado em linguagens em que objetos podem se inspecionar, como é habitual em linguagens tipificadas por pato?

Não.

A.classretorna uma classe.
B.classretorna uma classe.

Como você pode fazer a mesma chamada no tipo mais específico e obter um resultado compatível, o LSP é retido. O problema é que, com linguagens dinâmicas, você ainda pode chamar as coisas no resultado, esperando que elas estejam lá.

Mas vamos considerar uma linguagem estaticamente estrutural (tipo pato). Nesse caso, A.classretornaria um tipo com uma restrição de que ele deve ser Aou um subtipo de A. Isso fornece a garantia estática de que qualquer subtipo de Adeve fornecer um método T.classcujo resultado seja um tipo que satisfaça essa restrição.

Isso fornece uma afirmação mais forte que o LSP contém em idiomas que suportam digitação de patos e que qualquer violação do LSP em algo como Ruby ocorre mais devido ao mau uso dinâmico normal do que a uma incompatibilidade de design de idioma.

Telastyn
fonte
1
"Como você pode fazer a mesma chamada no tipo mais específico e obter um resultado compatível, o LSP mantém". LSP mantém se os resultados são idênticos, se eu entendi corretamente. Talvez possa haver alguma forma de "semana" de LSP em relação a determinadas restrições, exigindo, em vez de todas as propriedades, que apenas as restrições fornecidas sejam atendidas. Em qualquer caso, eu apreciaria qualquer referência.
Alexey
@ Alexey editado para incluir o que significa LSP. Se eu posso usar B onde eu espero A, então o LSP aguenta. Estou curioso para saber como você acha que a classe. Ruby possivelmente viola isso.
Telastyn
3
@ Alexey - Se o seu programa contém fail unless x.foo == 42e um subtipo retorna 0, é a mesma coisa. Isso não é uma falha do LSP, é a operação normal do seu programa. Polimorfismo não é uma violação do LSP.
Telastyn
1
@Alexey - Claro. Vamos assumir que é uma propriedade. Nesse caso, seu código viola o LSP, pois não permite que os subtipos tenham o mesmo comportamento semântico. Mas não é particularmente especial para linguagens dinâmicas ou tipadas por pato. Não há nada que eles façam no design do idioma que cause a violação. O código que você escreveu faz. Lembre-se de que o LSP é um princípio de design de programa (portanto, o deveria na definição) e não uma propriedade matemática de programas.
Telastyn
6
@ Alexey: se você escrever algo que depende de x.class == A, então é o seu código que viola o LSP , não o idioma. É possível escrever código que viole o LSP em quase todas as linguagens de programação.
Andres F.
7

No contexto do LSP, uma "propriedade" é algo que pode ser observado em um tipo (ou objeto). Em particular, fala sobre uma "propriedade comprovável".

Essa "propriedade" poderia existir um foo()método que não tem valor de retorno (e segue o contrato estabelecido em sua documentação).

Certifique-se de não confundir esse termo com "propriedade", como em " classé uma propriedade de todos os objetos no Ruby". Essa "propriedade" pode ser uma "propriedade LSP", mas não é automaticamente a mesma!

Agora, a resposta para suas perguntas depende muito de quão rigorosa você define "propriedade". Se você disser "a propriedade da classe Aé que .classretornará o tipo do objeto", Bna verdade ela possui essa propriedade.

Se, no entanto, você define a "propriedade" para ser " .classretornos A", então, obviamente, Bnão não têm essa propriedade.

No entanto, a segunda definição não é muito útil, pois você encontrou uma maneira geral de declarar uma constante.

Joachim Sauer
fonte
Só consigo pensar em uma definição de "propriedade" de um programa: para uma determinada entrada, ele retorna um determinado valor ou, de maneira mais geral, quando usado como um bloco em outro programa, esse outro programa para uma determinada entrada retornará um valor. determinados valores. Com esta definição, não vejo o que significa que " .classretornará o tipo do objeto". Se isso significa x.class == x.class, não é uma propriedade interessante.
Alexey
1
@ Alexey: Atualizei minha pergunta com um esclarecimento sobre o que "propriedade" significa no contexto do LSP.
Joachim Sauer
2
@ Alexey: olhando para o jornal, não encontro uma definição específica ou "propriedade". Provavelmente porque o termo é usado no sentido geral de CS "algo que pode ser observado / comprovado sobre algum objeto". Não tem nada a ver com o outro meio "um campo de um objeto".
Joachim Sauer
4
@ Alexey: Eu não sei mais o que posso lhe dizer. Eu uso a definição de "uma propriedade é alguma qualidade ou atributo de um objeto". "color" é uma propriedade de um objeto físico visível. "densidade" é uma propriedade de um material. "ter um método especificado" é uma propriedade de uma classe / objeto.
Joachim Sauer
4
@Alexey: Eu acho que você está jogando um bebê com a água do banho: só porque para algumas propriedades o LSP não pode ser mantido, não significa que é inútil ou "não se aplica em nenhum idioma". Mas essa discussão iria longe aqui.
Joachim Sauer
5

Pelo que entendi, não há nada na introspecção que seja incompatível com o LSP. Basicamente, desde que um objeto suporte os mesmos métodos que outro, os dois devem ser intercambiáveis. Ou seja, se o seu código espera um Addressobjeto, então não importa se é um CustomerAddressou um WarehouseAddress, desde que ambos fornecem (por exemplo) getStreetAddress(), getCityName(), getRegion()e getPostalCode(). Você certamente poderia criar algum tipo de decorador que usa um tipo diferente de objeto e usa a introspecção para fornecer os métodos necessários (por exemplo, uma DestinationAddressclasse que pega um Shipmentobjeto e apresenta o endereço de entrega como um Address), mas não é obrigatório e certamente não impedir que o LSP seja aplicado.

TMN
fonte
2
@ Alexey: Os objetos são "os mesmos" se eles suportam os mesmos métodos. Isso significa o mesmo nome, mesmo número e tipo de argumentos, o mesmo tipo de retorno e os mesmos efeitos colaterais (desde que sejam visíveis para o código de chamada). Os métodos podem se comportar de maneira completamente diferente, mas, desde que respeitem o contrato, tudo bem.
TMN
1
@ Alexey: mas por que eu teria esse contrato? Que uso real esse contrato atende? Se eu tivesse esse contrato, poderia simplesmente substituir todas as ocorrências x.class.namecom 'A' , efetivamente tornando-se x.class.name inútil .
Joachim Sauer
1
@ Alexey: novamente: só porque você pode definir um contrato que não pode ser cumprido estendendo outra classe não quebra o LSP. Significa apenas que você construiu uma classe não extensível. Se eu definir um método para "retornar se o bloco de código fornecido terminar em tempo finito ", também tenho um contrato que não pode ser cumprido. Isso não significa que a programação é inútil.
Joachim Sauer
2
@ Alexey tentando determinar se x.class.name == 'A'é um anti-padrão na digitação de pato: afinal, a digitação de pato vem de "Se grasna e anda como um pato, é um pato". Portanto, se ele se comporta Ae respeita os contratos que AA
realizam
1
@ Alexey Você foi apresentado com definições claras. A classe de um objeto não faz parte do seu comportamento ou contrato, nem do que você deseja chamá-lo. Você está equivocando "propriedade" com "campo de objeto, como x.myField", que, como foi apontado, NÃO é o mesmo. Nesse contexto, uma propriedade é mais como uma propriedade matemática , como invariantes de tipo. Além disso, é um anti-padrão para verificar o tipo exato se você deseja digitar o pato. Então, qual é o seu problema com LSP e digitação de pato novamente? ;)
Andres F.
4

Depois de examinar o artigo original de Barbara Liskov, descobri como concluir a definição da Wikipedia para que o LSP realmente pudesse ser satisfeito em quase qualquer idioma.

Primeiro de tudo, a palavra "comprovável" é importante na definição. Isso não é explicado no artigo da Wikipedia e "restrições" são mencionadas em outros lugares sem referência a ele.

Aqui está a primeira citação importante do artigo:

O que é necessário é um requisito mais forte que restrinja o comportamento dos subtipos: as propriedades que podem ser comprovadas usando a especificação do tipo presumido de um objeto devem ser mantidas, mesmo que o objeto seja realmente um membro de um subtipo desse tipo ...

E aqui está o segundo, explicando o que é uma especificação de tipo :

Uma especificação de tipo inclui as seguintes informações:

  • O nome do tipo;
  • Uma descrição do espaço de valor do tipo;
  • Para cada um dos métodos do tipo:
    • O seu nome;
    • Sua assinatura (incluindo exceções sinalizadas);
    • Seu comportamento em termos de pré-condições e pós-condições.

Portanto, o LSP só faz sentido em relação a determinadas especificações de tipo e, para uma especificação de tipo apropriada (para a vazia, por exemplo), pode ser satisfeita provavelmente em qualquer idioma.

Considero a resposta de Telastyn a mais próxima do que eu estava procurando, porque as "restrições" foram mencionadas explicitamente.

Alexey
fonte
Telastyn, se você pudesse adicionar essas citações à sua resposta, eu preferiria aceitar a sua do que a minha.
31412 Alexey
2
a marcação não é a mesma e mudei um pouco da ênfase, mas as aspas foram incluídas.
Telastyn 31/07/12
Desculpe, Joachim Sauer já mencionou propriedades "prováveis", das quais você não gostou. Em geral, você apenas reformulou as respostas existentes. Honestamente, eu não sei o que você está procurando ...
Andres F.
Não, não foi explicado "provável pelo quê?". A propriedade x.class.name = 'A'é comprovável para toda a xclasse, Ase você permitir muito conhecimento. A especificação do tipo não foi definida e sua relação exata com o LSP também não, embora informalmente tenham sido dadas algumas indicações. Já encontrei o que estava procurando no artigo de Liskov e respondi minha pergunta acima.
31412 Alexey
Eu acho que as palavras que você encorajou são a chave. Se o supertipo documentar que, para qualquer um xdesses tipos, x.woozleproduzirá undefined, nenhum tipo para o qual x.woozlenão produza undefinedserá um subtipo adequado. Se o supertipo não documentar nada x.woozle, o fato de que o uso x.woozledo supertipo acabe resultando undefinednão implicaria em nada o que poderia fazer no subtipo.
Supercat
3

Para citar o artigo da Wikipedia sobre LSP , "substituibilidade é um princípio na programação orientada a objetos". É um princípio e parte do design do seu programa. Se você escrever um código que dependa x.class == Adisso, é o seu código que está violando o LSP. Observe que esse tipo de código quebrado também é possível em Java, sem necessidade de digitação de pato.

Nada na digitação com patos quebra inerentemente o LSP. Somente se você abusar, como no seu exemplo.

Pensamento adicional: a verificação explícita da classe de um objeto não anula o objetivo da digitação do pato, afinal?

Andres F.
fonte
Andres, você pode dar sua definição de LSP, por favor?
Alexey
1
@ Alexey A definição precisa de LSP é declarada na Wikipedia em termos de subtipos. A definição informal é Liskov's notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, 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).. A classe exata de um objeto NÃO é uma das "propriedades desejáveis ​​do programa"; caso contrário, seria contrário não apenas à digitação com patos, mas também à subtipagem em geral, incluindo o sabor do Java.
Andres F.
2
@Alexey Observe também que seu exemplo de x.class == Aviolação de LSP e de digitação de pato. Não faz sentido usar a digitação de pato se você for verificar os tipos reais.
Andres F.
Andrés, essa definição não é precisa o suficiente para eu entender. Qual programa, um determinado, ou algum? O que é uma propriedade desejável? Se a classe estiver em uma biblioteca, aplicativos diferentes podem considerar propriedades diferentes desejáveis. Não vejo como a linha de código poderia violar o LSP, porque eu pensava que o LSP era uma propriedade de um par de classes em uma determinada linguagem de programação: ( A, B) satisfaz o LSP ou não. Se o LSP depender do código usado em outro lugar, não será explicado qual código é permitido. Espero encontrar algo aqui: cse.ohio-state.edu/~neelam/courses/788/lwb.pdf
Alexey
2
O @Alexey LSP é válido (ou não) para um design específico. É algo para procurar em um design; não é propriedade de um idioma em geral. Ela não recebe qualquer mais preciso do que a própria definição: Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T. É óbvio que x.classnão é uma das propriedades interessantes aqui; caso contrário, o polimorfismo de Java também não funcionaria. Não há nada inerente à digitação lenta no seu "problema x.class". Você concorda até agora?
Andres F.