.NET

Programación Orientada a Objetos en .NET (2)

Conceptos prácticos

 
Publicado el 13/Dic/2006
Actualizado el 13/Dic/2006
Autor: Guillermo 'guille' Som

Importante:
Este artículo está registrado por Iberprensa (Studio Press) y está prohibida la reproducción total o parcial incluso indicando la procedencia.


En esta ocasión veremos con ejemplos prácticos cómo utilizar las características de la POO usando lenguajes de .NET, de forma que tengamos claro cómo usar la herencia, el polimorfismo y la encapsulación, pero con código.
Versión originalmente publicada en Todo Programación número 7 (Oct 2004)


Introducción:

En el artículo anterior vimos algunos conceptos teóricos de la POO (Programación Orientada a Objetos) desde el punto de vista de los lenguajes de .NET Framework, en esta ocasión veremos con ejemplos prácticos cómo utilizar las características de la POO en Visual Basic .NET, (en el ZIP se incluye también el código para C#), de forma que tengamos claro cómo usar la herencia, el polimorfismo y la encapsulación, pero con código.

 

VISUAL BASIC .NET Y LA POO

Tal como comentamos en el artículo anterior, uno de los pilares de la POO es la herencia. Mediante la herencia podemos definir clases totalmente operativas y crear nuevas clases basadas en ellas de forma que hereden toda la funcionalidad de la clase base y nos permita ampliarla. Por tanto, vamos a empezar viendo cómo definir una clase y cómo aplicar en Visual Basic .NET el resto de conceptos relacionados con la programación orientada a objetos.

 

Nota:
Este artículo se publicó en Octubre de 2004, y por tanto todo lo aquí explicado está relacionado con la versión 1.1 de .NET Framework.
La versión de .NET Framework que hay actualmente (a la hora de escribir esta nota) es la 3.0, (que en el fondo es la misma que la 2.0), y se han introducido ciertos cambios o mejoras, pero básicamente lo aquí explicado sigue siendo tan válido ahora como hace dos años.
 

 

DEFINIR CLASES EN VISUAL BASIC .NET

Antes de poder usar las características de la POO tendremos primero que aprender a declarar una clase.
La declaración de una clase se define usando la instrucción Class seguida del nombre de la clase y termina usando las instrucciones End Class.
Dentro de ese bloque definiremos los campos, propiedades y métodos que queramos que tenga la clase.

NOTA:
En Visual Basic .NET la definición de una clase se puede hacer en cualquier fichero de código (con extensión .vb), aunque no es obligatorio hacerlo en un fichero independiente como ocurría con las versiones anteriores, es recomendable hacerlo, para que nos resulte más fácil de mantener.
 

 

Public Class A
   Private _prop2 As Integer
   Private _prop1 As String
   '
   Public Property Prop1() As String
      Get
         Return _prop1
      End Get
      Set(ByVal value As String)
         If value <> "" Then
            _prop1 = value
         End If
      End Set
   End Property
   Public Property Prop2() As Integer
      Get
         Return _prop2
      End Get
      Set(ByVal value As Integer)
         _prop2 = value
      End Set
   End Property
   '
   Public Sub Mostrar()
      Console.WriteLine("{0}, {1}", _prop1, _prop2)
   End Sub
End Class

Listado 1

 

Tal como podemos ver en el listado 1, tenemos una clase llamada A que define dos campos, dos propiedades y un método. Los dos campos, declarados como privados, se usan para mantener "internamente" la información que se expone mediante las dos propiedades públicas, de esta forma protegemos los datos y esta sería una forma de encapsular la información.

De las dos propiedades definidas para acceder a esos datos, solo la propiedad Prop1 hace una comprobación de que no se asigne una cadena vacía al campo que mantiene internamente la información, aunque en este ejemplo por su simplicidad no hacemos más comprobaciones, en una clase algo más compleja, se podrían realizar otras comprobaciones, por ejemplo si el valor a almacenar es una cuenta de email, podríamos comprobar que es una cadena correctamente formada.

Las propiedades suelen definir dos bloques de código, uno, el bloque Get se utiliza cuando queremos acceder al valor devuelto por la propiedad, el otro es el bloque Set, el cual se utilizará cuando asignemos un valor a la propiedad.

El método Mostrar se usará para mostrar el contenido de las dos propiedades por la consola y está definido como Sub (void en C#) porque no devuelve ningún valor.

 

SOBRESCRIBIR MIEMBROS HEREDADOS

Tal como comentamos en el artículo anterior, todas las clases de .NET se derivan directa o indirectamente de la clase Object. La clase definida en el listado 1 también se deriva de Object aunque no se indique expresamente.
La clase base de todas las clases de .NET tiene un método que se utiliza para recuperar información, (en formato cadena), del contenido de la clase: el método ToString.
Cada clase puede crear su propia versión del método ToString para que devuelva una representación adecuada, por ejemplo los tipos numéricos devuelven una cadena que representa al número que contiene.

En nuestra clase podemos redefinirlo para que nos devuelva el contenido de los dos datos que la clase mantiene:

Public Overrides Function ToString() As String
   Return String.Format("{0}, {1}", _prop1, _prop2)
End Function

Para indicarle al compilador que estamos redefiniendo un método ya existente, lo indicamos con la instrucción Overrides (override en C#). Debido a que ahora nuestra clase tiene una nueva versión de este método, cualquier clase que se derive de ella también heredará la nueva implementación del método ToString.

 

USAR LA HERENCIA EN VISUAL BASIC .NET

La clase que hemos definido en el listado 1 no indicaba de ninguna forma que se deriva de la clase Object, este es un caso excepcional, ya que todas las clases de .NET se derivan de forma "automática" de la clase Object.
Para indicar en Visual Basic .NET que una clase se deriva de otra, debemos usar la instrucción (o palabra clave) Inherits seguida de la clase que queremos usar como base.
Esa instrucción tiene que indicarse al principio de la declaración de la clase, antes que cualquier otra instrucción, con excepción de los comentarios.

Para crear una clase que se derive de la clase A definida en el listado 1, tendríamos que hacer lo siguiente:

Public Class B
   Inherits A
End Class

Tal como podemos comprobar, la clase B no define ningún método ni propiedad, pero realmente si que tiene métodos y propiedades: todos los que tenga la clase A (además de los de la clase Object).

Para comprobarlo podemos definir una variable del tipo de la clase B y comprobaremos que esta clase tiene los mismos miembros que la clase A, tal como se muestra en el listado 2.

 

Sub Main()
   Dim objB As New B

   objB.Prop1 = "guille"
   objB.Prop2 = 47

   objB.Mostrar()
   Console.WriteLine("{0}", objB.ToString)
End Sub

Listado 2

 

OCULTAR MIEMBROS HEREDADOS

Todo funciona como esperábamos, aunque hay un pequeño problema, si quisiéramos modificar el comportamiento de los miembros heredados por la clase B, no podríamos hacerlo. La razón es bien simple: la clase A no ha definido los miembros como virtuales (o reemplazables). Por tanto no se pueden crear nuevas versiones de los mismos en la clase B, o casi...
Realmente la clase B si que puede definir nuevas versiones de los miembros que tiene la clase A, al menos puede crear métodos y propiedades que tengan el mismo nombre, por ejemplo:

Public Sub Mostrar()
   Console.WriteLine("Mostrar en la clase B: {0}, {1}", Prop1, Prop2)
End Sub

Esto es totalmente correcto, al menos en el sentido de que no produce ningún error; lo más que producirá esa declaración es una advertencia del compilador indicándonos que ese método entra en conflicto con el definido en la clase base A, tal como podemos comprobar en la figura 1.

 

Figura 1. Advertencia de ocultación
Figura 1. Advertencia de ocultación

 

Esa advertencia nos informa que deberíamos indicar que la declaración "oculta" a la definida en la clase A y por tanto deberíamos usar la instrucción Shadows (new en C#). Aunque usemos Shadows, el problema real sigue existiendo: el método declarado en la clase B oculta al declarado (y heredado) en la clase A.
Si después de definir este método de la clase B volvemos a ejecutar el código del listado 2, comprobaremos que se utiliza el nuevo método.

Posiblemente el lector pensará que eso es lo que queríamos conseguir: tener nuestra propia versión del método Mostrar. Es más, si definimos una nueva clase que se derive de B podemos comprobar que realmente es ese método el que se hereda por la nueva clase.
En el listado 3 podemos ver la definición de la clase C y el código para comprobar lo que mostraría:

 

Public Class C
   Inherits B
End Class

Sub Main()
   Dim objC As New C

   objC.Prop1 = "guille"
   objC.Prop2 = 47

   objC.Mostrar()
   Console.WriteLine("{0}", objC.ToString)
End Sub

Listado 3

 

Y lo que imprime es exactamente lo mismo que usando la clase B. Por tanto, el objetivo está conseguido, es decir, la clase B ha "redefinido" un método de la clase A y esa nueva versión es la que se usará a partir de ese momento por las clases que se basen en la clase B.

Aparentemente así es. Al menos si lo tomamos al pie de la letra.
El único problema es que acabamos de romper una de las cualidades de la programación orientada a objetos: el polimorfismo.

Esta nueva definición del método Mostrar ya no tiene nada que ver con la definida por la clase A y por tanto no existe ninguna relación "polimórfica" entre ambos métodos.

Para ser más precisos, tanto la clase B como la clase C tienen dos definiciones del método Mostrar: el inicialmente heredado de la clase A y el nuevo definido por la clase B, aunque siempre prevalecerá el definido expresamente en la clase derivada frente al heredado de la clase base.

Si pudiésemos ver el objeto creado en la memoria a partir de la clase B (e incluso de la clase C), nos daríamos cuenta de que realmente está dividido en tres partes, tal como se muestra en la figura 2:

  1. La parte heredada de la clase Object

  2. La parte heredada de la clase A

  3. Las definiciones propias de la clase B

 

Figura 2. Objeto B en memoria
Figura 2. Objeto B en memoria

 

El método ToString definido en Object ha sido reemplazado o, para que lo comprendamos mejor, sustituido por el redefinido en la clase A, pero el método Mostrar de la clase A aún existe, lo que ocurre es que ha sido ocultado por el que se ha definido en la clase B.
Para demostrar que es así, que existen dos métodos Mostrar en la memoria, podemos utilizar el polimorfismo para "extraer" el método Mostrar que está oculto.
Para ello tendremos que declarar una variable del tipo de la clase A y decirle que extraiga del objeto B la parte que le corresponde: solo la parte definida en la clase A.
Esto se consigue de una forma muy simple: asignando a una variable del tipo A el contenido de la variable que apunta al objeto B:

Dim objA As A
objA = objB

A partir de este momento el objeto B está siendo referenciado por el objeto A, pero, y esto es importante, solo la parte que A conoce, es decir, la variable objA solamente podrá acceder al "trozo" del objeto B que se derivó de la clase A.
Por tanto, si llamamos al método Mostrar de la variable objA, accederemos al método Mostrar que el objeto B contiene porque la consiguió al derivarse de A.

Sí, esto es algo complicado y que no es fácil de comprender, pero es importante intentar asimilarlo, ya que es muy probable que lo necesitemos en nuestros proyectos, sobre todo si sabemos que se puede hacer.
Además de que esta sería la única forma que tenemos de acceder a ese método "oculto", ya que cualquier intento de acceder al método Mostrar mediante un objeto del tipo B, siempre accederá al definido en la propia clase B y que oculta al heredado de la clase A.

 

NOTA:
Cuando declaramos una variable y la instanciamos, (creando un nuevo objeto a partir de una clase), dicha variable simplemente tiene una referencia al objeto creado en la memoria, (la variable simplemente tiene un puntero al objeto real), por tanto, ese objeto existe y puede ser referenciado por otras variables, aunque esas otras variables deben ser de tipos "incluidos" en la clase usada para crear dicho objeto.
Al instanciar un nuevo objeto del tipo B, (tal como se muestra en la figura 2), podemos acceder a él mediante variables de tipo Object, de tipo A y, por supuesto, de tipo B.
 

 

INDICAR LOS MIEMBROS VIRTUALES

Como hemos comprobado, si queremos que los miembros de nuestra clase se puedan redefinir en clases derivadas, debemos indicarlo de forma expresa.

Para que esto sea así, utilizaremos la instrucción Overridable (virtual en C#). De esta forma le indicaremos al compilador que el método se puede redefinir, es decir, que en la clase derivada se puede crear una nueva versión "personalizada" de dicho miembro.
Con esto logramos que en sucesivas derivaciones de la clase solamente exista un mismo miembro polimórfico.
La ventaja principal es que si en otra clase decidimos crear una nueva versión de, por ejemplo, un método, cuando se cree un objeto en la memoria, solo existirá ese método, no varios métodos con el mismo nombre, pero sin ninguna relación entre ellos, exceptuando el hecho de que se llamen de la misma forma.

Tal como vimos en la sección SOBRESCRIBIR MIEMBROS HEREDADOS, tendremos que usar la instrucción Overrides (override en C#), para indicar que nuestra intención es crear una versión propia de uno de los miembros heredados.
Pero no solo basta con usar esa instrucción, ya que si queremos redefinir un miembro existente en alguna de las clases que hemos heredado, la "firma" de nuestra versión debe ser la misma que el original. Es decir, si es un método que devuelve una cadena y no recibe parámetros, debemos "respetar" esas mismas características, porque de lo que se trata es de cumplir con un contrato, dicho contrato estipula que tipo de miembro es, (un método o una propiedad), que tipo de datos devuelve, si recibe o no parámetros y en caso de que los reciba de que tipo deben ser.
Todo esto es para garantizar que esos miembros se puedan usar de forma independiente de la clase en el que se ha declarado, con idea de que podamos acceder a ellos mediante objetos de las clases que se han usado para crear la nueva, (las clases de las que se deriva la clase que sobrescribe el método).
Por ejemplo, si el método Mostrar definido en la clase A se hubiese declarado como virtual, (Overridable), y en la clase B lo hubiésemos redefinido usando la instrucción Overrides, podríamos acceder a dicho método (de un objeto creado a partir de la clase B) usando tanto una variable de la clase A como una de la clase B; ya que en la memoria solamente existirá un método llamado Mostrar. A diferencia de lo que ocurría antes (al no declararlo como virtual), que realmente existían dos métodos Mostrar.

 

DEFINIENDO INTERFACES

Una interfaz realmente es la definición de los miembros públicos de una clase. Pero en los lenguajes de programación de .NET también podemos definir clases especiales que simplemente definan cómo deben ser los miembros que una clase implemente. Es decir que características deben tener. De esta forma podemos garantizar que si varias clases implementan los miembros definidos en una interfaz, podemos usarlos de manera anónima, es decir, sin necesidad de saber si estamos usando un objeto de una clase o de otra, ya que si ambas clases implementan la interfaz, tendremos la certeza de que dichas clases tienen los miembros definidos en dicha interfaz.

Una interfaz representa un contrato, si una clase implementa una interfaz, está suscribiendo dicho contrato, es más, está obligada a cumplirlo, por tanto, la clase tendrá que definir todos los miembros que la interfaz contenga.

Antes de ver cómo usar las interfaces en nuestras clases, veamos cómo definir una interfaz.

Public Interface IPrueba2
    Property Prop1() As String
    Sub Mostrar()
End Interface

En este caso hemos definido una interfaz llamada IPrueba2 (por convención los nombres de las interfaces siempre empiezan con la letra I mayúscula), en la que se define una propiedad y un método.
Los miembros de las interfaces siempre son públicos y no deben implementar código, solamente la definición propiamente dicha.

 

UTILIZAR INTERFACES EN LAS CLASES

Cualquier clase que quiera disponer de los miembros definidos en la interfaz debe indicarlo de forma expresa, ya que no solo es suficiente con definir los miembros con nombres y características similares.

Para "implementar" en una clase los miembros definidos en una interfaz tendremos que usar la instrucción Implements seguida del nombre de la interfaz. Además tendremos que definir los métodos y propiedades que dicha interfaz contiene, aunque en Visual Basic además hay que indicarlo expresamente, de forma que se sepa con seguridad de que cada uno de esos miembros equivale a los definidos en la interfaz.

En el listado 4 vemos cómo definir una clase que utilice la interfaz que acabamos de ver en la sección anterior.

 

Public Class Prueba2
   Implements IPrueba2

   Public Sub Mostrar() _
              Implements IPrueba2.Mostrar
      ' nuestra version del método Mostrar
   End Sub

   Public Property Prop1() As String _
                   Implements IPrueba2.Prop1
      Get
         ' el código que devuelve
         ' el valor de la propiedad
      End Get
      Set(ByVal value As String)
         ' el código que asigna
         ' el valor de la propiedad
      End Set
   End Property
End Class

Listado 4

 

El método y las dos propiedades deben tener el mismo nombre y parámetros (si los hubiera) que los definidos en la interfaz.
Cuando trabajamos con Visual Basic además debemos indicar expresamente que dicho método o propiedad está "ligado" con el definido en la interfaz, cuando trabajamos con C# no es necesario indicarlo.
La ventaja de esta "redundancia" de VB es que podemos dar un nombre diferente al miembro implementado, pero "internamente" el compilador sabrá que nos estamos refiriendo al que implementa la interfaz.

Tal como podemos ver en el listado 5, a pesar de que en la clase Prueba2B al método le hemos dado otro nombre, realmente está haciendo referencia al que se ha declarado en la interfaz.

 

Public Class Prueba2B
   Implements IPrueba2

   Public Sub OtroNombre() _
              Implements IPrueba2.Mostrar
      Console.WriteLine("Este es el método Mostrar de la clase Prueba2B")
   End Sub

   Public Property Prop1() As String _
                   Implements IPrueba2.Prop1
      Get
         Return "Prop1 de la clase prueba2B"
      End Get
      Set(ByVal value As String)
         '
      End Set
   End Property
End Class

   Dim p2 As New Prueba2
   Dim p2b As New Prueba2B
   Dim i As IPrueba2
   '
   i = p2
   i.Mostrar()
   '
   i = p2b
   i.Mostrar()

Listado 5

 

POLIMORFISMO USANDO CLASES E INTERFACES

En los ejemplos mostrados ya hemos visto cómo usar el polimorfismo tanto a través de variables incluidas en las clases como con interfaces que dichas clases implementan; vamos a detallar un poco más, ya que esta es una de las características más usadas en .NET, por la sencilla razón de que muchas clases de .NET Framework implementan interfaces de forma que podamos acceder a los miembros implementados por medio de variables del tipo de dichas interfaces.

Es más, muchas de las clases de .NET además permiten que demos nueva funcionalidad a nuestras propias clases si implementamos ciertas interfaces.
Por ejemplo, si queremos que nuestra clase sea "clasificable", es decir, que se pueda usar en una colección que clasifique los elementos que contiene, nuestra clase debe implementar la interfaz IComparable.
Si definimos una clase en la que queremos que el método ToString actúe de forma que podamos especificar ciertos formatos a la hora de mostrar el contenido, nuestra clase debe implementar IFormattable.
Que queremos que nuestra clase actúe como una colección, en la que se pueda enumerar o recorrer el contenido de la misma, debemos implementar la interfaz IEnumerable.

Pero esas interfaces propias del .NET Framework lo que harán será darle una nueva funcionalidad a nuestras clases, por tanto, lo importante es saber de que forma actúan los objetos creados en la memoria.

Tal como hemos comentado antes, solo existe un objeto en la memoria y cuando accedemos a él lo podemos hacer bien usando alguna de las clases de las que se deriva o bien mediante alguna de las interfaces que implementa.
Cuando accedemos a dicho objeto mediante algunas de estas clases o interfaces simplemente estamos accediendo a la parte "conocida" por dicho tipo.
En el código del listado 4 y 5 la interfaz IPrueba2 "sabe" cómo acceder al método Mostrar y a la propiedad Prop1, independientemente del objeto que la haya implementado, por tanto si una clase implementa dicha interfaz podemos acceder a esos miembros mediante una variable del tipo IPrueba2, tal como se demuestra en el listado 5 en el que accedemos al método Mostrar definido en la interfaz e implementado por las dos clases.

 

USAR EL POLIMORFISMO PARA ACCEDER A ELEMENTOS DIFERENTES DE UN ARRAY O COLECCIÓN

Una de las utilidades del polimorfismo (de clases o interfaces) es que podemos crear arrays de variables de un tipo "básico" y en ese array incluir objetos que si bien son distintos, en el fondo tienen como parte componente la clase de la que se ha declarado el array.
El ejemplo más básico y válido tanto para las clases declaradas en el propio .NET Framework como las declaradas por nosotros mismos, es crear un array de tipo Object, dicho array podrá contener objetos de cualquier tipo (incluso tipos como números enteros, cadenas, etc.), y posteriormente poder acceder a cualquiera de los elementos mediante un objeto del tipo Object, en cuyo caso solo podremos acceder a los métodos que Object implementa o bien mediante objetos de un tipo en particular, en cuyo caso nos veremos obligados a hacer una conversión desde el tipo contenido en el array (Object) al tipo particular que nos interese.

En el listado 6 podemos ver un ejemplo en el que se crea un array de tipo Object pero que se almacenan tanto objetos del tipo clase A, clase B, IPrueba2, Integer y String.

 

Dim a(6) As Object
'
Dim a1 As New A
a1.Prop1 = "Objeto A"
Dim b1 As New B
b1.Prop1 = "Objeto B"
Dim c1 As New C
c1.Prop1 = "Objeto C"
'
a(0) = a1
a(1) = b1
a(2) = c1
a(3) = New Prueba2
a(4) = New Prueba2B
a(5) = 15
a(6) = "Hola"
'
Dim i As Integer
Console.Write("Usando el método ToString")
Console.WriteLine(" de los objetos contenidos")
For i = 0 To a.Length - 1
   Console.WriteLine("a({0}) = {1}", i, a(i).ToString())
Next
'
Console.WriteLine()
'
Console.WriteLine("Usando un Object")
Dim o As Object
For Each o In a
   Console.WriteLine("o.ToString = {0}", o.ToString())
Next
'
Console.WriteLine()
'
Console.WriteLine("Usando tipos específicos")
For Each o In a
   Console.WriteLine("El tipo es: {0}", o.GetType().Name)
   If TypeOf o Is A Then
      Dim tA As A = CType(o, A)
      tA.Mostrar()
   ElseIf TypeOf o Is IPrueba2 Then
      Dim tIPrueba2 As IPrueba2 = CType(o, IPrueba2)
      tIPrueba2.Mostrar()
   ElseIf TypeOf o Is Integer Then
      Dim tInt As Integer = CType(o, Integer)
      Console.WriteLine(tInt.ToString("00000"))
   ElseIf TypeOf o Is String Then
      Dim tStr As String = o.ToString
      Console.WriteLine(tStr)
   Else
      Console.WriteLine("o.ToString = {0}", o.ToString())
   End If
Next

Listado 6

 

Tal como podemos comprobar en el último bucle de dicho listado, se utiliza la instrucción compuesta TypeOf ... Is para saber si un objeto es de un tipo concreto (en C# usaríamos is), también podemos ver que usando el método GetType podemos obtener el tipo subyacente así como el nombre de dicho tipo.

Si nos fijamos, al hacer la comprobación TypeOf o Is A aquí se procesarán tanto los objetos del tipo A como los derivados de dicho tipo, lo mismo ocurre con la interfaz IPrueba2.

Pero este ejemplo al ser genérico y usando la clase Object seguramente no acabará de "cuajar", por tanto vamos a crear un ejemplo en el que crearemos variables de un tipo concreto: Cliente y derivaremos un par de clases en las que agregaremos nueva funcionalidad a la clase base, posteriormente crearemos un array del tipo Cliente en el que podremos almacenar variables de cualquiera de esos tipos derivados de ella.

Nota:
Por la extensión del listado, el mismo se incluye en el ZIP con el código de los ejemplos (tanto para Visual Basic como para C#), en el listado 7 puedes ver cómo usar esas clases.
 

En dicho código tendremos ocasión de ver cómo podemos implementar la interfaz IComparable para que estas clases se puedan agregar a una colección y posteriormente clasificarlas.
Además implementaremos la interfaz IFormattable para que, si los mostramos por la consola o usamos el método Format de la clase String, podamos usar los siguientes formatos personalizados:

  • ANS mostrará los apellidos, el nombre y el saldo
  • NAS mostrará el nombre, los apellidos y el saldo
  • AN mostrará los apellidos y el nombre (predeterminado)
  • NA mostrará el nombre y los apellidos
  • S mostrará el saldo

 

Sub Main()
    Dim acli(6) As Cliente
    '
    acli(0) = New Cliente("Jose", "Sanchez", 125.5D)
    acli(1) = New ClienteOro("Luis", "Rebelde", 2500.75D)
    acli(2) = New ClienteMoroso("Antonio", "Perez", -500.25D)
    acli(3) = New Cliente("Miguel", "Rodriguez", 200)
    acli(4) = New ClienteMoroso("Juan", "Ruiz", -310)
    acli(5) = New ClienteOro("Mariano", "Alvarez", 500.33D)
    acli(6) = New Cliente("Carlos", "Bueno", 975)
    ' 
    Console.WriteLine("Antes de clasificar:")
    For Each c As Cliente In acli
        Console.WriteLine("{0}, saldo= {1}", c, c.MostrarSaldo())
    Next
    Array.Sort(acli)
    '
    Console.WriteLine()
    Console.WriteLine("Después de clasificar:")
    For Each c As Cliente In acli
        Console.Write("{0}, saldo= {1}", c, c.MostrarSaldo())
        If TypeOf c Is ClienteOro Then
            Console.WriteLine(" -> $$$ es cliente ORO $$$")
        ElseIf TypeOf c Is ClienteMoroso Then
            Console.WriteLine(" -> OJO que es un cliente moroso")
        Else
            Console.WriteLine()
        End If
    Next
    '
    Console.WriteLine()
    Console.WriteLine("Mostrar usando formatos:")
    For Each c As Cliente In acli
        Console.WriteLine("Usando NAS= {0:NAS}", c)
        Console.WriteLine("Usando AN= {0:AN}", c)
        Console.WriteLine("Usando S= {0:S}", c)
    Next
    '
    Console.ReadLine()
End Sub

Listado 7

 

CONSTRUCTORES Y SOBRECARGA DE CONSTRUCTORES

El punto de inicio de cualquier clase, cuando se crea una instancia en la memoria, es un método especial al que se le conoce como constructor.

Un constructor no devuelve ningún valor, por tanto en Visual Basic sería un método de tipo Sub llamado New (en C# no se declara como void, simplemente tendrá el mismo nombre de la clase).

Los constructores también se pueden sobrecargar, es decir, pueden existir varias versiones en las que cada una de ellas reciba distintos parámetros, en número y/o en tipo.

Debido a que todas las clases (y estructuras) deben tener un constructor, si nosotros no lo definimos de forma expresa, será el propio compilador el que se encargue de añadirlo por nosotros, en ese caso será un constructor en el que no se reciba ningún argumento.
Aunque hay que tener presente que en el momento en que hemos definido un constructor el compilador ya no agregará ninguno de forma automática. Esto tiene sus ventajas, ya que en ocasiones es posible que nos interese que nuestras clases solamente se puedan instanciar si se le pasa algunos parámetros al constructor de la misma.
Pero si además de un constructor con parámetros queremos seguir manteniendo el constructor "predeterminado", entonces tendremos que declararlo aunque no escribamos nada de código en el interior.

En el listado 8 podemos ver la clase Cliente con tres constructores.

 

Public Class Cliente
   Implements IComparable, IFormattable
   '
   Private _nombre As String
   Private _apellidos As String
   Private _saldo As Decimal
   '
   Public Sub New()
   End Sub

   Public Sub New( _
         ByVal elNombre As String, _
         ByVal losApellidos As String)
      _nombre = elNombre
      _apellidos = losApellidos
   End Sub

   Public Sub New( _
         ByVal elNombre As String, _
         ByVal losApellidos As String, _
         ByVal elSaldo As Decimal)
      _nombre = elNombre
      _apellidos = losApellidos
      _saldo = elSaldo
   End Sub

Listado 8

 

LOS CONSTRUCTORES NO SE HEREDAN
El constructor es el único miembro de una clase que no se hereda, por tanto si necesitamos crear constructores en clases derivadas debemos declararlos de forma explícita.
 

 

LOS CONSTRUCTORES DE LAS ESTRUCTURAS

Los constructores de las estructuras son un caso especial, ya que siempre existirá un constructor sin parámetros, el cual además no podemos definirlo por medio de código, porque es el propio compilador el que se encargará de su creación. Por tanto en las estructuras solamente podemos definir constructores parametrizados (que reciban parámetros) y en el caso de definirlo, en el código del mismo, tendremos que asignarle un valor a cualquiera de las propiedades (o campos) públicos que tenga esa estructura, siempre y cuando no sean estáticos (compartidos).

 

CONSTRUCTORES QUE USAN OTROS CONSTRUCTORES

Debido a que los constructores pueden recibir parámetros, en algunas ocasiones nos puede ser útil poder llamar a otros constructores, por ejemplo de la clase base o de la misma clase.

En estos casos, en Visual Basic es fácil hacerlo, como sabemos que los constructores realmente son métodos llamados New, los podemos usar de la misma forma que haríamos con cualquier otro método.

Por ejemplo, si modificamos el código mostrado en el listado 8, podríamos hacer esto:

Public Sub New( _
      ByVal elNombre As String, _
      ByVal losApellidos As String, _
      ByVal elSaldo As Decimal)

   Me.New(elNombre, losApellidos)
   _saldo = elSaldo

End Sub

De forma que desde el constructor que recibe tres parámetros llamemos al que recibe dos.

En este caso, la instrucción o palabra clave Me representa a la instancia actual (el objeto que se ha creado en memoria).
Si en lugar de llamar a otro constructor de la propia clase, queremos llamar a un constructor de la clase base, en lugar de Me, usaremos MyBase.

En C# este mismo código se haría de la siguiente forma:

public Cliente(string elNombre, 
               string losApellidos, 
               decimal elSaldo) 
              : this(elNombre, losApellidos) 
{
    _saldo = elSaldo;
}

Es decir, se llamaría al otro constructor indicándolo después del cierre de paréntesis y separándolo con dos puntos.

En C# la instrucción o palabra clave que hace referencia a la instancia actual es this y la que hace referencia a la clase base es: base.

 

CLASES ABSTRACTAS

Tal como comentamos el mes anterior, en ocasiones tendremos la necesidad de crear clases que no se puedan usar para crear objetos, pero si para usarla como base de otras.
La razón principal para que hacer que una clase no sea instanciable es que dicha clase por sí sola no tenga ningún sentido, al menos para poder crear nuevos objetos de ese tipo, pero si tendrá sentido si la usamos como clase base de otras clases.

Por ejemplo, podríamos tener una clase Animal, la cual no tendría sentido si a partir de ella se pudiesen crear nuevos objetos, ya que el concepto de animal es demasiado abstracto para poder crear un objeto a partir de él. Pero si la podríamos utilizar para derivar de ella otras clases que bien podrían ser a la vez abstractas o bien clases "normales".

.NET Framework nos permite crear clases abstractas, las cuales son como las interfaces, pero las que pueden tener métodos y otros miembros que tengan no solo la definición de esos miembros sino también código funcional. Además, debido a que las clases abstractas están pensadas para usarse como clases base de otras, todos los miembros son virtuales de forma predeterminada, por tanto no es necesario indicarlos usando el modificador Overridable (virtual en C#).

 

DECLARAR CLASES ABSTRACTAS

La definición de una clase abstracta se hace como las clases normales, salvo de que hay que usar el modificador MustInherit (abstract en C#), de esta forma le indicamos al compilador de que nuestra intención es la de que nuestra clase sea "heredable".

Como ya hemos comentado anteriormente, el concepto de clases abstractas o clases que solo se pueden usar como clases base es importante cuando queremos ofrecer cierta funcionalidad a nuestras clases, sobre todo las que formen parte de ciertas jerarquías de clases, ya que las clases abstractas nos servirán para definir el comportamiento del resto de las clases, además de que, como las interfaces, nos permitirán disponer de cierta funcionalidad o, mejor dicho, nos permitirán dar unas pautas que los que decidan usarlas, tendrán que seguir.
Pero a diferencia de las interfaces, las clases abstractas no solo representarán un contrato, sino que también pueden ofrecer "de fábrica" un funcionamiento que, aunque las clases derivadas no implementen, éstas ya tendrán, en este aspecto funcionarán como las clases normales, ya que las clases derivadas podrán usar el código implementado en ellas.

 

MIEMBROS ABSTRACTOS

Cuando comentamos que las clases abstractas son como las interfaces no solo nos referíamos a que se podrían usar para proporcionar polimorfismo, sino porque en las clases abstractas también podemos definir miembros que a su vez sean abstractos, es decir, que en las clases abstractas solamente se defina el método o propiedad, pero que no tenga ninguna funcionalidad, es más, si declaramos un miembro como abstracto la clase que se derive de la clase abstracta estará obligada a definirlo.
Esto nos ofrece la ventaja de poder definir miembros funcionales y miembros que solo tengan sentido en las clases derivadas y por tanto serán las clases derivadas las que deban definir el código que los haga funcionales y le den la utilidad adecuada.

Para indicar que un miembro es abstracto, debemos indicarlo usando la instrucción MustOverride (abstract en C#), de esta forma nos permitirá el compilador escribir solo la definición del mismo y no tener que escribir ningún código.
En las clases derivadas tendremos que usar la instrucción Overrides (override en C#) de la misma forma que lo usamos con el resto de las clases para indicar que dicho método está reemplazando a un miembro definido en la clase base.

Una puntualización: Los miembros abstractos solo se pueden definir en clases abstractas.

En el código incluido en el ZIP que acompaña a este artículo se incluye un ejemplo que define y usa las clases abstractas, como es habitual, se incluye código para Visual Basic y C#.

 

CLASES SELLADAS (NO HEREDABLES)

Otro concepto, que posiblemente pueda parecer que no está "ligado" con la herencia es el de las clases selladas o no heredables.

Este tipo de clases no permitirán que se usen como clases base de otras nuevas clases.

La existencia de las clases normales y las abstractas nos permiten derivar nuevas clases a partir de ellas, eso tiene sentido, pero, ¿qué sentido puede tener una clase de la que no se puedan derivar nuevas clases?

La razón principal para definir una clase como sellada o no heredable es precisamente porque no queremos que nadie pueda modificar el comportamiento de dicha clase, ya que al no poder usarla como base de nuevas clases, nadie podrá ofrecer nueva funcionalidad, de esta forma nos aseguramos que esa clase siempre funcionará como la hemos definido.

Esto es así, porque al estar "sellada" tampoco podremos definir miembros virtuales, por la sencilla razón de que nadie podrá derivar nuevas clases y por tanto tampoco podrá reemplazar el comportamiento de los mismos.

Debido a que el comportamiento normal de una clase es que sea heredable, para poder crear clases que estén selladas, y por tanto hacerlas no heredables, debemos usar la instrucción o modificador NotInheritable (sealed en C#).

 

 

Conclusiones

Confío que todo el tema tratado sobre la programación orientada a objetos, y por extensión a la herencia, desde el punto de vista de un programador de .NET Framework nos permita afrontar todo este tema, que si bien al principio puede parecer algo "escabroso", realmente debería ser una forma natural de programar, sobre todo si tenemos en cuenta que todo el .NET Framework se basa en las clases, interfaces y demás conceptos relacionados con la POO.

 

Nos vemos.
Guillermo

 


Código de ejemplo (comprimido):

 

El código de ejemplo (para Visual Basic y C#): POO_Iberprensa.zip - 19.80 KB

MD5 checksum: 9E8FB3E10DD7CCC8826D2B0C1D5F6533

Nota:
Los proyectos están creados con Visual Studio .NET 2003 pero se pueden usar en cualquier versión de Visual Studio 2005, incluso las versiones Express, aunque en este último caso, tendrás que cargar de forma independiente los proyectos de Visual Basic o de Visual C#.

 


Ir al índice principal de el Guille