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 ), 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 amigos. funciones 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 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 usa 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), siendo la técnica correspondiente en la programación basada en prototipos en lugar de eso se llama 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. [2] [3]

La herencia no debe confundirse con la subtipificación . [4] [5] En algunos idiomas, la herencia y los subtipos coinciden, [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 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 . [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 (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. [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 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. [9] 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. [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 suponía que la herencia múltiple  ... era muy difícil de implementar eficientemente. Por ejemplo, en un resumen de C++ en su libro sobre Objective C , Brad Cox en realidad afirmó que agregar herencia múltiple a C++ era imposible. Por lo tanto, la herencia múltiple parecía Más que un desafío. Dado que 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]

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 vínculo para la herencia entre A y C. La cadena ABC se conoce como camino de herencia .
Una clase derivada con herencia multinivel se declara de la siguiente manera:
// clase de implementación del lenguaje C++ A { ... }; // clase base clase B : public A { ... }; // B derivado de A clase C : public B { ... }; // C derivado de B                     
Este proceso se puede extender a cualquier número de niveles.
Herencia jerárquica
Aquí es donde una clase sirve como superclase (clase base) para más de una subclase. Por ejemplo, una clase principal, A, puede tener dos subclases B y C. La clase principal de B y C es A, pero B y C son dos subclases separadas.
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 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

Las subclases , clases derivadas , clases herederas o clases secundarias son clases derivadas modulares que heredan una 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: [13]

clase Subclase : visibilidad SuperClase { // miembros de la subclase };    

Algunos lenguajes también admiten la herencia de otras construcciones. Por ejemplo, en Eiffel , los contratos que definen la especificación de una clase también son heredados por los herederos. 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 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. Estas clases que no se pueden subclasificar restringen la reutilización , particularmente 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 (no existen) o instancias de superclases ( actualizar 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étodos 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 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 , 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 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 del método anulado

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. [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. 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 el método transform() 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  SumaComputadora :  def  __init__ ( self ,  a ,  b ):  self . a  =  un  yo . segundo  =  segundo def  transformar ( self ,  x ):  generar  NotImplementedError  entradas def ( self ):  rango de retorno  ( self . a , self . b )  def  calcular ( self ):  devuelve  la suma ( self . transform ( valor )  para  el valor  en  self . inputs ())clase  SquareSumComputer ( SumComputer ):  def  transformar ( self ,  x ):  devolver  x  *  xclase  CubeSumComputer ( SumComputer ):  def  transformar ( self ,  x ):  devolver  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. [ cita necesaria ] 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. [ cita necesaria ] 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". [dieciséis]

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. [17]

Herencia versus subtipificación

La herencia es similar pero distinta de la subtipificación . [4] La subtipificación permite sustituir un tipo determinado por otro tipo o abstracción y se dice que establece una relación es-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 A y puede usarse como A siempre que se especifique B (mediante una referencia, un puntero o el objeto en sí).

clase A { público : void HacerAlgoComo () const {} };       clase B : público A { público : void DoSomethingBLike () const {} };          void UseAnA ( const A & a ) { a . Hacer algo parecido (); }     void SomeFunc () { B b ; UtiliceAnA ( 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 la 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 . [18] (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 al diseñar un programa impone ciertas restricciones.

Por ejemplo, considere una clase Persona que contiene el nombre, fecha de nacimiento, dirección y 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, empleador y salario de la persona.

Al definir esta jerarquía de herencia ya hemos definido ciertas restricciones, no todas ellas deseables:

Soltería
Al utilizar la herencia única, una subclase puede heredar de una sola superclase. Siguiendo con el ejemplo anterior, un objeto Persona puede ser Estudiante o Empleado , pero no ambos. El uso de herencia múltiple resuelve parcialmente este problema, ya que luego se puede definir una clase StudentEmployee que hereda tanto de Student como de Employee . Sin embargo, en la mayoría de las implementaciones, aún 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 lo hace posible mediante el soporte de herencia repetida .
Estático
La jerarquía de herencia de un objeto se fija en la instanciación cuando se selecciona el tipo de objeto y no cambia con el tiempo. Por ejemplo, el gráfico de herencia no permite que un objeto Estudiante se convierta en un objeto Empleado manteniendo el estado de su superclase Persona . (Este tipo de comportamiento, sin embargo, se puede lograr con el patrón decorador ). Algunos han criticado la herencia, sosteniendo que bloquea 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 superclase del objeto. Incluso si la superclase no ha sido declarada pública, el cliente aún puede convertir el objeto a su tipo de superclase. Por ejemplo, no hay forma de darle a una función un puntero al promedio de calificaciones y al expediente académico de un Estudiante sin darle también acceso a esa función 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 estilo buffet, en lugar de restringirse a los comportamientos de sus clases antecesoras.

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, interpretada por , que combina propiedades de herencia y composición en un nuevo concepto. [ cita necesaria ]

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" : [6] las modificaciones a la implementación de la clase base pueden causar cambios de comportamiento inadvertidos en las subclases. El uso de interfaces evita este problema porque no se comparte ninguna implementación, solo la API. [19] Otra forma de decir esto 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 y afirmó que no la incluiría si rediseñara Java. [19] Los diseños de lenguaje 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 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. [ cita necesaria ] Demasiadas capas hacen que la depuración sea un desafío importante, ya que resulta difícil determinar qué capa debe depurarse.

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.

Ver también

Notas

  1. ^ Esto generalmente es cierto solo en lenguajes orientados a objetos basados ​​en clases de tipo 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 poderoso mecanismo en 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. S2CID  1104130.
  3. ^ Davies, turco (2021). Métodos Avanzados y Aprendizaje Profundo en Visión por Computador . Ciencia Elsevier. págs. 179–342.
  4. ^ ab Cook, William R.; colina, Walter; Enlatado, Peter S. (1990). La herencia no es subtipificación . Actas del 17º Simposio ACM SIGPLAN-SIGACT sobre principios de lenguajes de programación (POPL). págs. 125-135. CiteSeerX 10.1.1.102.8635 . doi :10.1145/96709.96721. ISBN  0-89791-343-4.
  5. ^ Cardelli, Luca (1993). Programación tipográfica (Informe técnico). Corporación de equipos digitales . pag. 32–33. Informe de investigación del SRC 45.
  6. ^ abc Mikhajlov, Leonid; Sekerinski, Emil (1998). Un estudio del frágil problema de la clase base (PDF) . Actas de la 12ª Conferencia Europea sobre Programación Orientada a Objetos (ECOOP). Apuntes de conferencias sobre informática. vol. 1445. Saltador. págs. 355–382. doi :10.1007/BFb0054099. ISBN 978-3-540-64737-9. Archivado 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 conferencias sobre informática. vol. 7920. Saltador. págs. 577–601. doi :10.1007/978-3-642-39038-8_24. ISBN 978-3-642-39038-8.
  8. ^ Hoare, COCHE (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) . Jornada de trabajo IFIP sobre lenguajes de simulación. Oslo: Centro de Computación de Noruega.
  10. ^ Dahl, Ole-Johan (2004). "El nacimiento de la orientación a objetos: los lenguajes Simula" (PDF) . Apuntes de conferencias sobre informática. vol. 2635, págs. 15-25. doi :10.1007/978-3-540-39993-3_3. ISBN 978-3-540-21366-6. {{cite book}}: |journal=ignorado ( ayuda ) ; Falta o está vacío |title=( ayuda )
  11. ^ "Herencia de C++". www.cs.nmsu.edu .
  12. ^ Stroustrup, Bjarne (1994). El diseño y evolución de C++ . Pearson. pag. 417.ISBN 9780135229477.
  13. ^ Schildt, Herbert (2003). La referencia completa C++ . Tata McGraw Hill. pag. 417.ISBN 978-0-07-053246-5.
  14. ^ Balagurusamy, E. (2010). Programación orientada a objetos con C++ . Tata McGraw Hill. pag. 213.ISBN 978-0-07-066907-9.
  15. ^ anular (referencia de C#)
  16. ^ "GotW n.° 60: Diseño de clases seguras para 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). "Por qué se extiende es malo" . Consultado el 10 de marzo de 2015 .
  20. ^ Seiter, Linda M.; Palsberg, Jens; Lieberherr, Karl J. (1996). "Evolución del comportamiento de los objetos mediante relaciones de contexto". Notas de ingeniería de software de ACM SIGSOFT . 21 (6): 46. CiteSeerX 10.1.1.36.5053 . doi :10.1145/250707.239108. 
  21. ^ América, Pierre (1991). Diseño de un lenguaje de programación orientado a objetos con subtipos de comportamiento. Escuela/Taller REX sobre los fundamentos de los lenguajes orientados a objetos. Apuntes de conferencias sobre informática. vol. 489, págs. 60–90. doi :10.1007/BFb0019440. ISBN 978-3-540-53931-5.

Otras lecturas