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 ), manteniendo una implementación similar . También se define como derivar nuevas clases (subclases) de las existentes, como la superclase o la 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 a través de la 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, [1] especificar una nueva implementación manteniendo los mismos comportamientos ( realizando una interfaz ), reutilizar código y extender independientemente el software original a través de clases e interfaces públicas . Las relaciones de objetos o clases a través de la herencia dan lugar a un grafo acíclico dirigido .
Una clase heredada se denomina subclase de su clase padre o superclase. El término "herencia" se utiliza de forma general tanto para la programación basada en clases como para la basada en prototipos, pero en un uso restringido el término se reserva para la programación basada en clases (una clase hereda de otra), y la técnica correspondiente en la programación basada en prototipos se denomina en cambio delegación (un objeto delega en otro). Los patrones de herencia que modifican clases se pueden predefinir de acuerdo con parámetros de interfaz de red simples, de modo que se preserve la compatibilidad entre lenguajes. [2] [3]
La herencia no debe confundirse con la subtipificación . [4] [5] En algunos lenguajes, la herencia y la subtipificación concuerdan, [a] mientras que en otros 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 asegura la subtipificación conductual). Para distinguir estos conceptos, la subtipificación a veces se denomina 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 . [6] Aún así, la herencia es un mecanismo comúnmente utilizado para establecer relaciones de subtipo. [7]
La herencia se contrasta con la composición de objetos , donde un objeto contiene otro objeto (u objetos de una clase contienen objetos de otra clase); véase composición en lugar de herencia . La composición implementa una relación tiene-un , en contraste con la relación es-un de la subtipificación.
En 1966, Tony Hoare presentó algunas observaciones sobre los registros y, en particular, la idea de las subclases de registros, tipos de registros con propiedades comunes pero discriminados por una etiqueta de variante y que tienen campos privados para la variante. [8] 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 recopilaban en una superclase, y cada superclase podía tener potencialmente una superclase. Los valores de una subclase eran, por lo tanto, objetos compuestos, que consistían en un cierto número de partes de prefijo que pertenecían a varias superclases, más una parte principal que pertenecía a la subclase. Todas estas partes estaban concatenadas entre sí. [9] Los atributos de un objeto compuesto serían accesibles mediante la notación de puntos. Esta idea se adoptó por primera vez en el lenguaje de programación Simula 67. [10] La idea luego se extendió a Smalltalk , C++ , Java , Python y muchos otros lenguajes.
Existen varios tipos de herencia, según el paradigma y el lenguaje específico. [11]
"Se creía que la herencia múltiple era muy difícil de implementar de manera eficiente. Por ejemplo, en un resumen de C++ en su libro sobre Objective C , Brad Cox afirmó que agregar herencia múltiple a C++ era imposible. Por lo tanto, la herencia múltiple parecía un desafío mayor. Como yo había considerado la herencia múltiple ya en 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". [12]
—Bjarne Stroustrup
// Implementación del lenguaje C++ clase A { ... }; // Clase base clase B : public A { ... }; // B derivada de A clase C : public B { ... }; // C derivada de B
Las subclases , clases derivadas , clases herederas o clases hijas son clases derivadas modulares que heredan una o más entidades del lenguaje de una o más clases (llamadas superclase , clases base o clases padre ). La semántica de la herencia de clases varía de un lenguaje a otro, pero comúnmente la subclase hereda automáticamente las variables de instancia y las funciones miembro de sus superclases.
La forma general de definir una clase derivada es: [13]
clase SubClase : visibilidad SuperClase { // miembros de la subclase };
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 estar haciendo referencia en realidad 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 las funciones de la superclase con funciones completamente nuevas que deben compartir la misma firma de método .
En algunos lenguajes, una clase puede declararse como no subclasificable agregando ciertos modificadores de clase a la declaración de clase. Algunos ejemplos incluyen la final
palabra clave en Java y C++11 en adelante o la sealed
palabra clave en C#. Dichos modificadores se agregan a la declaración de clase antes de la class
palabra clave y la declaración del identificador de clase. Dichas clases no subclasificables restringen la reutilización , en particular cuando los desarrolladores solo tienen acceso a binarios precompilados y no al código fuente .
Una clase no subclasificable 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 (no existen) o instancias de superclases ( la conversión ascendente 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 según si se admite la herencia múltiple o solo la herencia simple en el lenguaje de programación que se está utilizando.
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 por una nueva función con el mismo nombre y firma de tipo en una subclase). Un método privado no es anulable simplemente porque no es accesible para clases distintas de la clase de la que es una función miembro (aunque esto no es cierto para C++). Un final
método en Java, un sealed
método en C# o una frozen
característica en Eiffel no se pueden anular.
Si un método de superclase es un método virtual , entonces las invocaciones del método de superclase se enviarán de forma dinámica . 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á de forma estática (es decir, la dirección de la llamada de 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 .
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++. [14]
La herencia se utiliza para correlacionar dos o más clases entre sí.
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 denomina 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 padre (base)? La respuesta varía entre los lenguajes de programación, y algunos lenguajes proporcionan la capacidad de indicar que un comportamiento particular no debe anularse y debe comportarse como lo define 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 métodos diferentes para anular otros métodos. [15] Una alternativa a la anulación es ocultar el código heredado.
La herencia de implementación es el mecanismo por el cual una subclase reutiliza el código de una clase base. De manera predeterminada, la subclase conserva todas las operaciones de la clase base, pero puede anular algunas o todas las operaciones y reemplazar la implementación de la clase base por la suya propia.
En el siguiente ejemplo de Python, las subclases SquareSumComputer y CubeSumComputer anulan el método transform() de la clase base SumComputer . La clase base incluye 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 por 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 __ ( self , a , b ) : self.a = a self.b = b def transform ( self , x ): genera un error no implementado def entradas ( self ) : rango de retorno ( self.a , self.b ) def calculate ( self ) : devuelve suma ( self.transform ( value ) para valor en self.inputs ( ) ) clase SquareSumComputer ( SumComputer ): def transform ( self , x ): devuelve x * xclase CubeSumComputer ( SumComputer ): def transform ( self , x ): devuelve x * x * x
En la mayoría de los sectores, la herencia de clases con el único propósito de reutilizar código ha caído en desuso. [ cita requerida ] La preocupación principal 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. [ cita requerida ] En C++, la herencia privada se puede utilizar como una forma de herencia de implementación sin sustituibilidad. Mientras que la herencia pública representa una relación "es-un" y la delegación representa una relación "tiene-un", la herencia privada (y protegida) puede considerarse como una relación "se implementa en términos de". [16]
Otro uso frecuente de la herencia es garantizar que las clases mantengan una cierta interfaz común; es decir, que implementen los mismos métodos. La clase padre puede ser una combinación de operaciones implementadas y operaciones que se implementarán en las clases hijas. A menudo, no hay cambio de interfaz entre el supertipo y el subtipo: la hija implementa el comportamiento descrito en lugar de su clase padre. [17]
La herencia es similar a la subtipificación, pero distinta de ella . [4] La subtipificación permite sustituir un tipo determinado por otro tipo o abstracción y se dice que establece una relación de tipo es-un entre el subtipo y alguna abstracción existente, ya sea de forma implícita o explícita, dependiendo del soporte del lenguaje. La relación se puede expresar explícitamente mediante la 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 tanto una subclase como un subtipo de A y se puede utilizar como una A dondequiera que se especifique una B (a través de una referencia, un puntero o el propio objeto).
clase A { público : void DoSomethingALike () const {} }; clase B : público A { público : void DoSomethingBlike () const {} }; void UseAnA ( const A & a ) { a.DoSomethingALike ( ) ; } void 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 totalmente posible derivar una clase cuyo objeto se comportará incorrectamente cuando se usa en un contexto donde se espera la clase padre; consulte el principio de sustitución de Liskov . [18] (Compare connotación/denotación ). En algunos lenguajes OOP, las nociones de reutilización de código y subtipificación coinciden porque la única forma de declarar un subtipo es definir una nueva clase que herede la implementación de otra.
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 cargo, 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:
El principio de reutilización compuesta es una alternativa a la herencia. Esta técnica admite el polimorfismo y la reutilización de código al separar los comportamientos de la jerarquía de clases primaria e incluir clases de comportamiento específicas según sea necesario en cualquier clase de dominio empresarial. Este enfoque evita la naturaleza estática de una jerarquía de clases al permitir modificaciones de comportamiento en tiempo de ejecución y permite que una clase implemente comportamientos de manera aleatoria, en lugar de estar restringida a los comportamientos de sus clases antecesoras.
La herencia de implementación ha sido 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 interfaz en su lugar y favorecen la composición sobre la herencia. Por ejemplo, el patrón decorador (como se mencionó anteriormente) se ha propuesto para superar la naturaleza estática de la herencia entre clases. Como una 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. [ cita requerida ]
Según Allen Holub , el principal problema con la herencia de implementación es que introduce un acoplamiento innecesario en forma del "problema de la clase base frágil" : [6] las modificaciones a 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. [19] Otra forma de decirlo es que "la herencia rompe la encapsulación ". [20] El problema surge claramente en sistemas abiertos orientados a objetos como los frameworks , 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. [6]
Según se informa, el inventor de Java, James Gosling, se ha pronunciado en contra de la herencia de implementación, afirmando que no la incluiría si tuviera que rediseñar Java. [19] Los diseños de lenguajes que desacoplan la herencia de la subtipificación (herencia de interfaz) aparecieron ya en 1990; [21] 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 conducir al problema del yo-yo . Cuando la herencia se utilizó como un enfoque principal para estructurar programas a fines de la década de 1990, los desarrolladores tendían 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, con muchas capas que consistían en solo 1 o 2 líneas de código real. [ cita requerida ] Demasiadas capas hacen que la depuración sea un desafío significativo, ya que se vuelve 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.