stringtranslate.com

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 ), 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.

Historia

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.

Tipos

Herencia única
Herencia múltiple

Existen varios tipos de herencia, según el paradigma y el lenguaje específico. [11]

Herencia única
donde las subclases heredan las características de una superclase. Una clase adquiere las propiedades de otra clase.
Herencia múltiple
donde una clase puede tener más de una superclase y heredar características de todas las clases principales.

"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
Herencia multinivel
donde una subclase se hereda de otra subclase. No es raro que una clase se derive de otra clase derivada como se muestra en la figura "Herencia multinivel".
Herencia multinivel
La clase A sirve como clase base para la clase derivada B , que a su vez sirve como clase base para la clase derivada C . La clase B se conoce como clase base intermedia porque proporciona un enlace para la herencia entre A y C . La cadena ABC se conoce como ruta de herencia .
Una clase derivada con herencia multinivel se declara de la siguiente manera:
// 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                     
Este proceso puede extenderse a cualquier número de niveles.
Herencia jerárquica
En este caso, una clase actúa como superclase (clase base) para más de una subclase. Por ejemplo, una clase padre, A, puede tener dos subclases, B y C. La clase padre de B y C es A, pero B y C son dos subclases independientes.
Herencia híbrida
La herencia híbrida se produce cuando se produce una combinación de dos o más de los tipos de herencia mencionados anteriormente. Un ejemplo de esto es cuando una clase A tiene una subclase B que a su vez tiene dos subclases, C y D. Se trata de una combinación de herencia multinivel y herencia jerárquica.

Subclases y superclases

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 .

Clases no subclasificables

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 finalpalabra clave en Java y C++11 en adelante o la sealedpalabra clave en C#. Dichos modificadores se agregan a la declaración de clase antes de la classpalabra 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.

Métodos no reemplazables

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 finalmétodo en Java, un sealedmétodo en C# o una frozencaracterística en Eiffel no se pueden anular.

Métodos virtuales

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 .

Visibilidad de los miembros heredados

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]

Aplicaciones

La herencia se utiliza para correlacionar dos o más clases entre sí.

Primordial

Ilustración de la anulación del método

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.

Reutilización de código

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]

Herencia vs. subtipificación

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.

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 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:

Soltería
Si se utiliza la herencia simple, una subclase puede heredar de una sola superclase. Siguiendo con el ejemplo anterior, un objeto Person puede ser Student o Employee , pero no ambos. El uso de la herencia múltiple resuelve parcialmente este problema, ya que se puede definir una clase StudentEmployee que herede tanto de Student como de Employee . Sin embargo, en la mayoría de las implementaciones, todavía puede heredar de cada superclase solo una vez y, por lo tanto, no admite casos en los que un estudiante tiene dos trabajos o asiste a dos instituciones. El modelo de herencia disponible en Eiffel hace esto posible mediante el soporte para la herencia repetida .
Estático
La jerarquía de herencia de un objeto se fija en la instanciación cuando se selecciona el tipo del objeto y no cambia con el tiempo. Por ejemplo, el gráfico de herencia no permite que un objeto Student se convierta en un objeto Employee mientras retiene el estado de su superclase Person . (Este tipo de comportamiento, sin embargo, se puede lograr con el patrón decorador ). Algunos han criticado la herencia, argumentando que encierra a los desarrolladores en sus estándares de diseño originales. [19]
Visibilidad
Siempre que el código del cliente tiene acceso a un objeto, generalmente tiene acceso a todos los datos de la superclase del objeto. Incluso si la superclase no se ha declarado pública, el cliente puede convertir el objeto a su tipo de superclase. Por ejemplo, no hay forma de dar a una función un puntero al promedio de calificaciones y al expediente académico de un estudiante sin dar también a esa función acceso a todos los datos personales almacenados en la superclase Persona del estudiante . Muchos lenguajes modernos, incluidos C++ y Java, proporcionan un modificador de acceso "protegido" que permite que las subclases accedan a los datos, sin permitir que ningún código fuera de la cadena de herencia acceda a ellos.

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.

Problemas y alternativas

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.

Véase también

Notas

  1. ^ Esto generalmente es cierto solo en lenguajes OO basados ​​en clases con tipado estático, como C++ , C# , Java y Scala .

Referencias

  1. ^ Johnson, Ralph (26 de agosto de 1991). "Diseño de clases reutilizables" (PDF) . www.cse.msu.edu .
  2. ^ Madsen, OL (1989). "Clases virtuales: un mecanismo poderoso en la programación orientada a objetos". Actas de congresos sobre sistemas, lenguajes y aplicaciones de programación orientada a objetos - OOPSLA '89 . págs. 397–406. doi :10.1145/74877.74919. ISBN 0897913337.S2CID1104130  .​
  3. ^ Davies, Turk (2021). Métodos avanzados y aprendizaje profundo en visión artificial . Elsevier Science. págs. 179–342.
  4. ^ ab Cook, William R.; Hill, Walter; Canning, Peter S. (1990). La herencia no es subtipificación . Actas del 17.º Simposio ACM SIGPLAN-SIGACT sobre Principios de Lenguajes de Programación (POPL). pp. 125–135. CiteSeerX 10.1.1.102.8635 . doi :10.1145/96709.96721. ISBN .  0-89791-343-4.
  5. ^ Cardelli, Luca (1993). Typeful Programming (Informe técnico). Digital Equipment Corporation . p. 32–33. Informe de investigación de SRC 45.
  6. ^ abc Mikhajlov, Leonid; Sekerinski, Emil (1998). Un estudio del problema de la clase base frágil (PDF) . Actas de la 12.ª Conferencia Europea sobre Programación Orientada a Objetos (ECOOP). Apuntes de clase en informática. Vol. 1445. Springer. págs. 355–382. doi :10.1007/BFb0054099. ISBN. 978-3-540-64737-9Archivado desde el original (PDF) el 13 de agosto de 2017. Consultado el 28 de agosto de 2015 .
  7. ^ Tempero, Ewan; Yang, Hong Yul; Noble, James (2013). Qué hacen los programadores con la herencia en Java (PDF) . ECOOP 2013–Programación orientada a objetos. Apuntes de clase en informática. Vol. 7920. Springer. págs. 577–601. doi :10.1007/978-3-642-39038-8_24. ISBN 978-3-642-39038-8.
  8. ^ Hoare, CAR (1966). Manejo de registros (PDF) (Informe técnico). Págs. 15-16.
  9. ^ Dahl, Ole-Johan ; Nygaard, Kristen (mayo de 1967). Declaraciones de clases y subclases (PDF) . Conferencia de trabajo de la IFIP sobre lenguajes de simulación. Oslo: Centro Noruego de Computación.
  10. ^ Dahl, Ole-Johan (2004). "El nacimiento de la orientación a objetos: los lenguajes Simula" (PDF) . De la orientación a objetos a los métodos formales . Lecture Notes in Computer Science. Vol. 2635. págs. 15-25. doi :10.1007/978-3-540-39993-3_3. ISBN . 978-3-540-21366-6.
  11. ^ "Herencia de C++". www.cs.nmsu.edu . Archivado desde el original el 24 de septiembre de 2023 . Consultado el 16 de mayo de 2018 .
  12. ^ Stroustrup, Bjarne (1994). El diseño y la evolución de C++ . Pearson. pág. 417. ISBN. 9780135229477.
  13. ^ Schildt, Herbert (2003). La referencia completa de C++ . Tata McGraw Hill. pág. 417. ISBN 978-0-07-053246-5.
  14. ^ Balagurusamy, E. (2010). Programación orientada a objetos con C++ . Tata McGraw Hill. pág. 213. ISBN 978-0-07-066907-9.
  15. ^ override(Referencia de C#)
  16. ^ "GotW #60: Diseño de clases seguro frente a excepciones, parte 2: herencia". Gotw.ca. Consultado el 15 de agosto de 2012 .
  17. ^ Venugopal, KR; Buyya, Rajkumar (2013). Dominar C++ . Tata McGraw Hill Education Private Limited. pag. 609.ISBN 9781259029943.
  18. ^ Mitchell, John (2002). "10 "Conceptos en lenguajes orientados a objetos"". Conceptos en lenguaje de programación . Cambridge University Press. p. 287. ISBN 978-0-521-78098-8.
  19. ^ abc Holub, Allen (1 de agosto de 2003). «Why extends is evil» (Por qué extender es malo). Archivado desde el original el 24 de febrero de 2019. Consultado el 10 de marzo de 2015 .
  20. ^ Seiter, Linda M.; Palsberg, Jens; Lieberherr, Karl J. (1996). "Evolución del comportamiento de objetos utilizando relaciones de contexto". ACM SIGSOFT Software Engineering Notes . 21 (6): 46. CiteSeerX 10.1.1.36.5053 . doi :10.1145/250707.239108. 
  21. ^ America, Pierre (1991). Diseño de un lenguaje de programación orientado a objetos con subtipificación conductual. REX School/Taller sobre los fundamentos de los lenguajes orientados a objetos. Apuntes de clase en informática. Vol. 489. págs. 60–90. doi :10.1007/BFb0019440. ISBN 978-3-540-53931-5.

Lectura adicional