Cuándo utilizar la herencia

Actualización: noviembre 2007

La herencia es un concepto de programación útil, pero es fácil utilizarlo de manera poco adecuada. A menudo, las interfaces solucionan mejor la situación. Este tema y Cuándo se deben utilizar interfaces le ayudan a entender cuándo se debe utilizar cada enfoque.

La herencia es una buena opción cuando:

  • La jerarquía de herencia representa una relación de identidad y no una relación de pertenencia.

  • Se puede volver a utilizar código de las clases base.

  • Es necesario aplicar la misma clase y los mismos métodos a tipos de datos diferentes.

  • La jerarquía de clases es poco profunda y es poco probable que otros programadores agreguen muchos más niveles.

  • Desea realizar cambios globales en clases derivadas modificando una clase base.

Estas consideraciones se explican en este orden a continuación.

Herencia y relaciones de identidad

Dos maneras de mostrar las relaciones de clase en la programación orientada a objetos son las relaciones de identidad y de pertenencia. En una relación de identidad, la clase derivada es claramente un tipo de clase base. Por ejemplo, una clase denominada PremierCustomer representa una relación de tipo identidad con una clase base denominada Customer, puesto que un cliente principal es un cliente. No obstante, una clase denominada CustomerReferral representa una relación de pertenencia con la clase Customer porque una cartera de clientes tiene un cliente, pero no es un tipo de cliente.

Los objetos de una jerarquía de herencia deben tener una relación de identidad con la clase base puesto que heredan los campos, propiedades, métodos y eventos definidos en dicha clase. Las clases que representan una relación de pertenencia con otras clases no son válidas para jerarquías de herencia debido a que podrían heredar propiedades y métodos inadecuados. Por ejemplo, si la clase CustomerReferral se derivase de la clase Customer descrita anteriormente, podría heredar propiedades que no tendrían sentido, como ShippingPrefs y LastOrderPlaced. Las relaciones de pertenencia como ésta deben representarse mediante clases o interfaces no relacionadas. La siguiente ilustración muestra ejemplos de relaciones de tipo "es un" y "tiene un".

Clases base y reutilización de código

Otra razón para usar la herencia es la ventaja de poder reutilizar el código. Las clases bien diseñadas, una vez depuradas, pueden utilizarse una y otra vez como base de nuevas clases.

Un ejemplo común de reutilización eficaz de código está relacionado con bibliotecas que administran estructuras de datos. Por ejemplo, suponga que tiene una gran aplicación comercial que administra varias clases de listas en la memoria. Una es una copia en memoria de la base de datos de clientes, que se lee desde una base de datos al iniciar la sesión para conseguir mayor velocidad. La estructura de datos tendría un aspecto similar al siguiente:

Class CustomerInfo
    Protected PreviousCustomer As CustomerInfo
    Protected NextCustomer As CustomerInfo
    Public ID As Integer
    Public FullName As String

    Public Sub InsertCustomer(ByVal FullName As String)
        ' Insert code to add a CustomerInfo item to the list.
    End Sub

    Public Sub DeleteCustomer()
        ' Insert code to remove a CustomerInfo item from the list.
    End Sub

    Public Function GetNextCustomer() As CustomerInfo
        ' Insert code to get the next CustomerInfo item from the list.
        Return NextCustomer
    End Function

    Public Function GetPrevCustomer() As CustomerInfo
        'Insert code to get the previous CustomerInfo item from the list.
        Return PreviousCustomer
    End Function
End Class

La aplicación también podría tener una lista similar de productos que el usuario ha agregado a una lista del carro de la compra, como se muestra en el siguiente fragmento de código:

Class ShoppingCartItem
    Protected PreviousItem As ShoppingCartItem
    Protected NextItem As ShoppingCartItem
    Public ProductCode As Integer
    Public Function GetNextItem() As ShoppingCartItem
        ' Insert code to get the next ShoppingCartItem from the list.
        Return NextItem
    End Function
End Class

Aquí puede ver un modelo: dos listas se comportan del mismo modo (inserciones, eliminaciones y recuperaciones) pero funcionan con tipos de datos diferentes. Mantener dos bases de código para realizar esencialmente las mismas funciones no es eficaz. La solución más eficaz consiste en separar la administración de listas en su propia clase y después heredar de esa clase para diferentes tipos de datos:

Class ListItem
    Protected PreviousItem As ListItem
    Protected NextItem As ListItem
    Public Function GetNextItem() As ListItem
        ' Insert code to get the next item in the list.
        Return NextItem
    End Function
    Public Sub InsertNextItem()
        ' Insert code to add a item to the list.
    End Sub

    Public Sub DeleteNextItem()
        ' Insert code to remove a item from the list.
    End Sub

    Public Function GetPrevItem() As ListItem
        'Insert code to get the previous item from the list.
        Return PreviousItem
    End Function
End Class

La clase ListItem sólo tiene que ser depurada una vez. Después podrá generar clases que la utilicen sin tener que pensar nunca más sobre la administración de listas. Por ejemplo:

Class CustomerInfo
    Inherits ListItem
    Public ID As Integer
    Public FullName As String
End Class
Class ShoppingCartItem
    Inherits ListItem
    Public ProductCode As Integer
End Class

Aunque la reutilización de código basado en la herencia es una herramienta eficaz, también tiene riesgos asociados. Incluso los sistemas mejor diseñados cambian a veces de tal modo que los diseñadores no podrían prever. A veces, los cambios en una jerarquía de clases existente pueden tener consecuencias no deseadas; en la sección Cambios en el diseño de la clase base después de la implementación de "El problema de fragilidad de la clase base" se describen algunos ejemplos.

Clases derivadas intercambiables

Las clases derivadas de una jerarquía de clases pueden a veces intercambiarse con la clase base, un proceso denominado polimorfismo basado en la herencia. Este enfoque combina las mejores características del polimorfismo basado en la interfaz con la opción de reutilizar o reemplazar código de una clase base.

Así, esto podría ser útil en un paquete de dibujo. Por ejemplo, considere el siguiente fragmento de código, que no utiliza herencia:

Sub Draw(ByVal Shape As DrawingShape, ByVal X As Integer, _
    ByVal Y As Integer, ByVal Size As Integer)

    Select Case Shape.type
        Case shpCircle
            ' Insert circle drawing code here.
        Case shpLine
            ' Insert line drawing code here.
    End Select
End Sub

Este enfoque presenta algunos problemas. Si alguien decide agregar una opción de elipse posteriormente, será necesario modificar el código fuente; puede ocurrir que los usuarios a los que va dirigido ni siquiera tengan acceso al código fuente. Un problema más sutil consiste en que para dibujar una elipse se necesita otro parámetro (las elipses tienen un diámetro principal y uno secundario) que no sería relevante para el caso de la línea. Si alguien desea agregar una polilínea (múltiples líneas conectadas), entonces se agregaría otro parámetro que no sería relevante para otros casos.

La herencia resuelve la mayoría de estos problemas. Las clases base bien diseñadas dejan la implementación de métodos específicos para las clases derivadas, de modo que se pueda incluir cualquier forma. Otros programadores pueden implementar métodos en clases derivadas con la documentación de la clase base. Otros elementos de clase (como las coordenadas x e y) se pueden integrar en la clase base porque todas las descendientes los utilizan. Por ejemplo, Draw podría ser un método MustOverride:

MustInherit Class Shape
    Public X As Integer
    Public Y As Integer
    MustOverride Sub Draw()
End Class

Después podría agregar los elementos necesarios a esa clase para las diferentes formas. Por ejemplo, una clase Line podría necesitar únicamente un campo Length:

Class Line
    Inherits Shape
    Public Length As Integer
    Overrides Sub Draw()
        ' Insert code here to implement Draw for this shape.
    End Sub
End Class

Este planteamiento es útil debido a que otros programadores que no tienen acceso al código fuente pueden extender la clase base con nuevas clases derivadas según sea necesario. Por ejemplo, una clase denominada Rectangle podría derivarse de la clase Line:

Class Rectangle
    Inherits Line
    Public Width As Integer
    Overrides Sub Draw()
        ' Insert code here to implement Draw for the Rectangle shape.
    End Sub
End Class

En este ejemplo se muestra cómo se puede pasar de clases de propósito general a clases muy específicas mediante la implementación de detalles en cada nivel.

En este punto podría ser conveniente volver a evaluar si la clase derivada realmente representa una relación de identidad o, por el contrario, es una relación de pertenencia. Si la nueva clase de rectángulo se compone solamente de líneas, entonces la herencia no es la mejor opción. No obstante, si el nuevo rectángulo es una línea con una propiedad de ancho, entonces se mantiene la relación de identidad.

Jerarquías de clases poco profundas

La herencia se adapta mejor a jerarquías de clases relativamente poco profundas. Las jerarquías de clases complejas y profundas en exceso pueden ser difíciles de desarrollar. La decisión de utilizar una jerarquía de clases implica sopesar sus ventajas y su complejidad. Como norma general, las jerarquías deberían limitarse a seis niveles o menos. No obstante, la profundidad máxima de una jerarquía de clases concreta depende de varios factores, incluida la complejidad de cada nivel.

Cambios globales en clases derivadas a través de la clase base

Una de las características más eficaces de la herencia es la posibilidad de realizar cambios en una clase base que se propagan a las clases derivadas. Si se usa con cuidado, puede actualizarse la implementación de un solo método y decenas, e incluso cientos, de clases derivadas podrán utilizar el nuevo código. No obstante, esta práctica puede resultar peligrosa puesto que tales cambios podrían generar problemas en clases heredadas diseñadas por otras personas. Debe tenerse cuidado para asegurarnos de que la nueva clase es compatible con las clases que utilizan la original. Concretamente, debe evitarse cambiar el nombre o el tipo de los miembros de la clase base.

Suponga, por ejemplo, que diseña una clase base con un campo de tipo Integer para almacenar la información de código postal, y que otros programadores han creado clases derivadas que utilizan el campo de código postal heredado. Suponga además que el campo de código postal almacena cinco dígitos y que la oficina de correos ha ampliado los códigos postales con un guión y cuatro dígitos adicionales. En el peor de los casos, podría modificar el campo en la clase base para almacenar una cadena de diez caracteres, pero otros programadores tendrían que cambiar y volver a compilar las clases derivadas para utilizar el tamaño y el tipo de datos nuevos.

La forma más segura de cambiar una clase base es simplemente agregar nuevos miembros. Por ejemplo, podría agregar un nuevo campo para almacenar cuadro dígitos más en el ejemplo de código postal descrito anteriormente. De esta forma, las aplicaciones cliente pueden actualizarse para que utilicen el nuevo campo sin interrumpir las aplicaciones existentes. Esta posibilidad de extender clases base en una jerarquía de herencia es una ventaja importante que no existe con interfaces.

Vea también

Conceptos

Cuándo se deben utilizar interfaces

Cambios en el diseño de la clase base después de la implementación