A resposta com a classificação mais alta para essa pergunta sobre o Princípio da Substituição de Liskov faz um esforço para distinguir entre os termos subtipo e subclasse . Também afirma que algumas línguas conflitam as duas, enquanto outras não.
Para as linguagens orientadas a objetos com as quais eu estou mais familiarizado (Python, C ++), "type" e "class" são conceitos sinônimos. Em termos de C ++, o que significaria ter uma distinção entre subtipo e subclasse? Digamos, por exemplo, que Foo
seja uma subclasse, mas não um subtipo, de FooBase
. Se foo
for uma instância de Foo
, esta linha:
FooBase* fbPoint = &foo;
não é mais válido?
Respostas:
Subtipagem é uma forma de polimorfismo de tipo em que um subtipo é um tipo de dados que está relacionado a outro tipo de dados (o supertipo) por alguma noção de substituibilidade, o que significa que os elementos do programa, geralmente sub-rotinas ou funções, escritas para operar em elementos do supertipo também podem operar em elementos do subtipo.
Se
S
for um subtipo deT
, a relação de subtipo é frequentemente escritaS <: T
, para significar que qualquer termo do tipoS
pode ser usado com segurança em um contexto em que um termo do tipoT
é esperado. A semântica precisa da subtipagem depende crucialmente dos detalhes do que "usado com segurança em um contexto em que" significa em uma determinada linguagem de programação.Subclassificação não deve ser confundida com subtipagem. Em geral, a subtipagem estabelece um relacionamento é-a, enquanto a subclasse apenas reutiliza a implementação e estabelece uma relação sintática, não necessariamente uma relação semântica (a herança não garante a subtipagem comportamental).
Para distinguir esses conceitos, a subtipagem também é conhecida como herança de interface , enquanto a subclasse é conhecida como herança de implementação ou herança de código.
Referências
Subtipagem
Herança
fonte
public
herança introduz um subtipo enquanto aprivate
herança introduz uma subclasse.Um tipo , no contexto sobre o qual estamos falando aqui, é essencialmente um conjunto de garantias comportamentais. Um contrato , se você quiser. Ou, terminologia de empréstimo do Smalltalk, um protocolo .
Uma classe é um pacote de métodos. É um conjunto de implementações de comportamento .
A subtipagem é um meio de refinar o protocolo. A subclassificação é um meio de reutilização de código diferencial, ou seja, reutilização de código apenas descrevendo a diferença de comportamento.
Se você usou Java ou C♯, pode ter encontrado o conselho de que todos os tipos devem ser
interface
tipos. De fato, se você ler On Understanding Data Abstraction, Revisited , de William Cook , talvez saiba que, para fazer OO nesses idiomas, você deve usar apenasinterface
s como tipos. (Além disso, o engraçado é que o Java utilizouinterface
s diretamente dos protocolos da Objective-C, que por sua vez são extraídos diretamente do Smalltalk.)Agora, se seguirmos esse conselho de codificação para sua conclusão lógica e imaginarmos uma versão do Java, onde apenas
interface
s são tipos, e classes e primitivas não, então umainterface
herança de outra criará um relacionamento de subtipagem, enquanto umaclass
herança de outra será seja apenas para reutilização de código diferencial viasuper
.Até onde eu sei, não há linguagens estaticamente tipificadas mainstream que distinguem estritamente entre herdar código (herança de implementação / subclasse) e herdar contratos (subtipo). Em Java e C♯, a herança de interface é pura subtipagem (ou pelo menos era, até a introdução de métodos padrão no Java 8 e provavelmente C♯ 8 também), mas a herança de classe também está subdividida, bem como a herança de implementação. Lembro-me de ler sobre um dialeto experimental LISP orientado a objeto, estaticamente tipado, que distinguia estritamente entre mixins (que contêm comportamento), estruturas (que contêm estado), interfaces (que descrevemcomportamento) e classes (que compõem zero ou mais estruturas com um ou mais mixins e estão em conformidade com uma ou mais interfaces). Somente classes podem ser instanciadas e apenas interfaces podem ser usadas como tipos.
Em uma linguagem OO de tipo dinâmico, como Python, Ruby, ECMAScript ou Smalltalk, geralmente pensamos no (s) tipo (s) de um objeto como o conjunto de protocolos aos quais ele está em conformidade. Observe o plural: um objeto pode ter vários tipos, e não estou falando apenas do fato de que cada objeto do tipo
String
também é um objeto do tipoObject
. (BTW: observe como eu usei nomes de classe para falar sobre tipos? Que estúpido da minha parte!) Um objeto pode implementar vários protocolos. Por exemplo, no Ruby,Arrays
podem ser anexados, eles podem ser indexados, podem ser iterados e comparados. São quatro protocolos diferentes que eles implementam!Agora, Ruby não tem tipos. Mas a comunidade Ruby tem tipos! Eles existem apenas na cabeça dos programadores. E na documentação. Por exemplo, qualquer objeto que responda a um método chamado
each
produzindo seus elementos um por um é considerado um objeto enumerável . E existe um mixin chamadoEnumerable
que depende desse protocolo. Assim, se seu objeto tem o correto tipo (que só existe na cabeça do programador), então é permitido misturar (herdam) oEnumerable
mixin, e assim obter todos os tipos de métodos legais para livre, comomap
,reduce
,filter
e assim em.Da mesma forma, se um objeto responde a
<=>
, então ele é considerado para implementar o comparável protocolo, e pode misturar naComparable
mixin e obter coisas como<
,<=
,>
,<=
,==
,between?
, eclamp
de graça. No entanto, ele também pode implementar todos esses métodos em si, e não herdarComparable
nada, e ainda assim seria considerado comparável .Um bom exemplo é a
StringIO
biblioteca, que essencialmente falsifica os fluxos de E / S com cadeias. Ele implementa todos os mesmos métodos daIO
classe, mas não há relação de herança entre os dois. No entanto, aStringIO
pode ser usado em qualquer lugar queIO
possa ser usado. Isso é muito útil em testes de unidade, onde você pode substituir um arquivo oustdin
por umStringIO
sem precisar fazer mais alterações no seu programa. ComoStringIO
está em conformidade com o mesmo protocolo queIO
, ambos são do mesmo tipo, embora sejam classes diferentes e não compartilhem nenhum relacionamento (exceto o trivial que ambos estendemObject
em algum momento).fonte
Talvez seja útil primeiro distinguir entre um tipo e uma classe e depois mergulhar na diferença entre subtipagem e subclasse.
Para o restante desta resposta, assumirei que os tipos em discussão são estáticos (uma vez que a subtipagem geralmente surge em um contexto estático).
Vou desenvolver um pseudocódigo de brinquedo para ajudar a ilustrar a diferença entre um tipo e uma classe, porque a maioria dos idiomas os confunde pelo menos em parte (por uma boa razão que tocarei brevemente).
Vamos começar com um tipo. Um tipo é um rótulo para uma expressão no seu código. O valor desse rótulo e se ele é consistente (para algum tipo de definição de sistema consistente de consistente) com o valor de todos os outros rótulos pode ser determinado por um programa externo (um verificador de tipos) sem executar o programa. É isso que torna essas etiquetas especiais e merecedoras de seu próprio nome.
Em nossa linguagem de brinquedos, podemos permitir a criação de rótulos como esse.
Então, podemos rotular vários valores como sendo desse tipo.
Com essas declarações, o nosso typechecker agora pode rejeitar declarações como
se um dos requisitos do nosso sistema de tipos for que cada expressão tenha um tipo único.
Vamos deixar de lado, por enquanto, como isso é desajeitado e como você terá problemas para atribuir um número infinito de tipos de expressões. Podemos voltar a ele mais tarde.
Uma classe, por outro lado, é uma coleção de métodos e campos agrupados (potencialmente com modificadores de acesso, como privado ou público).
Uma instância desta classe tem a capacidade de criar ou usar definições pré-existentes desses métodos e campos.
Poderíamos optar por associar uma classe a um tipo, de modo que cada instância de uma classe seja automaticamente rotulada com esse tipo.
Mas nem todo tipo precisa ter uma classe associada.
Também é concebível que em nossa linguagem de brinquedos nem toda classe tenha um tipo, especialmente se nem todas as nossas expressões tiverem tipos. É um pouco mais complicado (mas não impossível) imaginar como seriam as regras de consistência do sistema de tipos se algumas expressões tivessem tipos e outras não.
Além disso, em nossa linguagem de brinquedos, essas associações não precisam ser únicas. Poderíamos associar duas classes do mesmo tipo.
Agora, lembre-se de que não é necessário que o nosso datilógrafo rastreie o valor de uma expressão (e, na maioria dos casos, não é ou é impossível fazê-lo). Tudo o que sabe são os rótulos que você contou. Como um lembrete anterior, o verificador de datilografia só conseguiu rejeitar a declaração
0 is of type String
por causa de nossa regra de tipo criada artificialmente de que expressões devem ter tipos únicos e que já tínhamos rotulado a expressão como0
outra coisa. Não tinha nenhum conhecimento especial do valor de0
.E quanto a subtipagem? Subtipagem de poço é o nome de uma regra comum na digitação de textos que relaxa as outras regras que você possa ter. Ou seja, se
A is subtype of B
em todo lugar o seu datilógrafo exigir um rótuloB
, ele também aceitará umA
.Por exemplo, podemos fazer o seguinte para nossos números, em vez do que tínhamos anteriormente.
Subclassificação é uma abreviação para declarar uma nova classe que permite reutilizar métodos e campos declarados anteriormente.
Não precisamos associar instâncias de
ExtendedStringClass
com,String
como fizemos comStringClass
, uma vez que, afinal, é uma classe totalmente nova, simplesmente não precisamos escrever tanto. Isso nos permitiria fornecerExtendedStringClass
um tipo incompatível comString
o ponto de vista do datilógrafo.Da mesma forma, poderíamos ter decidido fazer uma aula totalmente nova
NewClass
e feitoAgora, todas as instâncias de
StringClass
podem ser substituídasNewClass
do ponto de vista do datilógrafo.Então, em teoria, subtipagem e subclasse são coisas completamente diferentes. Mas nenhuma linguagem que eu conheça que tenha tipos e classes realmente faz as coisas dessa maneira. Vamos começar a reduzir nossa linguagem e explicar a lógica por trás de algumas de nossas decisões.
Primeiro, mesmo que, em teoria, classes completamente diferentes possam receber o mesmo tipo ou uma classe possa ter o mesmo tipo que valores que não são instâncias de nenhuma classe, isso dificulta gravemente a utilidade do verificador de letras. O typechecker é efetivamente roubado da capacidade de verificar se o método ou campo que você está chamando em uma expressão realmente existe nesse valor, o que provavelmente é uma verificação que você gostaria se estivesse com o problema de tocar junto com um typechecker. Afinal, quem sabe qual é o valor realmente embaixo desse
String
rótulo; pode ser algo que não possui, por exemplo, umconcatenate
método!Ok, então vamos estipular que toda classe gera automaticamente um novo tipo com o mesmo nome que aquela classe e
associate
instâncias com esse tipo. Isso nos permite livrar-nos dosassociate
nomes diferentes entreStringClass
eString
.Pelo mesmo motivo, provavelmente desejamos estabelecer automaticamente um relacionamento de subtipo entre os tipos de duas classes em que uma é uma subclasse da outra. Afinal, é garantido que a subclasse possui todos os métodos e campos da classe pai, mas o oposto não é verdadeiro. Portanto, embora a subclasse possa passar a qualquer momento que você precisar de um tipo da classe pai, o tipo da classe pai deve ser rejeitado se você precisar do tipo da subclasse.
Se você combinar isso com a estipulação de que todos os valores definidos pelo usuário devem ser instâncias de uma classe, você poderá
is subclass of
obter o dobro do dever e se livraris subtype of
.E isso nos leva às características que a maioria das linguagens populares OO de tipo estatístico compartilha. Há um conjunto de tipos "primitivos" (por exemplo
int
,float
etc.) que não estão associados a nenhuma classe e não são definidos pelo usuário. Então você tem todas as classes definidas pelo usuário que possuem automaticamente tipos com o mesmo nome e identificam subclassificação com subtipagem.A nota final que farei é sobre o clunkiness de declarar tipos separadamente dos valores. A maioria dos idiomas confunde a criação dos dois, de modo que uma declaração de tipo também é uma declaração para gerar valores totalmente novos que são automaticamente rotulados com esse tipo. Por exemplo, uma declaração de classe geralmente cria o tipo e também uma maneira de instanciar valores desse tipo. Isso elimina algumas das imperfeições e, na presença de construtores, também permite criar infinitos rótulos de valores com um tipo em um só toque.
fonte