La respuesta mejor calificada a esta pregunta sobre el Principio de sustitución de Liskov se esfuerza por distinguir entre los términos subtipo y subclase . También señala que algunos idiomas combinan los dos, mientras que otros no.
Para los lenguajes orientados a objetos con los que estoy más familiarizado (Python, C ++), "tipo" y "clase" son conceptos sinónimos. En términos de C ++, ¿qué significaría tener una distinción entre subtipo y subclase? Digamos, por ejemplo, que Foo
es una subclase, pero no un subtipo, de FooBase
. Si foo
es una instancia de Foo
, ¿sería esta línea:
FooBase* fbPoint = &foo;
ya no es valido?
Respuestas:
El subtipo es una forma de polimorfismo de tipo en el que un subtipo es un tipo de datos que está relacionado con otro tipo de datos (el supertipo) por alguna noción de sustituibilidad, lo que significa que los elementos del programa, generalmente subrutinas o funciones, escritas para operar en elementos del supertipo también pueden operar en elementos del subtipo.
Si
S
es un subtipo deT
, la relación de subtipo a menudo se escribeS <: T
, lo que significa que cualquier término de tipoS
puede usarse de manera segura en un contexto dondeT
se espera un término de tipo . La semántica precisa del subtipo depende crucialmente de los detalles de lo que "se usa con seguridad en un contexto donde" significa en un lenguaje de programación dado.La subclasificación no debe confundirse con la subtipificación. En general, el subtipo establece una relación is-a, mientras que la subclasificación solo reutiliza la implementación y establece una relación sintáctica, no necesariamente una relación semántica (la herencia no garantiza el subtipo de comportamiento).
Para distinguir estos conceptos, el subtipo también se conoce como herencia de interfaz , mientras que la subclasificación se conoce como herencia de implementación o herencia de código.
Referencias
Subtipo de
herencia
fuente
public
herencia introduce un subtipo mientras que laprivate
herencia introduce una subclase.Un tipo , en el contexto del que estamos hablando aquí, es esencialmente un conjunto de garantías de comportamiento. Un contrato , si quieres. O, tomando prestada la terminología de Smalltalk, un protocolo .
Una clase es un conjunto de métodos. Es un conjunto de implementaciones de comportamiento .
Subtipar es un medio de refinar el protocolo. La subclasificación es un medio de reutilización del código diferencial, es decir, la reutilización del código al describir solo la diferencia de comportamiento.
Si ha utilizado Java o C♯, es posible que haya encontrado el consejo de que todos los tipos deberían ser
interface
tipos. De hecho, si lee William Cook's On Understanding Data Abstraction, Revisited , entonces puede saber que para hacer OO en esos idiomas, solo debe usarinterface
s como tipos. (También, dato curioso: Java se cribainterface
directamente desde los protocolos de Objective-C, que a su vez se toman directamente de Smalltalk).Ahora, si seguimos ese consejo de codificación hasta su conclusión lógica e imaginamos una versión de Java, donde solo los
interface
s son tipos, y las clases y las primitivas no lo son, entonces unointerface
heredando de otro creará una relación de subtipo, mientras que unoclass
heredará de otro ser simplemente para la reutilización de código diferencial a través desuper
.Hasta donde sé, no hay lenguajes estáticos convencionales que distingan estrictamente entre el código heredado (herencia de implementación / subclasificación) y los contratos heredados (subtipo). En Java y C♯, la herencia de interfaz es subtipo puro (o al menos lo era, hasta la introducción de métodos predeterminados en Java 8 y probablemente también C♯ 8), pero la herencia de clase también es subtipo, así como la herencia de implementación. Recuerdo haber leído sobre un dialecto experimental LISP orientado a objetos estáticamente tipado, que distinguía estrictamente entre mixins (que contienen comportamiento), estructuras (que contienen estado), interfaces (que describencomportamiento) y clases (que componen cero o más estructuras con uno o más mixins y se ajustan a una o más interfaces). Solo se pueden crear instancias de clases y solo se pueden usar interfaces como tipos.
En un lenguaje OO de tipo dinámico como Python, Ruby, ECMAScript o Smalltalk, generalmente pensamos en los tipos de un objeto como el conjunto de protocolos a los que se ajusta. Tenga en cuenta el plural: un objeto puede tener múltiples tipos, y no solo estoy hablando del hecho de que cada objeto de tipo
String
también es un objeto de tipoObject
. (Por cierto: observe cómo usé los nombres de clase para hablar sobre los tipos. ¡Qué estúpido de mi parte!) Un objeto puede implementar múltiples protocolos. Por ejemplo, en Ruby,Arrays
se pueden agregar, se pueden indexar, se pueden iterar y se pueden comparar. ¡Son cuatro protocolos diferentes que implementan!Ahora, Ruby no tiene tipos. ¡Pero la comunidad Ruby tiene tipos! Sin embargo, solo existen en la cabeza de los programadores. Y en la documentación. Por ejemplo, cualquier objeto que responda a un método llamado
each
al entregar sus elementos uno por uno se considera un objeto enumerable . Y hay un mixin llamadoEnumerable
que depende de este protocolo. Por lo tanto, si el objeto tiene el correcto tipo (que sólo existe en la mente del programador), entonces se dejó mezclar en (hereda de) laEnumerable
mixin, y así obtener todo tipo de métodos fresco de forma gratuita, comomap
,reduce
,filter
etc. en.Del mismo modo, si un objeto responde a
<=>
, entonces se considera para implementar el comparables protocolo, y se puede mezclar en elComparable
mixin y obtener cosas por el estilo<
,<=
,>
,<=
,==
,between?
, yclamp
de forma gratuita. Sin embargo, también puede implementar todos esos métodos en sí mismo, y no heredarComparable
en absoluto, y aún se consideraría comparable .Un buen ejemplo es la
StringIO
biblioteca, que esencialmente falsifica las secuencias de E / S con cadenas. Implementa los mismos métodos que laIO
clase, pero no existe una relación de herencia entre los dos. Sin embargo, aStringIO
se puede usar en todas partes yIO
se puede usar. Esto es muy útil en pruebas unitarias, donde puede reemplazar un archivo ostdin
con unStringIO
sin tener que hacer más cambios en su programa. Dado que seStringIO
ajusta al mismo protocolo queIO
, ambos son del mismo tipo, a pesar de que son clases diferentes y no comparten ninguna relación (aparte del trivial que ambos extiendenObject
en algún momento).fuente
Quizás sea primero útil distinguir entre un tipo y una clase y luego sumergirse en la diferencia entre subtipo y subclasificación.
Para el resto de esta respuesta, voy a suponer que los tipos en discusión son tipos estáticos (ya que el subtipo generalmente aparece en un contexto estático).
Voy a desarrollar un pseudocódigo de juguete para ayudar a ilustrar la diferencia entre un tipo y una clase porque la mayoría de los idiomas los combinan al menos en parte (por una buena razón que mencionaré brevemente).
Comencemos con un tipo. Un tipo es una etiqueta para una expresión en su código. El valor de esta etiqueta y si es consistente (para algún tipo de definición específica de sistema de consistente) con el valor de todas las otras etiquetas puede ser determinado por un programa externo (un typechecker) sin ejecutar su programa. Eso es lo que hace que estas etiquetas sean especiales y merezcan su propio nombre.
En nuestro lenguaje de juguetes, podríamos permitir la creación de etiquetas como esta.
Entonces podríamos etiquetar varios valores como de este tipo.
Con estas declaraciones, nuestro typechecker ahora puede rechazar declaraciones como
Si uno de los requisitos de nuestro sistema de tipos es que cada expresión tiene un tipo único.
Dejemos de lado por ahora lo torpe que es esto y cómo va a tener problemas para asignar un número infinito de tipos de expresiones. Podemos volver a eso más tarde.
Una clase, por otro lado, es una colección de métodos y campos que se agrupan (potencialmente con modificadores de acceso como privado o público).
Una instancia de esta clase tiene la capacidad de crear o usar definiciones preexistentes de estos métodos y campos.
Podríamos elegir asociar una clase con un tipo de modo que cada instancia de una clase se etiquete automáticamente con ese tipo.
Pero no todos los tipos necesitan tener una clase asociada.
También es concebible que en nuestro lenguaje de juguete no todas las clases tengan un tipo, especialmente si no todas nuestras expresiones tienen tipos. Es un poco más complicado (pero no imposible) imaginar cómo se verían las reglas de coherencia del sistema de tipos si algunas expresiones tuvieran tipos y otras no.
Además, en nuestro lenguaje de juguete, estas asociaciones no tienen que ser únicas. Podríamos asociar dos clases con el mismo tipo.
Ahora tenga en cuenta que no hay ningún requisito para que nuestro typechecker rastree el valor de una expresión (y en la mayoría de los casos no lo hará o es imposible hacerlo). Todo lo que sabe son las etiquetas que le has dicho. Como recordatorio anterior, el comprobador de tipos solo podía rechazar la declaración
0 is of type String
debido a nuestra regla de tipo creada artificialmente de que las expresiones deben tener tipos únicos y ya habíamos etiquetado la expresión como0
algo diferente. No tenía ningún conocimiento especial del valor de0
.Entonces, ¿qué pasa con el subtipo? Subtipo de pozo es un nombre para una regla común en la verificación de tipos que relaja las otras reglas que pueda tener. Es decir, si en
A is subtype of B
todas partes su typechecker exige una etiquetaB
, también aceptará unA
.Por ejemplo, podríamos hacer lo siguiente para nuestros números en lugar de lo que teníamos anteriormente.
La subclasificación es una forma abreviada de declarar una nueva clase que le permite reutilizar métodos y campos previamente declarados.
No tenemos que asociar instancias de
ExtendedStringClass
conString
como lo hicimosStringClass
ya que, después de todo, es una clase completamente nueva, simplemente no teníamos que escribir tanto. Esto nos permitiría darExtendedStringClass
un tipo que es incompatibleString
desde el punto de vista del typechecker.Del mismo modo, podríamos haber decidido hacer una clase completamente nueva
NewClass
y hacerAhora cada instancia de
StringClass
puede ser sustituidaNewClass
por el punto de vista del typechecker.Entonces, en teoría, el subtipo y la subclasificación son cosas completamente diferentes. Pero ningún lenguaje que conozca que tenga tipos y clases realmente hace las cosas de esta manera. Comencemos por reducir nuestro lenguaje y expliquemos los fundamentos de algunas de nuestras decisiones.
En primer lugar, a pesar de que, en teoría, las clases completamente diferentes podrían recibir el mismo tipo o una clase podría recibir el mismo tipo que los valores que no son instancias de ninguna clase, esto obstaculiza gravemente la utilidad del comprobador de tipos. El typechecker es efectivamente despojado de la capacidad de verificar si el método o campo que está llamando dentro de una expresión realmente existe en ese valor, lo que probablemente sea una comprobación que le gustaría si tiene la molestia de jugar junto con un máquina de escribir Después de todo, quién sabe cuál es el valor realmente debajo de esa
String
etiqueta; ¡podría ser algo que no tiene, por ejemplo, unconcatenate
método en absoluto!Bien, entonces estipulemos que cada clase genera automáticamente un nuevo tipo del mismo nombre que esa clase y
associate
las instancias de ese tipo. Eso nos permite deshacernos deassociate
los diferentes nombres entreStringClass
yString
.Por la misma razón, probablemente queremos establecer automáticamente una relación de subtipo entre los tipos de dos clases donde una es una subclase de otra. Después de toda la subclase se garantiza que tiene todos los métodos y campos que tiene la clase padre, pero lo contrario no es cierto. Por lo tanto, aunque la subclase puede pasar en cualquier momento que necesite un tipo de la clase principal, el tipo de la clase principal debe rechazarse si necesita el tipo de la subclase.
Si combina esto con la estipulación de que todos los valores definidos por el usuario deben ser instancias de una clase, entonces puede
is subclass of
hacer doble trabajo y deshacerse de élis subtype of
.Y esto nos lleva a las características que comparten la mayoría de los lenguajes OO de tipo estático populares. Hay un conjunto de tipos "primitivos" (por ejemplo
int
,float
etc.) que no están asociados con ninguna clase y no están definidos por el usuario. Luego tiene todas las clases definidas por el usuario que automáticamente tienen tipos del mismo nombre e identifican subclases con subtipos.La nota final que haré es en torno a la complejidad de declarar tipos por separado de los valores. La mayoría de los idiomas combinan la creación de los dos, de modo que una declaración de tipo también es una declaración para generar valores completamente nuevos que se etiquetan automáticamente con ese tipo. Por ejemplo, una declaración de clase generalmente crea el tipo, así como una forma de instanciar valores de ese tipo. Esto elimina parte de la fragilidad y, en presencia de constructores, también le permite crear etiquetas de infinitos valores con un tipo de un solo trazo.
fuente