Herencia (programación orientada a objetos)
En la programación orientada a objetos, la herencia es el mecanismo de basar un objeto o clase en otro objeto (herencia basada en prototipos) o clase (herencia basada en clases), conservando una implementación similar. También se define como derivar nuevas clases (subclases) de las existentes, como superclase o clase base, y luego formarlas en una jerarquía de clases. En la mayoría de los lenguajes orientados a objetos basados en clases como C++, un objeto creado mediante herencia, un "objeto hijo", adquiere todas las propiedades y comportamientos del "objeto padre", con la excepción de: constructores, destructores, operadores sobrecargados y funciones amigas de la clase base. La herencia permite a los programadores crear clases que se basan en clases existentes, especificar una nueva implementación manteniendo los mismos comportamientos (realizando una interfaz), reutilizar código y extender de forma independiente el software original a través de clases e interfaces públicas. Las relaciones de objetos o clases mediante herencia dan lugar a un grafo acíclico dirigido.
Una clase heredada se denomina subclase de su clase principal o superclase. El término "herencia" se utiliza libremente tanto para la programación basada en clases como para la programación basada en prototipos, pero en un uso restringido el término está reservado para la programación basada en clases (una clase hereda de otra), con la técnica correspondiente en la programación basada en prototipos. la programación se llama en cambio delegación (un objeto delega a otro). Los patrones de herencia que modifican las clases se pueden predefinir de acuerdo con parámetros simples de la interfaz de red, de modo que se preserve la compatibilidad entre idiomas.
La herencia no debe confundirse con la subtipificación. En algunas lenguas la herencia y la subtipificación coinciden, mientras que en otras difieren; en general, la subtipificación establece una relación es-un, mientras que la herencia solo reutiliza la implementación y establece una relación sintáctica, no necesariamente una relación semántica (la herencia no garantiza la subtipificación conductual). Para distinguir estos conceptos, a veces se hace referencia a la subtipificación como herencia de interfaz (sin reconocer que la especialización de las variables de tipo también induce una relación de subtipificación), mientras que la herencia tal como se define aquí se conoce como herencia de implementación o herencia de código. Aún así, la herencia es un mecanismo comúnmente utilizado para establecer relaciones de subtipo.
La herencia se contrasta con la composición de objetos, donde un objeto contiene otro objeto (o los objetos de una clase contienen objetos de otra clase); ver composición sobre herencia. La composición implementa una relación tiene-un, en contraste con la relación es-un de subtipo.
Historia
En 1966, Tony Hoare presentó algunas observaciones sobre los registros, y en particular presentó la idea de subclases de registros, tipos de registros con propiedades comunes pero discriminados por una etiqueta de variante y con campos privados para la variante. Influenciados por esto, en 1967 Ole-Johan Dahl y Kristen Nygaard presentaron un diseño que permitía especificar objetos que pertenecían a diferentes clases pero tenían propiedades comunes. Las propiedades comunes se reunieron en una superclase, y cada superclase podría tener potencialmente una superclase. Los valores de una subclase eran, por tanto, objetos compuestos, que constaban de un cierto número de partes de prefijo pertenecientes a varias superclases, más una parte principal que pertenecía a la subclase. Todas estas partes estaban concatenadas. Los atributos de un objeto compuesto serían accesibles mediante notación de puntos. Esta idea se adoptó por primera vez en el lenguaje de programación Simula 67. Luego, la idea se extendió a Smalltalk, C++, Java, Python y muchos otros lenguajes.
Tipos


Existen varios tipos de herencia, según el paradigma y el lenguaje específico.
- Sucesiones individuales
- donde las subclases heredan las características de una superclase. Una clase adquiere las propiedades de otra clase.
- Múltiple herencia
- donde una clase puede tener más de una superclase y heredar características de todas las clases padres.
"Ley múltiple ... se suponía que era muy difícil de implementar eficientemente. Por ejemplo, en un resumen de C++ en su libro sobre Objetivo C, Brad Cox afirmaba que agregar múltiples herencias a C++ era imposible. Así, la herencia múltiple parecía más un reto. Desde Había considerado la herencia múltiple desde 1982 y encontré una técnica de implementación simple y eficiente en 1984, no pude resistir el desafío. Sospecho que este es el único caso en el que la moda afectó la secuencia de eventos".
—Bjarne Stroustrup- Sucesiones multinivel
- donde una subclase es heredada de otra subclase. No es raro que una clase se derive de otra clase derivada, como se muestra en la figura "la herencia multinivel".
Sucesiones multinivel - La clase A sirve como un clase base para el clase derivada B, que a su vez sirve como un clase base para el clase derivada C. La clase B es conocido como intermedio clase base porque proporciona un enlace para la herencia entre A y C. La cadena ABC es conocido como herencia.
- Una clase derivada con herencia multinivel se declara como sigue:
// C++clase A {} ... }; / Clase de baseclase B : público A {} ... }; // B derivado de Aclase C : público B {} ... }; // C derivadas de B
- Este proceso puede extenderse a cualquier número de niveles.
- herencia jerárquica
- Aquí es donde una clase sirve como una superclase (clase básica) para más de una subclase. Por ejemplo, una clase padre, A, puede tener dos subclases B y C. Tanto la clase madre de B como C es A, pero B y C son dos subclases separadas.
- Sucesión híbrida
- La herencia híbrida es cuando se produce una mezcla de dos o más de los tipos de herencia anteriores. Un ejemplo de esto es cuando una clase A tiene una subclase B que tiene dos subclases, C y D. Esta es una mezcla de herencia multinivel y herencia jerárquica.
Subclases y superclases
Subclases, clases derivadas, clases herederas o clases hijas son clases derivadas modulares que heredan uno o más entidades de lenguaje de una o más clases (llamadas superclase, clases base o clases principales). La semántica de la herencia de clases varía de un idioma a otro, pero comúnmente la subclase hereda automáticamente las variables de instancia y funciones miembro de sus superclases.
La forma general de definir una clase derivada es:
clase SubClass: visibilidad SuperClass{} // Miembros de subclase};
- El colon indica que la subclase hereda de la superclase. La visibilidad es opcional y, si está presente, puede ser privado o público. La visibilidad predeterminada es privado. Visibilidad especifica si las características de la clase base son derivada privada o derivación pública.
Algunos lenguajes también admiten la herencia de otras construcciones. Por ejemplo, en Eiffel, los herederos también heredan los contratos que definen la especificación de una clase. La superclase establece una interfaz común y una funcionalidad fundamental, que las subclases especializadas pueden heredar, modificar y complementar. El software heredado por una subclase se considera reutilizado en la subclase. Una referencia a una instancia de una clase puede en realidad referirse a una de sus subclases. La clase real del objeto al que se hace referencia es imposible de predecir en tiempo de compilación. Se utiliza una interfaz uniforme para invocar las funciones miembro de objetos de varias clases diferentes. Las subclases pueden reemplazar funciones de superclase con funciones completamente nuevas que deben compartir la misma firma de método.
Clases no subclasificables
En algunos idiomas, una clase puede declararse como no subclasificable agregando ciertos modificadores de clase a la declaración de clase. Los ejemplos incluyen la palabra clave final
en Java y C++ 11 en adelante o la palabra clave sealed
en C#. Dichos modificadores se agregan a la declaración de clase antes de la palabra clave class
y la declaración del identificador de clase. Estas clases que no se pueden subclasificar restringen la reutilización, especialmente cuando los desarrolladores sólo tienen acceso a archivos binarios precompilados y no al código fuente.
Una clase que no se puede subclasificar no tiene subclases, por lo que se puede deducir fácilmente en tiempo de compilación que las referencias o punteros a objetos de esa clase en realidad hacen referencia a instancias de esa clase y no a instancias de subclases (que no existen) o instancias. de superclases (la actualización de un tipo de referencia viola el sistema de tipos). Debido a que el tipo exacto del objeto al que se hace referencia se conoce antes de la ejecución, se puede utilizar el enlace temprano (también llamado envío estático) en lugar del enlace tardío (también llamado envío dinámico), que requiere una o más búsquedas en la tabla de métodos virtuales dependiendo de si se trata de herencia múltiple. o solo la herencia única son compatibles con el lenguaje de programación que se está utilizando.
Métodos no anulables
Así como las clases pueden no ser subclasificables, las declaraciones de métodos pueden contener modificadores de método que impiden que el método sea anulado (es decir, reemplazado con una nueva función con el mismo nombre y firma de tipo en una subclase). Un método privado no se puede anular simplemente porque no es accesible para otras clases que no sean la clase de la que es miembro (aunque esto no es cierto para C++). Un método final
en Java, un método sealed
en C# o una característica congelada
en Eiffel no se pueden anular.
Métodos virtuales
Si un método de superclase es un método virtual, las invocaciones del método de superclase se enviarán dinámicamente. Algunos lenguajes requieren que el método se declare específicamente como virtual (por ejemplo, C++) y en otros, todos los métodos son virtuales (por ejemplo, Java). Una invocación de un método no virtual siempre se enviará estáticamente (es decir, la dirección de la llamada a la función se determina en tiempo de compilación). El envío estático es más rápido que el envío dinámico y permite optimizaciones como la expansión en línea.
Visibilidad de las miembros heredadas
(feminine)La siguiente tabla muestra qué variables y funciones se heredan dependiendo de la visibilidad dada al derivar la clase, utilizando la terminología establecida por C++.
Visión de la clase base | Visibilidad de clase derivada | ||
---|---|---|---|
Derivación privada | Derivación protegida | Derivación pública | |
|
|
|
|
Aplicaciones
La herencia se utiliza para correlacionar dos o más clases entre sí.
Anulación

Muchos lenguajes de programación orientados a objetos permiten que una clase u objeto reemplace la implementación de un aspecto (normalmente un comportamiento) que ha heredado. Este proceso se llama anulación. La anulación introduce una complicación: ¿qué versión del comportamiento utiliza una instancia de la clase heredada: la que forma parte de su propia clase o la de la clase principal (base)? La respuesta varía entre los lenguajes de programación, y algunos lenguajes brindan la capacidad de indicar que un comportamiento particular no debe anularse y debe comportarse según lo definido por la clase base. Por ejemplo, en C#, el método o propiedad base solo se puede anular en una subclase si está marcado con el modificador virtual, abstracto o anular, mientras que en lenguajes de programación como Java, se pueden llamar a diferentes métodos para anular otros métodos. Una alternativa a la anulación es ocultar el código heredado.
Reutilización de código
La herencia de implementación es el mecanismo mediante el cual una subclase reutiliza el código de una clase base. Por defecto, la subclase retiene todas las operaciones de la clase base, pero la subclase puede anular algunas o todas las operaciones, reemplazando la implementación de la clase base por la suya propia.
En el siguiente ejemplo de Python, las subclases SquareSumComputer y CubeSumComputer anulan transform() Método de la clase base SumComputer. La clase base comprende operaciones para calcular la suma de los cuadrados entre dos números enteros. La subclase reutiliza toda la funcionalidad de la clase base con la excepción de la operación que transforma un número en su cuadrado, reemplazándola con una operación que transforma un número en su cuadrado y cubo respectivamente. Por lo tanto, las subclases calculan la suma de los cuadrados/cubos entre dos números enteros.
A continuación se muestra un ejemplo de Python.
clase SumComputer: def __init_()auto, a, b): auto.a = a auto.b = b def transformación()auto, x): aumento No Implemented Error def insumos()auto): Regreso rango()auto.a, auto.b) def computador()auto): Regreso suma()auto.transformación()valor) para valor dentro auto.insumos()clase SquareSumComputer()SumComputer): def transformación()auto, x): Regreso x * xclase CubeSumComputer()SumComputer): def transformación()auto, x): Regreso x * x * x
En la mayoría de los sectores, la herencia de clases con el único propósito de reutilizar el código ha caído en desgracia. La principal preocupación es que la herencia de implementación no proporciona ninguna garantía de sustituibilidad polimórfica: una instancia de la clase reutilizada no necesariamente puede sustituirse por una instancia de la clase heredada. Una técnica alternativa, la delegación explícita, requiere más esfuerzo de programación, pero evita el problema de la sustituibilidad. En C++, la herencia privada se puede utilizar como una forma de herencia de implementación sin posibilidad de sustitución. Mientras que la herencia pública representa un "is-a" La relación y la delegación representan una relación "has-a" relación, la herencia privada (y protegida) puede considerarse como una "se implementa en términos de" relación.
Otro uso frecuente de la herencia es garantizar que las clases mantengan una determinada interfaz común; es decir, implementan los mismos métodos. La clase principal puede ser una combinación de operaciones implementadas y operaciones que se implementarán en las clases secundarias. A menudo, no hay ningún cambio de interfaz entre el supertipo y el subtipo: el niño implementa el comportamiento descrito en lugar de su clase principal.
Herencia versus subtipificación
La herencia es similar pero distinta de la subtipificación. La subtipificación permite sustituir un tipo determinado por otro tipo o abstracción y se dice que establece una relación is-a entre el subtipo y alguna abstracción existente, ya sea implícita o explícitamente, dependiendo del soporte del lenguaje. La relación se puede expresar explícitamente mediante herencia en lenguajes que admiten la herencia como mecanismo de subtipificación. Por ejemplo, el siguiente código C++ establece una relación de herencia explícita entre las clases B y A, donde B es a la vez una subclase y un subtipo de < i>A y se puede utilizar como A siempre que se especifique una B (a través de una referencia, un puntero o el objeto mismo).
clase A {} público: vacío DoSomething ALike() const {}};clase B : público A {} público: vacío DoSomething BLike() const {}};vacío UseAnA()const A" a) {} a.DoSomething ALike();}vacío SomeFunc() {} B b; UseAnA()b); // b se puede sustituir por una A.}
En los lenguajes de programación que no admiten la herencia como mecanismo de subtipificación, la relación entre una clase base y una clase derivada es solo una relación entre implementaciones (un mecanismo para la reutilización de código), en comparación con una relación entre tipos. La herencia, incluso en lenguajes de programación que admiten la herencia como mecanismo de subtipificación, no implica necesariamente una subtipificación conductual. Es completamente posible derivar una clase cuyo objeto se comportará incorrectamente cuando se use en un contexto donde se espera la clase principal; ver el principio de sustitución de Liskov. (Compare connotación/denotación.) En algunos lenguajes de programación orientada a objetos, las nociones de reutilización de código y subtipo coinciden porque la única forma de declarar un subtipo es definir una nueva clase que herede la implementación de otra.
Restricciones de diseño
El uso extensivo de la herencia en el diseño de un programa impone ciertas restricciones.
Por ejemplo, considere una clase Persona que contiene el nombre, la fecha de nacimiento, la dirección y el número de teléfono de una persona. Podemos definir una subclase de Persona llamada Estudiante que contiene el promedio de calificaciones de la persona y las clases tomadas, y otra subclase de Persona llamada Empleado que contiene el puesto de trabajo, el empleador y el salario de la persona.
Al definir esta jerarquía de herencia ya hemos definido ciertas restricciones, no todas las cuales son deseables:
- Sensibilidad
- Usando una sola herencia, una subclase puede heredar de sólo una superclase. Continuando el ejemplo dado anteriormente, a Persona el objeto puede ser Estudiante o un Employee, pero no ambos. Usar la herencia múltiple resuelve parcialmente este problema, ya que se puede definir un StudentEmployee clase que hereda de ambos Estudiante y Employee. Sin embargo, en la mayoría de las implementaciones, todavía puede heredar de cada superclase sólo una vez, y por lo tanto, no apoya casos en los que un estudiante tiene dos empleos o asiste a dos instituciones. El modelo de herencia disponible en Eiffel hace posible esto mediante el apoyo a la repetida herencia.
- Estática
- La jerarquía de herencia de un objeto se fija al instante cuando se selecciona el tipo del objeto y no cambia con el tiempo. Por ejemplo, el gráfico de la herencia no permite un Estudiante objeto de convertirse en un Employee objeto manteniendo el estado de su Persona superclase. (Este tipo de comportamiento, sin embargo, se puede lograr con el patrón de decorador.) Algunos han criticado la herencia, alegando que bloquea a los desarrolladores en sus estándares de diseño originales.
- Visibilidad
- Siempre que el código del cliente tiene acceso a un objeto, generalmente tiene acceso a todos los datos de superclase del objeto. Incluso si la superclase no ha sido declarada pública, el cliente todavía puede lanzar el objeto a su tipo de superclase. Por ejemplo, no hay manera de dar una función un puntero a un Estudiante's grado promedio y transcripción sin dar acceso a esa función a todos los datos personales almacenados en el estudiante Persona superclase. Muchos idiomas modernos, incluyendo C++ y Java, proporcionan un modificador de acceso "protegido" que permite a las subclas acceder a los datos, sin permitir que ningún código fuera de la cadena de herencia lo acceda.
El principio de reutilización compuesto es una alternativa a la herencia. Esta técnica soporta el polimorfismo y la reutilización de códigos separando comportamientos de la jerarquía de clase primaria e incluyendo clases de comportamiento específicas como se requiere en cualquier clase de dominio empresarial. Este enfoque evita la naturaleza estática de una jerarquía de clases permitiendo modificaciones de comportamiento a tiempo de ejecución y permite que una clase implemente comportamientos tipo buffet, en lugar de limitarse a los comportamientos de sus clases de ancestro.
Problemas y alternativas
La herencia de implementación es controvertida entre los programadores y teóricos de la programación orientada a objetos desde al menos la década de 1990. Entre ellos se encuentran los autores de Design Patterns, que abogan por la herencia de la interfaz y favorecen la composición sobre la herencia. Por ejemplo, el patrón decorador (como se mencionó anteriormente) se propuso para superar la naturaleza estática de la herencia entre clases. Como solución más fundamental al mismo problema, la programación orientada a roles introduce una relación distinta, jugada por, que combina propiedades de herencia y composición en un nuevo concepto.
Según Allen Holub, el principal problema con la herencia de implementación es que introduce un acoplamiento innecesario en forma de "problema de clase base frágil": las modificaciones en la implementación de la clase base pueden causar cambios de comportamiento involuntarios en las subclases. . El uso de interfaces evita este problema porque no se comparte ninguna implementación, solo la API. Otra forma de decir esto es que "la herencia rompe la encapsulación". El problema surge claramente en sistemas abiertos orientados a objetos, como los marcos, donde se espera que el código del cliente herede de las clases proporcionadas por el sistema y luego sustituya las clases del sistema en sus algoritmos.
Según se informa, el inventor de Java, James Gosling, se ha pronunciado en contra de la herencia de implementación y afirmó que no la incluiría si rediseñara Java. Los diseños de lenguaje que desacoplan la herencia de la subtipificación (herencia de interfaz) aparecieron ya en 1990; un ejemplo moderno de esto es el lenguaje de programación Go.
La herencia compleja, o la herencia utilizada dentro de un diseño insuficientemente maduro, puede provocar el problema del yo-yo. Cuando la herencia se utilizó como enfoque principal para estructurar programas a fines de la década de 1990, los desarrolladores tendieron a dividir el código en más capas de herencia a medida que crecía la funcionalidad del sistema. Si un equipo de desarrollo combinaba múltiples capas de herencia con el principio de responsabilidad única, esto daba como resultado muchas capas de código muy delgadas, muchas de las cuales consistían en solo 1 o 2 líneas de código real. Demasiadas capas hacen que la depuración sea un desafío importante, ya que resulta difícil determinar qué capa necesita ser depurada.
Otro problema con la herencia es que las subclases deben definirse en el código, lo que significa que los usuarios del programa no pueden agregar nuevas subclases en tiempo de ejecución. Otros patrones de diseño (como Entidad-componente-sistema) permiten a los usuarios del programa definir variaciones de una entidad en tiempo de ejecución.