Simular la herencia en Visual Basic

por Guillermo "guille" Som

 

Publicado el 10/Dic/98 (en esa fecha es cuando se tendría que haber publicado "oficialmente")
Revisado el 30/Dic/98 (o en esta otra, pero... nada de nada...)
¡Por fin! el 24/Feb/99 (Se supone que esta es la correcta)

Por favor lee la Nota del 29/Ago/2003



Es un tema muy trillado este de la "herencia" en el Visual Basic. Pero a pesar de todo lo que se diga, aunque te lo pinten de color de rosa, lo cierto es que Visual Basic no tiene herencia... y no me refiero a que no sigan apareciendo nuevas versiones de este lenguaje, sino a la herencia de la Programación Orientada a Objetos (OOP en inglés).
La verdad es que herencia, lo que se dice herencia, el Visual Basic no tiene, ni tampoco el Delphi, ya que estamos, (aunque no me gusta meter cizaña, todo hay que decirlo... je); al menos si lo miramos desde el punto de vista de los programadores de C++ o de los puristas de la OOP.
Ya te digo que todo este tema está muy trillado y ya se ha comentado en diferentes ocasiones, incluso creo que hasta yo ya lo he tratado antes...
Lo que importa, es saber cómo "intentar" simular esa herencia en Visual Basic... o como mínimo saber hasta dónde podemos con nuestro querido VB, al menos con las versiones 5 y posteriores, (hasta el momento la 6ª); puede que en futuras versiones sea de otra forma... todo se andará.

Un poco de historia... la mía

Este artículo lo tendría que haber publicado en mayo de este año (1998), que es cuando me puse a "estudiar" lo que el manual del Borland C++ 2.0 decía sobre esto de la herencia. El citado manual está fechado en 1991 y básicamente dice lo mismo que en otro publicado anteriormente, cuando apareció la primera versión del Turbo C++, no es que esto de la herencia sea un invento de la gente de Borland... pero por aquella época, (mayo del 91), era un compilador económico... y era mi única "fuente de inspiración".

Bueno, al tema... Leyendo sobre la herencia, intenté adaptar la información obtenida al Visual Basic, incluso intenté crear un generador de clases heredadas...
Resultado: No se puede adaptar...
Motivo: porque el VB no lo permite.
¿Se puede simular? Un poco, pero se pierde la razón de ser de la herencia... es decir, tendremos que "simularla" en la mayoría de las ocasiones...

Veamos lo que se puede hacer con el C++, al menos en la forma básica de la herencia, que es la que mi "coco" llega a entender.

Los siguientes ejemplos están sacados del manual "Getting started", capítulo 4, "A C++ primer" del compilador Borland C++ 2.0
Ahora veamos lo que decía Borland en la página 63 del citado capitulo sobre

La herencia

Las clases normalmente no existen en la nada. Un programa normalmente tiene que trabajar con varias estructuras de datos diferentes pero relacionadas. Por ejemplo, podrías tener un Buffer de memoria en el que poder almacenar y recuperar cierta información. Más tarde, puedes necesitar unos búferes más especializados: un buffer de ficheros que se use para transferir información a ficheros de disco, y quizás un buffer que contenga información para impresora, y otro que maneje la información enviada y recibida de un módem. Estos búferes especializados, claramente tienen muchas características en común, pero cada uno tiene sus diferencias debido que se manejan de forma diferente los ficheros, impresoras y modems.
La solución C++ a estas "parecidas pero diferentes" situaciones es la de permitir a las clases heredar características y conductas de una o más clases base. Este es un salto intuitivo; la herencia es quizás la pequeña gran diferencia entre C++ y C. Las clases que han heredado de las clases base se llaman clases derivadas. Una clase derivada puede ser asimismo la clase base de la que pueden derivar otras clases. [...]

Al final hacen referencia, para explicar lo de la herencia, a un ejemplo sobre la familia de los insectos, en el que la raíz serían los insectos, de este salen dos ramas: los insectos con alas y sin alas, de los insectos alados, salen a su vez una serie de ramas...
Hablando sobre este ejemplo, en la página 49 del mismo manual, dicen:

Una vez que una característica está definida, todas las características bajo esa definición incluyen esa características. Si identificas un insecto como miembro de los dípteros, por ejemplo una mosca, no necesitas puntualizar que tiene un par de alas. La especie mosca hereda esa característica.
La OOP es el proceso de crear clases jerárquicas. Una de las cosas importantes que añade C++ a C es un mecanismo por el cual las clases pueden heredar características de una más simple y general. Este mecanismo es llamado herencia. La herencia provee un funcionamiento común a la vez que permite tanta especialización como se necesite. [...]

Resumiendo: una clase "heredada" de otra clase... hereda todas las características de la clase base, pero permite adaptarse a sus propias características... ¿?

Veámoslo con un ejemplo: este es uno de los que dan en el mencionado capítulo "A C++ primer".
Tenemos una clase gráfica que muestra un punto en la pantalla, entre algunos de los métodos que tiene, están: Show, Hide y MoveTo que sirven para mostrar, ocultar y mover un punto a otra posición de la pantalla.
Tenemos otra clase: círculo, que se deriva de punto, es decir hereda la clase punto, por tanto también hereda los métodos indicados, los cuales tendrán el mismo fin: mostrar, ocultar y mover a otra posición, pero en lugar de un sólo punto, tendrá que hacerlo con un círculo.
Osea círculo hereda los métodos de punto, pero los adapta a sus necesidades, ya que no es lo mismo mostrar un punto que dibujar un círculo... en esto estamos de acuerdo ¿verdad?
Porque lo que internamente haga el método Show de la clase punto, no será lo mismo que ese mismo método de círculo.
Y tu dirás... pues hagamos un método diferente para cada clase, pero con el mismo nombre... y eso es lo que hay que hacer... aunque con el Visual Basic se hace de forma diferente a como se haría en C++ o casi... al menos se usaría de forma diferente.
En C++ la clase "circle" se definiría de esta forma: (según el manual de C++ mencionado)

class Circle : Point {    //derivada de la clase Point
    int Radius;
public:
    Circle (int InitX, int InitY, int InitRadius);    //este sería el constructor de la clase
    void Show(void);            //métodos heredados de la clase Point
    void Hide(void);
    void MoveTo(int NewX, int NewY);
    void Expand(int ExpandBy);        //métodos propios a Circle
    void Contract(int ContractBy);
};

A continuación vendría el código de esas funciones para dibujar un círculo.

La clase Point se definiría de esta forma...
Hay que decir que Point hereda ciertas propiedades de otra clase base: Location, la cual simplemente contiene la posición de un punto en la pantalla.
En el ejemplo mencionado, la case Location tiene "protegidas" las propiedades X e Y, es decir que no son visibles a una aplicación externa, pero si lo son para las clases derivadas de ella, esto en VB no es posible, ya que sólo se pueden "heredar" los métodos y propiedades públicas. Este tipo de propiedades protegidas, en Visual Basic se podrían simular usando Friend, ya que una propiedad o método Friend es visible dentro del mismo proyecto, pero como he dicho no se pueden heredar...

Vamos a ver cómo se declaran las clases Location y Point en C++ (siempre según los listados del manual mencionado)

class Location {
protected:    //permite a las clases derivadas acceder a los datos privados
    int X;
    int Y;
public:        //estas funciones son accesibles desde fuera
    Location (int InitX, int InitY);
    int GetX();
    int GetY();
};

class Point : public Location { //derivada de location
    //las derivación pública significa que X e Y están protegidas en Point
    //y aunque no se "declaren", pueden usarse como si lo estuviesen
    //ya que se han heredado de Location.
    //-** esto no es posible en VB **-
protected:
    Boolean Visible;
public:
    Point (int InitX, int InitY);    //constructor, cosa que VB no tiene
    void Show();
    void Hide();
    void MoveTo(int NewX, int NewY);
    Boolean IsVisible();
};

Después hay que implementar el funcionamiento de las funciones, código que no incluyo para no extenderme.

Lo que cada una de estas clases hace, no viene al caso, ya que son cosas propias del lenguaje, incluso del "proveedor" de ese lenguaje. Por si no lo sabias, una función declarada como void en C/C++ es igual que un Sub en VB.
Los constructores de las clases de C++, son las funciones que se encargan de inicializar los datos de esa clase, por norma tienen el mismo nombre que la clase... esta característica no está disponible en VB, por tanto, tendremos que crear nuestras propias funciones de inicialización.
En el caso de Location, lo que se hace es asignar los valores iniciales de X e Y, los cuales se pasan como parámetros. En la clase Point se asignan, además de estos dos valores pasados como parámetros, el valor de la variable Visible, que inicialmente será False.
En la clase Circle, además de X e Y, se pasa como parámetro el valor del Radio.

Para finalizar con los ejemplos de C++, vamos a ver como mostrar un punto y un círculo; si no estás muy "ducho" en el C/C++, te diré que normalmente la declaración de una variable y su inicialización se hacen en un sólo paso; por ejemplo, para declarar una variable del tipo int y asignarle el valor 100 se haría así:
int X = 100;
En Visual Basic tenemos que hacerlo en dos pasos: primero se declara la variable y posteriormente se le asigna el valor:
Dim X As Integer
X = 100

Ahora los ejemplos de cómo usar estas clases en C++:

//el punto
Point APoint(100, 50);     //se declara e inicia con los valores para X e Y
APoint.Show();        //se muestra
...
APoint.MoveTo(300, 150); //se mueve el punto a otra posición
...
APoint.Hide();        //se oculta el punto

//el círculo
Circle MyCircle(100, 200, 50);     //se declara una variable del tipo Circle
                //y se inicia la posición y tamaño
MyCircle.Show();
...
MyCircle.MoveTo(200, 250);
...
MyCircle.Expand(50);
...
MyCircle.Contract(75);
...
MyCircle.Hide();

Ningún misterio ¿verdad? Esto mismo se podría hacer en Visual Basic de esta forma:

'el punto
Dim APoint As New cPoint
APoint.Iniciar 100, 50
APoint.Show
...
APoint.MoveTo 300, 150
...
APoint.Hide

'el círculo
Dim MyCircle As New cCircle
MyCircle.Iniciar 100, 200, 50
MyCircle.Show
...
MyCircle.MoveTo 200, 250
...
MyCircle.Expand 50
...
MyCircle.Contract 75
...
MyCircle.Hide

Siempre y cuando la clase cCircle dispusiera de sus propios métodos Show, MoveTo y Hide. Pero si cCircle "hereda" estos métodos de la clase cPoint y queremos que sea casi una herencia real, el Visual Basic no nos permitiría usarla así...
¿Más complicado?
Si.
Entonces... mejor sin herencia...
Puede ser...
Pero no creo que hayas llegado hasta aquí para abandonar tan fácilmente... aunque todavía estás a tiempo...

Vamos a ver si logro justificar todo el código que seguirá en breve...
Para ello vamos a tratar un poco el tema del polimorfismo, otra de las características de la programación orientada a objetos y que es la que el Visual Basic usa para simular la herencia, aunque no sea eso la herencia, pero...

Como te he comentado antes, tanto la clase Punto como la clase Círculo tienen métodos con el mismo nombre, pero que internamente usan código diferente, adecuado a la acción que tenga que realizar cada clase.
En el otro ejemplo del Buffer para ficheros, impresoras y modems, podíamos tener un método que enviara la información contenida en ese buffer al medio correspondiente, no sería lo mismo "imprimir" en un fichero que en una impresora, pero los tres tipos de buffers podían tener un método Print y actuarían de forma diferente según el medio al que va dirigida la impresión.
Podíamos tener un objeto que lo mismo pueda mostrar o enviar una información a un fichero que a una impresora que a un modem... pero que debe ser lo suficientemente "inteligente", (por decirlo de algún modo), de saber cómo actuar en cada caso.
Los métodos comunes podían estar implementados (o definidos) en una clase básica: Buffer y los específicos de cada medio estarían implementados en las clases que se han derivado de la clase básica.
¿Te enteras por dónde va el tema?

El problema es que conseguir esto con el Visual Basic, no se hace de una forma tan "natural" como lo es en C++ (y posiblemente en Delphi, aunque lo desconozco). En VB tendríamos que declarar una variable del tipo genérico Buffer y asignarle a esta variable-objeto el tipo especifico que queremos usar para poder acceder a los métodos heredados de la clase Buffer.
Si tenemos que cBuffer es la clase básica y cFile el tipo específico que hereda los métodos comunes de cBuffer, haríamos algo así:

'Dimensionamos las variables del tipo adecuado
Dim objBuffer As cBuffer
Dim objFile As cFile

'Creamos un nuevo objeto cFile:
Set objFile = New cFile

'Para usar los métodos comunes de cBuffer implementados en cFile:
Set objBuffer = objFile
...
objBuffer.Print "una cadena enviada a un fichero"
objBuffer.Close

Sin embargo, cuando queramos usar un método propio del objeto cFile, por ejemplo borrar el fichero, cosa que no se podría hacer con una impresora y por tanto sólo estaría implementada esta funcionalidad en cFile, se haría así:

objFile.Delete

Ya que ese método existe en la clase cFile y no en cBuffer.

 

El código en VB... por favor.

Ahora, que espero que tengas más claro todo esto de usar métodos propios de una clase y otros genéricos o heredados de otra clase, veamos el código para "implementar" las clases Location, Point y Circle en Visual Basic.

La clase base es cLocation, que tiene tres propiedades: X e Y para saber la posición del punto y Papel que será el objeto en el que se mostrará el gráfico dibujado; ese objeto puede ser un formulario o cualquier objeto que tenga los métodos gráficos usados: Line, Circle, etc.; por ejemplo un control Picture e incluso un objeto Printer... pero no un control Image.
Además de estas tres propiedades, la clase tendrá un método: Iniciar, que permitirá inicializar los valores iniciales (valga tanta redundancia) de la posición X e Y, así como el objeto en el que se mostrarán los gráficos.
Veremos que este método será implementado de forma diferente en la clase cCircle, ya que necesita un parámetro adicional. Pero eso lo veremos en su momento, ahora vamos a ver el código de cLocation:

'
Option Explicit

'Propiedades públicas, si no se van a hacer comprobaciones
'es más rápido usarlo así
Public X As Integer
Public Y As Integer

'Contendrá el objeto en el que se dibujarán los gráficos
Private m_Papel As Object

Public Sub Iniciar(elPapel As Object, ByVal InitX As Integer, ByVal InitY As Integer)
    'Inicializar los valores
    X = InitX
    Y = InitY
    Set Papel = elPapel
End Sub

Public Property Set Papel(NewPapel As Object)
    'Usamos Set porque al tratar con asignaciones a objetos,
    'es necesario usar Set Papel = ElObjeto
    Set m_Papel = NewPapel
End Property

Public Property Get Papel() As Object
    Set Papel = m_Papel
End Property

Como ves, es bastante simple, de hecho es la más simple de las clases que veremos.
Lo único que hay que aclarar es que las "variables" públicas de una clase, (en este caso X e Y), se convierten en propiedades de esa clase, como veremos a continuación, al implementar esas propiedades, se convierten en procedimientos Property Get y Let.
En el caso de "Papel" lo he codificado usando Property ya que en este caso no interesa usar Property Let, sino Property Set... además de que, como veremos en un momento, necesitaremos hacer ciertas comprobaciones... cosa que también se podía hacer para los valores de X e Y.

La clase cPoint es un poco más complicada, ya que en esta clase queremos "heredar" la clase cLocation. En cPoint vamos a declarar un objeto del tipo cLocation que será el que contenga los valores de las propiedades X, Y y Papel... esto lo hago así para no tener que declarar variables privadas para almacenar dichos valores, además de que si en la clase cLocation se hicieran algunas comprobaciones sobre los valores a asignar a esas propiedades, no habría que repetir el código usado para hacer dichas comprobaciones. Por ejemplo, la propiedad Papel podría admitir sólo objetos de cierto tipo, para que generara un error cuando no sea así...
El código para hacer esto sería:

Public Property Set Papel(NewPapel As Object)
    'Usamos Set porque al tratar con asignaciones a objetos,
    'es necesario usar Set Papel = ElObjeto
    Set m_Papel = NewPapel

    'Podríamos hacer ciertas comprobaciones de que sólo se admitan
    'objetos que tengan ciertas características, por ejemplo:
    'que tengan la propiedad DrawWidth
    On Local Error Resume Next
    m_Papel.DrawWidth = 2
    If Err Then
        Err = 0
        On Local Error GoTo 0
        With Err
            .Description = "Este objeto no soporta DrawWidth"
            .Source = "Clase cLocation"
            .Raise 438 'El objeto no soporta esta propiedad...
        End With
    Else
        'Es importante que se detenga la detección de errores, sino
        'no se generaría el error, al tener una rutina de detección...
        On Local Error GoTo 0
        
        'El objeto debe ser del tipo Form, Picture o Printer
        
        'Si no es del tipo Form...
        If Not (TypeOf m_Papel Is Form) Then
            'ni del tipo PictureBox...
            If Not (TypeOf m_Papel Is PictureBox) Then
                'ni es una impresora,
                If Not (TypeOf m_Papel Is Printer) Then
                    'producir un error indicándolo
                    With Err
                        .Description = "Type Mismatch: El objeto debe ser del tipo Form o PictureBox"
                        .Source = "Clase cLocation"
                        .Raise 13 'type mismatch
                    End With
                End If
            End If
        End If
    End If
End Property

Veamos el código de la clase cPoint para que comprendas mejor cómo se "justifica" el uso de un objeto del tipo cLocation...

Option Explicit

'"Hereda" los métodos y propiedades de cLocation
Implements cLocation

'Esta clase se usa para obtener los valores de X, Y y Papel
Private m_cLocation As cLocation

'Variable para almacenar el valor de si está Visible o no
Private m_Visible As Boolean


Private Sub Class_Initialize()
    Set m_cLocation = New cLocation
End Sub

Private Sub Class_Terminate()
    Set m_cLocation = Nothing
End Sub

Vamos a ver ahora los métodos "heredados" de la clase cLocation:
Cuando se usa Implements, siempre se usa de la forma que veremos, es decir: se indica el nombre de la clase seguido de un guión bajo y el nombre del método, igual que cuando se accede a los eventos de un formulario o un control...

Un detalle: Cuando se usa Implements, hay que codificar todos los procedimientos implementados, aunque sólo sea con un comentario, la cuestión es que estén "usados", (al desplegar la lista de la derecha los verás en negrita), sino se hace, el VB nos lo recordará.

Private Property Set cLocation_Papel(NewPapel As Object)
    ' Aquí simplemente se asigna, ya que el código de cLocation
    ' se encarga de hacer las comprobaciones
    Set m_cLocation.Papel = NewPapel
End Property

Private Property Get cLocation_Papel() As Object
    Set cLocation_Papel = m_cLocation.Papel
End Property

Private Sub cLocation_Iniciar(elPapel As Object, ByVal InitX As Integer, ByVal InitY As Integer)
    'Inicia los valores de X e Y,
    'así como el objeto en el que se mostrarán los gráficos
    m_cLocation.Iniciar elPapel, InitX, InitY
    'With m_cLocation
    '    .X = InitX
    '    .Y = InitY
    '    Set .Papel = elPapel
    'End With
    m_Visible = False
End Sub

Private Property Get cLocation_Y() As Integer
    cLocation_Y = m_cLocation.Y
End Property

Private Property Let cLocation_Y(ByVal NewY As Integer)
    'Cambiamos también el valor de la clase cLocation
    m_cLocation.Y = NewY
End Property

Private Property Let cLocation_X(ByVal NewX As Integer)
    'Cambiamos también el valor de la clase cLocation
    m_cLocation.X = NewX
End Property

Private Property Get cLocation_X() As Integer
    'El valor de X se obtiene de la clase cLocation
    cLocation_X = m_cLocation.X
End Property

Fíjate en lo que te comenté antes: X e Y en la clase cLocation están declaradas como variables públicas, pero en la "implementación" se convierten en procedimientos Property

Public Property Get Visible() As Boolean
    Visible = m_Visible
End Property

Public Sub Show()
    m_Visible = True
    'Dibujar el punto con el color de primer plano
    With m_cLocation
        .Papel.DrawWidth = 2
        .Papel.Line (.X, .Y)-(.X, .Y), .Papel.ForeColor
    End With
End Sub

Public Sub Hide()
    m_Visible = False
    'Si no es una impresora, permitir ocultar el punto
    If Not (TypeOf m_cLocation.Papel Is Printer) Then
        'Dibujar el punto con el color de fondo
        With m_cLocation
            .Papel.DrawWidth = 2
            .Papel.Line (.X, .Y)-(.X, .Y), .Papel.BackColor
        End With
    End If
End Sub

Public Sub MoveTo(ByVal NewX As Integer, ByVal NewY As Integer)
    Hide
    'Asignar la nueva posición
    m_cLocation.X = NewX
    m_cLocation.Y = NewY
    Show
End Sub

Para terminar, veamos los métodos de inicialización de la clase:

Private Sub Class_Initialize()
    ' Creamos un nuevo objeto del tipo cLocation
    Set m_cLocation = New cLocation
End Sub

Private Sub Class_Terminate()
    ' Ya no neceitamos más ese objeto, ¡eliminarlo!
    Set m_cLocation = Nothing
End Sub

 

Para usar esta clase, tendremos que usar una variable del tipo cLocation para poder llamar al método Iniciar, ¿por qué? porque Iniciar es un método de cLocation y no podemos acceder a él directamente desde una variable del tipo cPoint ya que ese método es privado...

Veamos el código:

' Declaramos las variables del tipo adecuado
Dim unPunto As cPoint
Dim tLocation As cLocation

' Creamos un nuevo objeto del tipo cPoint
Set unPunto = New cPoint

' cPoint no tiene un método Iniciar, pero cLocation si lo tiene
' y cPoint lo implementa
Set tLocation = unPunto
' Ahora tLocation apuntará a las propiedades y métodos implementados en cPoint
tLocation.Iniciar Me, 3000, 2000
' Mostramo el punto
unPunto.Show
' Lo mismo ocurre con los valores de X e Y, para manipularlos hay
' que usar el "interface" de cLocation
Label1(1) = "Punto, X= " & tLocation.X & ", Y= " & tLocation.Y

Parece extraño ¿verdad?
Pues aquí está el "quid" de la cuestión.
La variable unPunto es del tipo cPoint.
La clase cPoint implementa las propiedades y métodos de cLocation mediante el interface expuesto por esa clase...

Osea que al hacer esto: Set tLocation = unPunto, lo que se hace es asignar un "puntero" que hace referencia al interface (o plantilla) del tipo cLocation que está en cPoint, por tanto se pueden acceder a las propiedades y métodos, como en el caso de tLocation.X para averiguar el valor almacenado en X.

Para acceder a los "propios" métodos de cPoint, simplemente se usan directamente en la variable unPunto:
unPunto.Show

Sigue siendo difícil de digerir ¿verdad?
Pues vamos a complicarlo un poco más.
La clase cCircle se deriva de cPoint, si se me permite usar esa expresión, ya que derivar, lo que se dice derivar, no se deriva... recuerda que en VB no hay herencia real... Bueno, cCircle implementa la clase cPoint... si realmente existiera herencia en Visual Basic, la clase cLocation también se heredaría, pero como este no es el caso, tendremos que implementar también la clase cLocation, siempre y cuando necesitemos acceder a las propiedades y métodos "implementados" por esa clase; osea, que no es obligatorio hacerlo, aunque en nuestro caso "necesitaremos" hacerlo...

No vamos a ver todo el código, este puedes verlo si descargas el zip con los listados. Vamos a ver sólo parte de él, el resto te lo puedes imaginar.
Lo importante es saber que los diferentes "interfaces" implementados en una clase hacen posible acceder a los métodos y propiedades de la clase heredada, pero el código usado en esos procedimientos será el que nosotros queramos que tenga... o el que deba tener, para hacer lo que deba hacer...
La "plantilla" implementada por el interface de una clase sólo "impone" los nombres de los procedimientos y sus parámetros, los cuales estamos obligados a respetar, incluso estamos "obligados" a respetar y codificar, aunque, como te comenté antes, aunque sólo sea con un simple comentario.
Vamos a ver un poco de código de cCircle y ahora sigo con el rollo...

' Hereda los métodos y propiedades de cLocation
Implements cLocation

' Hereda los métodos y propiedades de cPoint
Implements cPoint

' La clase Location se usa para manejar los valores de X, Y y Papel
Private m_cLocation As cLocation

' Variable para almacenar el valor del radio
Private m_Radius As Integer
' Para saber si el círculo está visible o no
Private m_Visible As Boolean

Fíjate que he usado una variable para saber si está visible o no, antes había usado el valor almacenado en la propiedad Visible de cPoint, pero la verdad es que es un "despilfarro" de recursos y memoria para hacer algo que no necesita nada más que asignar un valor en una variable, sin más comprobaciones...
Veamos ahora el código usado en el método Iniciar implementado por la clase cLocation, el resto es igual que lo que te mostré para cPoint.

Private Sub cLocation_Iniciar(elPapel As Object, ByVal InitX As Integer, ByVal InitY As Integer)
    Me.Iniciar elPapel, InitX, InitY, 500
End Sub

En este caso llamamos al método "propio" de cCircle para inicializar, pero con un valor por defecto para el radio, esto lo hago así para el caso de que se quiera inicializar mediante el interface de cLocation. Si no se le asignara ese valor (o el que tu estimes conveniente), no se vería el círculo, ya que sería cero.
Como ves, la clase cCircle tiene su propio método de iniciarse, ya que es necesario informarle del radio que queremos que tenga, lo mismo ocurriría si se hiciera una clase para dibujar líneas, necesitaríamos indicarle hasta dónde se debe dibujar dicha línea.
Veamos el código del método Iniciar de cCircle:

'
Public Sub Iniciar(elPapel As Object, ByVal InitX As Integer, ByVal InitY As Integer, ByVal InitRadius As Integer)
    ' Cada clase debe tener su propio método de inicio,
    ' aunque se pueda usar el que está implementado
    m_cLocation.Iniciar elPapel, InitX, InitY
    ' y asignar las variables propias de esta clase
    m_Radius = InitRadius
End Sub

Ahora veamos cómo están codificados los métodos Show, Visible y MoveTo de la interface implementada de cPoint:

'
Private Sub cPoint_MoveTo(ByVal NewX As Integer, ByVal NewY As Integer)
    cPoint_Hide
    m_cLocation.X = NewX
    m_cLocation.Y = NewY
    cPoint_Show
End Sub

Private Sub cPoint_Show()
    m_Visible = True
    With m_cLocation
        .Papel.DrawWidth = 2
        ' Dibujar el círculo con el color de primer plano,
        ' aunque no es necesario especificar el color,
        ' ya que se usa por defecto el valor de ForeColor
        .Papel.Circle (.X, .Y), m_Radius ', .Papel.ForeColor
        
        ' Si se imprime en la impresora y se quiere que cada figura
        ' se muestre en una hoja diferente, añadir esto a cada uno de los
        ' métodos Show
        'If TypeOf .Papel Is Printer Then
        '    .Papel.EndDoc
        'End If
    End With
End Sub

Private Property Get cPoint_Visible() As Boolean
    cPoint_Visible = m_Visible
End Property

El método Show, lo que hace es dibujar un círculo, ya que ese es el "motivo" de esta clase, sino no se llamaría cCircle.
Si te fijas en el método MoveTo, verás que se llama a los métodos implementados de cPoint, para ello hay que usar el nombre completo del procedimiento, este código no debería parecerte extraño, ya que se hace lo mismo que si quisieras llamar al evento Click del Command1, es decir, usar el nombre completo: cPoint_Hide.

Los métodos propios de esta clase se codifican como es costumbre: mediante procedimientos públicos:

'
Public Sub Expand(ByVal ExpandBy As Integer)
    cPoint_Hide
    m_Radius = m_Radius + ExpandBy
    If m_Radius < 0 Then m_Radius = 0
    cPoint_Show
End Sub

Public Sub Contract(ByVal ContractBy As Integer)
    Expand -ContractBy
End Sub

Veamos ahora cómo usar esta clase:

'
Dim unCirculo As cCircle

Set unCirculo = New cCircle

    Dim tPoint As cPoint
    Dim tLocation As cLocation

    ' El método Iniciar está implementado de forma separada en cCircle
    ' porque se necesita un parámetro más para el radio del círculo
    unCirculo.Iniciar elPapel, 1300, 1300, 1200
    ' cCircle no tiene un método Show, pero si lo tiene cPoint
    ' y en cCircle está implementado para dibujar círculos
    Set tPoint = unCirculo
    tPoint.Show
    ' Para averiguar las coordenadas hay que usar el interface
    ' que expone esos valores de forma pública: cLocation,
    ' pero se asigna desde unCirculo ya que esa clase implementa
    ' el interface de cLocation
    Set tLocation = unCirculo
    Label1(0) = "X= " & tLocation.X & ", Y= " & tLocation.Y & _
                ", Radio= " & unCirculo.Radio


' Mover el círculo de sitio

    ' cCircle no tiene un método MoveTo, pero si lo tiene cPoint
    ' y en cCircle está implementado para mover círculos
    Set tPoint = unCirculo
    tPoint.MoveTo 2500, 1700


' Expandir / Contraer el círculo

    ' Se usan los método propios de cCircle
    unCirculo.Expand 400

    unCirculo.Contract 700

El final de todo esto.

Quisiera aclarar que no es lo mismo usar Implements que declarar una variable pública de una clase determinada. Si en lugar de haber usado:
Implements cLocation en la clase cPoint (o en cualquier otra), hubiésemos hecho esto otro:
Public Location As cLocation
Podríamos acceder a los métodos y propiedades de cLocation de esta forma:
unPunto.Location.Inicio Me, 1000, 2000
unPunto.Show

La verdad es que funcionaría igual, pero ya no tendríamos una "plantilla" de cLocation dentro de cPoint.

Con cCircle, la cosa se complicaría un poco más, podríamos hacer esto:
Public Punto As cPoint
y quitar los dos Implements e incluso la declaración de m_Location, pero para acceder a las propiedades y métodos de cLocation desde cCircle tendríamos que usar Point como intermediario:
Punto.Location.X = NewX
Y desde fuera de la clase:
unCirculo.Punto.Location.X

Es decir: acceder a las propiedades de cLocation mediante la variable pública declarada en cPoint, igual que hemos visto en unPunto; pero la cosa no funcionaría si quisiéramos usar el método Show de cPunto:
unCirculo.Punto.Show
ya que mostraría un punto y no un círculo, salvo que creáramos un método Show propio de cCircle, pero nuevamente perderíamos las ventajas de poder usar los interfaces "heredados" de las otras clases.

Para entender todo esto de las ventajas de usar los métodos implementados, veamos un procedimiento que mostrará la posición X e Y de una figura que "Implemente" la clase cLocation.

'
Private Function InfoFigura(unaFigura As cLocation) As String
    InfoFigura = "X= " & unaFigura.X & ", Y= " & unaFigura.Y
End Function

La función InfoFigura espera una variable del tipo cLocation y parecería lógico usarla sólo con variables del tipo cLocation, como podría ser tLocation que vimos anteriormente, pero como el resto de los objetos "heredan" esa clase, no es necesario usar una clase intermedia.

Para usarla, simplemente se pasa la figura de la que queremos obtener la posición y el resultado lo asignamos a un Label o a cualquier sitio en el que queramos mostrar la "cadena" devuelta por esa función:
Label1(0) = InfoFigura(unCirculo)
Label1(1) = InfoFigura(unPunto)
Label1(2) = InfoFigura(unaLinea)

Se puede enviar a esa función cualquiera de los objetos que queramos, siempre que esos objetos "Implementen" la clase cLocation que es la que tiene las propiedades X e Y, dando como resultado la posición de cada una de esas figuras.
Lo que debe "quedarte" claro, si es que es posible que se pueda quedar algo claro... es que sólo se usa de cada una de las figuras pasadas como parámetro la parte que le corresponde de cLocation, es decir es como si se hiciese una criba y sólo se permitiera el paso al cLocation que tiene el objeto indicado en el parámetro.

Si quieres curiosear un poco, añade este código a la función InfoFigura y verás los resultados que da:

Debug.Print TypeName(unaFigura),

If TypeOf unaFigura Is cLocation Then _
    Debug.Print "cLocation",

If TypeOf unaFigura Is cPoint Then _
    Debug.Print "cPoint",

If TypeOf unaFigura Is cLine Then _
    Debug.Print "cLine",

If TypeOf unaFigura Is cCircle Then _
    Debug.Print "cCircle",

El TypeName te mostrará el nombre de la clase pasada a la función, es decir "cPunto" si el objeto es del tipo cPunto, etc.
Pero en el caso de TypeOf nos dirá que unPunto es del tipo cLocation y también del tipo cPoint; unCirclo será además de esos dos, como es evidente, también del tipo cCircle.
Es decir que cada Interface implementada se tiene en cuenta.
Confío que ahora tenga más sentido usar todo esto de los interfaces implementados.

Como recordatorio final:
Cuando se implementa una clase, sólo se heredan los nombres de los métodos y propiedades, así como los parámetros, pero no el código que tuviesen esos procedimientos.

Aunque si en lugar de usar los métodos y propiedades heredados mediante variables intermedias, quisieras usarlos de forma directa, simplemente tendrías que declarar en cada clase los métodos y propiedades heredados como públicos de esa clase y desde ellos llamar a los que se han "implementado" internamente, por ejemplo:

Public Property Get X() As Integer
    ' Llamar al procedimiento implementado
    X = cLocation_X
End Property

Y lo mismo con el resto de las propiedades y métodos:

Public Sub MoveTo(ByVal NewX As Integer, ByVal NewY As Integer)
    ' Llamar al procedimiento implementado
    cPoint_MoveTo NewX, NewY
End Sub

El único inconveniente es que habría que escribir más código, pero además de poder usar los métodos y propiedades de forma directa:
unCirclo.MoveTo
no perderíamos las ventajas de usar los interfaces.

Seguramente pensarás que esto es una "chapuza"... y tienes razón... pero es la única forma de poder hacerlo con lo que ahora mismo nos ofrece el Visual Basic en este tema de la herencia... al menos por lo que yo sé... que igualmente hay por ahí suelto algún "gurú" que sabe cómo hacerlo mejor y de otra forma más fácil... 8-)

El coñazo de todo esto es tener que codificar todos y cada uno de los métodos y propiedades de cada una de las clases "heredadas" en una clase, aunque esta parte se puede "automatizar", es decir usar un programilla que se encargue de hacerlo... aunque eso no evita que haya que hacerlo...
Ese "heredador" automático lo puedes conseguir en uno de los zips que te pongo más abajo.

 

Despedida y cierre.

Como verás... prácticamente lo tratado no tiene mucho que ver con el título del "artículo", ya que la herencia no se puede simular en Visual Basic, salvo con las "distancias" mostradas aquí. La razón principal y causante de que el VB no disponga de herencia es por el hecho de que está basado en COM (Component Object Model) y COM trabaja sólo con Interfaces, no con herencia.
La herencia pura se basa en heredar "código" y COM sólo da "compatibilidad" binaria... pero dejémoslo estar aquí, ya que esto del COM es otro tema y lo único que yo podría hacer con este tema sería "copiar y pegar" lo que otros más entendidos que yo ya han dicho... así que...

De todas formas, nos conformaremos con esto... que es al fin y al cabo lo que el "pobre" Visual Basic nos pone a nuestra disposición, que creo que es más que suficiente.

En el código que hay en el ZIP he puesto también una pequeña aplicación que "hereda" e implementa automáticamente en una clase la clase que le indiquemos. Desde mi punto de vista se merece un comentario separado, pero eso será en otra ocasión, sino no terminaría nunca de publicar este articulillo.

Nos vemos.
Guillermo

Este link te bajará el código de ejemplo de las clases aquí tratadas. (herencia.zip 8.52 KB)
Este otro te permitirá bajar el "heredador de clases. (genHerencia.zip 17.8 KB)


Nota del 29/Ago/2003:

Esta nota es para aclarar, que no para justificar, (aunque puede que también se justifique), un comentario de este artículo, por el que algunos programadores de Delphi se han sentido "ofendidos", o casi...

Si antes de seguir leyendo quieres ver "parte" del revuelo que se ha formado, sigue este link:
http://www.clubdelphi.com/foros/showthread.php?s=&threadid=3101

El comentario que levantó la polémica fue este:

La verdad es que herencia, lo que se dice herencia, el Visual Basic no tiene, ni tampoco el Delphi, ya que estamos, (aunque no me gusta meter cizaña, todo hay que decirlo... je); al menos si lo miramos desde el punto de vista de los programadores de C++ o de los puristas de la OOP.

El pasado 21 de agosto, recibo un mensaje procedente de Federico Firenze, (no se presentó como tal, pero después descubro que es uno de los moderadores de ese foro de Delphi), en el mensaje me pregunta los fundamentos en los que me baso para decir en este artículo que Delphi carece de herencia.
Mi respuesta fue esta:


Los "fundamentos" del porqué dije eso hace 5 años... ni me acuerdo...
Pero en el párrafo "aclaro" que al menos desde el punto de vista de los
"puristas" de la OOP.

...<pego el párrafo mostrado arriba, pero que no lo muestro aquí para no volver a repetirlo>...

Con tanto tiempo, ya no recuerdo en qué me basé para decirlo, pero seguro que
en algún sitio "alguien" dijo algo...
 

Creo, (al menos así lo entiendo yo), que en ese párrafo fatídico queda bien claro, (aparte de que lo dije en plan "broma"), que si la herencia la tomamos desde el punto de vista de los programadores de C++ o los puristas de la OOP, el Delphi no tiene herencia. Pero, (aunque yo no programe en Delphi), yo se que Delphi soporta la herencia, y los que utilizan Delphi deberían saberlo y eso es lo que realmente debería importar.
Pero no creo que fuese un comentario como para "levantar polémica" y en ningún momento dije que el Delphi no soporta la herencia, aunque, según he leído en el mencionado "hilo" del foro, la herencia que soporta Delphi es herencia simple, (igual que los lenguajes basados en .NET Framework, Java y algún otro), por lo que puede ser que ese comentario lo hiciera por esa razón, pero aún así, aunque después de casi 5 años no recuerde de dónde "me llegó ese mensaje", no es motivo para armar ningún tipo de "historias". Historias que después llegan a las descalificaciones sobre mi persona y sobre Visual Basic. Y no es por defender al Visual Basic, ya que nunca me ha gustado entrar en ese tipo de "lucha", cada uno sabe, (o debería saber), con lo que trabaja (o programa), debería conocer sus limitaciones. Cierto es que si no conocemos "al enemigo" no deberíamos criticarlo, (si alguno piensa que ese ha sido mi "pecado", pido disculpas), cosa que hace uno de los participantes de ese mismo hilo, (el cual en otro mensaje dice que antes de criticar a un lenguaje, (con respecto a otro), se deben conocer los dos), cuando comenta (más bien pregunta) cómo puede el Visual Basic soportar Polimorfismo sin soportar herencia, y pone como ejemplo un código que usa enlace tardío (late binding). Este señor, (Federico), no sabe cómo se puede conseguir el Polimorfismo mediante la implementación de interfaces, (al menos en VB); y esto no es algo particular de Visual Basic, por supuesto no voy a decir si Delphi soporta o no soporta este tipo de "herencia de interfaces", ya que lo desconozco, pero me imagino que "debería" soportarla, lo que si es cierto es que Visual Basic, al estar basado en COM, es el único tipo de herencia que soporta.

Pues esto es todo, así al menos, los que leáis este artículos "atraídos" por esos comentarios, que sepáis de qué va todo esto...

Nos vemos.
Guillermo
P.S.
El artículo está como estaba desde que lo publiqué, lo único que he cambiado, aparte de esta nota y algunas "tildes" (o palabras acentuadas), es el formato (y el código que ahora está coloreado), pero no he cambiado nada del artículo que originalmente publiqué; que por otra parte, creo que para estar escrito hace casi 5 años, (a pesar de que algunos no lo hayan leído completo), cuando la herencia era algo que nos había estado vetado a los programadores de Visual Basic, no me "quedó" mal del todo, je, je.


ir al índice