.NET |
Programación Orientada a Objetos en .NET (2)Conceptos prácticosPublicado el 13/Dic/2006
|
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 POOTal 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:
DEFINIR CLASES EN VISUAL BASIC .NETAntes de poder usar las características de la POO
tendremos primero que aprender a declarar una clase. NOTA:
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 HEREDADOSTal 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. 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 .NETLa 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 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 HEREDADOSTodo 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... 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.
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. 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.
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. 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:
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. 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. 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.
NOTA:
INDICAR LOS MIEMBROS VIRTUALESComo 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. 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.
DEFINIENDO INTERFACESUna 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.
UTILIZAR INTERFACES EN LAS CLASESCualquier 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. 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 INTERFACESEn 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. 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.
USAR EL POLIMORFISMO PARA ACCEDER A ELEMENTOS DIFERENTES DE UN ARRAY O COLECCIÓNUna 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. 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: 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.
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 CONSTRUCTORESEl 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. 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
LOS CONSTRUCTORES DE LAS ESTRUCTURASLos 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 CONSTRUCTORESDebido 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). 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 ABSTRACTASTal 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. 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 ABSTRACTASLa 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.
MIEMBROS ABSTRACTOSCuando 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. 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. 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#).
ConclusionesConfí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.
|
Código de ejemplo (comprimido): |
El código de ejemplo (para Visual Basic y C#): POO_Iberprensa.zip -
19.80 KB Nota:
|