Beruflich Dokumente
Kultur Dokumente
NET
Introducción:
La programación orientada a objetos (POO) nos permite escribir código menos propenso a fallos
además de permitirnos la reutilización de código de forma más conveniente.
En este artículo veremos las características de la POO desde el punto de vista de los lenguajes de .NET
Framework y cómo utilizar los distintos elementos que nos permitirán crear código que sea más fácil de
escribir y mantener.
INTERFACES
Cuando hablamos de polimorfismo, ineludiblemente tenemos que hablar de las interfaces, ya que,
principalmente, nos posibilita utilizar esta característica de la POO. La pregunta es: ¿qué es una
interfaz? Aquí no hablamos de "interfaces de usuario", es decir, lo que se mostrará al usuario de nuestra
aplicación, sino a una clase especial en la que solamente se definen los métodos y propiedades que una
clase que la implemente debe codificar. Las interfaces representan un contrato, de forma que cualquier
clase que la implemente debe utilizar los miembros de la interfaz usando la misma forma en que ésta la
ha descrito: mismo número de argumentos, mismo tipo de datos devuelto, etc.
Gracias a la implementación de interfaces podemos crear relaciones entre clases que no estén derivadas
de la misma clase base, pero que tengan métodos comunes, al menos en la forma, aunque no
necesariamente en el fondo. Anteriormente usamos el ejemplo del método Guardar, este método se
puede definir en una interfaz, las clases que quieran implementar un método Guardar "estandarizado"
firmarán un contrato con la interfaz que lo especifica, aunque la forma interna de funcionamiento solo
atañe al programador de la clase, lo importante es saber que cualquier clase que haya firmado ese
contrato tendrá que seguir las condiciones impuestas por la interfaz, de esta forma todas las clases
tendrán un método Guardar "compatible", aunque, tal como mostramos antes, cómo se realice esa
acción de guardar no debe preocuparnos, simplemente nos fiaremos de que se ha implementado
adecuadamente para almacenar los datos que la clase manipula.
NOTA: HERENCIA MÚLTIPLE Y HERENCIA SIMPLE
En C++ y algunos otros lenguajes orientados a objetos se permiten la herencia múltiple, es decir, una
clase se puede derivar de varias clases a la vez. Los lenguajes de .NET Framework, usan lo que se
denomina herencia simple, es decir, una clase solo se puede derivarse directamente de otra clase, si
bien se permite implementar múltiples interfaces.
Pero debido a cómo funciona la herencia, cualquier clase derivada a partir de otra, heredará
indirectamente todas las clases e interfaces que la clase base haya heredado o declarado. Además, en
.NET, todas las clases siempre se derivan de la clase base Object que es la clase que está en la parte
superior de la jerarquía de clases.
SOBRECARGA (OVERLOAD)
Una de las características que también nos ofrece los lenguajes orientados a objetos es la posibilidad de
definir varias funciones de las clases con un mismo nombre, de esta forma, podremos crear versiones
diferentes, por ejemplo para que reciban argumentos de distintos tipos sin necesidad de cambiarle el
nombre.
Supongamos que queremos hacer una función que realice cualquier tipo de operación sobre dos valores
numéricos, sería lógico pensar que si esos valores son de tipo entero, el resultado que devuelva la
función también debería ser de tipo entero, en caso de que los valores a usar en la operación son de tipo
flotante, el resultado podría devolverlo de ese mismo tipo.
En los lenguajes no orientado a objetos, tendríamos que crear dos funciones con nombres diferentes,
por ejemplo: sumaInt y sumaFloat. Pero la sobrecarga nos permite crear dos funciones que se llamen
suma y el compilador utilizará la adecuada según el tipo de datos que pasemos como argumentos.
El único requisito para poder crear sobrecargas de métodos es que las diferentes versiones se
diferencien en los argumentos, ya sea porque sean de diferentes tipos de datos o porque el número de
argumentos usados sea diferente, de esa forma el compilador no tendrá ningún problema en saber cual
debe usar en cada ocasión. La sobrecarga la podemos aplicar tanto a los constructores como a cualquier
otro método de la clase.
NOTA: Sobrecarga
No existirá la posibilidad de crear métodos sobrecargados si solamente se diferencian en el tipo de
datos devuelto, ya que en esos casos el compilador no podrá decidir correctamente qué método debe
utilizar.
Además de los campos, métodos y propiedades, las clases tienen otros miembros como los eventos y
las enumeraciones. Éstos nos permitirán recibir notificaciones de cuando algo ocurra (eventos) o
declarar ciertos valores constantes que podemos usar para restringir algunos valores asignados a las
propiedades o que nos permitan seleccionar de forma coherente la información que queremos obtener
(enumeraciones).
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
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.
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.
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
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.
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
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
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.
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.
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
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
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#).
Dos maneras de mostrar las relaciones de clase en la programación orientada a objetos son las
relaciones de identidad y de pertenencia. En una relación de identidad, la clase derivada es claramente
un tipo de clase base. Por ejemplo, una clase denominada PremierCustomer representa una relación de
tipo identidad con una clase base denominada Customer, puesto que un cliente principal es un cliente.
No obstante, una clase denominada CustomerReferral representa una relación de pertenencia con la
clase Customer porque una cartera de clientes tiene un cliente, pero no es un tipo de cliente.
Los objetos de una jerarquía de herencia deben tener una relación de identidad con la clase base puesto
que heredan los campos, propiedades, métodos y eventos definidos en dicha clase. Las clases que
representan una relación de pertenencia con otras clases no son válidas para jerarquías de herencia
debido a que podrían heredar propiedades y métodos inadecuados. Por ejemplo, si la clase
CustomerReferral se derivase de la clase Customer descrita anteriormente, podría heredar propiedades
que no tendrían sentido, como ShippingPrefs y LastOrderPlaced. Las relaciones de pertenencia como
ésta deben representarse mediante clases o interfaces no relacionadas. La siguiente ilustración muestra
ejemplos de relaciones de tipo "es un" y "tiene un".
Clases base y reutilización de código
Otra razón para usar la herencia es la ventaja de poder reutilizar el código. Las clases bien diseñadas,
una vez depuradas, pueden utilizarse una y otra vez como base de nuevas clases.
Un ejemplo común de reutilización eficaz de código está relacionado con bibliotecas que administran
estructuras de datos. Por ejemplo, suponga que tiene una gran aplicación comercial que administra
varias clases de listas en la memoria. Una es una copia en memoria de la base de datos de clientes, que
se lee desde una base de datos al iniciar la sesión para conseguir mayor velocidad. La estructura de
datos tendría un aspecto similar al siguiente: