Entrega número 18, (13/Dic/2006)
Publicada el 13/Dic/2006
Actualizado el 01/Feb/2007
Autor: Guillermo 'guille' Som
En esta entrega del curso de iniciación a la programación con Visual Basic .NET vamos a ver los conceptos teóricos de la programación orientada a objetos (POO). Todo lo aquí explicado es válido para todas las versiones de Visual Basic .NET, aunque hay conceptos que en la versión 2005 se han ampliado o mejorado, lo aquí explicado es igualmente válido. En otra ocasión te explicaré las cosas nuevas de la versión 2005 de Visual Basic para .NET.
Importante:
Este artículo está registrado por Iberprensa (Studio Press) y está prohibida la reproducción total o parcial incluso indicando la procedencia.
Versión originalmente publicada en Todo Programación número 6 (Sep 2004).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.
LA PROGRAMACIÓN ORIENTADA A OBJETOS
En Todo Programación existe una sección denominada Cuadernos de Principiantes donde se estudia algoritmia y estructuras de datos a nivel iniciación. Está planificado que se estudie a nivel teórico la programación orientada a objetos, por tanto para aquellos que no tengáis noción alguna sobre POO mejor guarda a buen recaudo este número de TP y espera a aprender los conceptos teóricos necesarios para luego aplicarlos en el marco, nunca mejor dicho, de .NET.
LOS PILARES DE LA POO
Recordemos que tres son las principales características de un lenguaje orientado a objetos, es decir, se considera que un lenguaje está totalmente orientado a objetos si es capaz de proveer estas tres características:
Encapsulación
Herencia
Polimorfismo
Veamos una pequeña descripción de cada una de ellas y después las ampliaremos para comprender mejor su significado y cómo puede ayudarnos a crear aplicaciones que aprovechen todas las posibilidades que nos da la POO.
• La ENCAPSULACIÓN es la cualidad de unificar los datos y la forma de manipularlos, de esta forma podemos ocultar el funcionamiento de una clase y exponer solo los datos que manipula (mediante propiedades), así como proveer de medios para poder manipular dichos datos (mediante métodos). De esta forma solo exponemos al mundo exterior la información y la forma de manipularla, ocultando los detalles usados para manejar esos datos y, lo que es más importante, evitando que nadie manipule de una forma no controlada dicha información.
• La HERENCIA es la cualidad de poder crear nuevas clases (o tipos) basadas en otras clases, de forma que la nueva clase obtenga todas las características de la clase que ha heredado, tanto los datos que contiene como la forma de manipularlos, pudiendo añadir nuevas características e incluso cambiar el comportamiento de algunas de las incluidas en la clase base, (siempre que así se haya previsto). Mediante la herencia podemos crear de forma fácil una jerarquía de clases que comparten un mismo comportamiento básico pero que cada nueva generación puede tener (y de hecho tiene) un nuevo comportamiento.
• El POLIMORFISMO es la cualidad de implementar de forma particular algunas de las características que tienen las clases, de forma que cuando necesitemos usarlas no nos preocupe la implementación interna que cada una tenga, lo que realmente nos interesa o nos debe importar es que podemos usar esas características e incluso podamos acceder a ellas de forma anónima... o casi.
OTROS CONCEPTOS DE LA POO
Tal como tendrás oportunidad de ver en los Cuadernos de Principiantes y lo indicado en el cuadro Los pilares de la POO, la POO se basa en tres características que son comunes a todos los lenguajes orientados a objetos, pero si tenemos esas características y no sabemos cómo aplicarlas, la verdad es que no nos será de mucha utilidad.
Pero antes de ver algo de código concreto, creo que es importante que aprendamos otros conceptos relacionados también con la POO, pero esta vez desde un punto de vista del programador, es decir, vamos a dejar en parte la teoría y vamos a ser algo más prácticos, ya que los siguientes conceptos serán con los que tendremos que "bregar" a diario. Además nos interesa conocerlos para aprovechar lo que un lenguaje de programación orientado a objetos nos ofrece, si bien, es posible que, al menos de forma genérica, no todos los lenguajes dispongan de ellos.
Por eso, aunque lo que se ha dicho y se diga a continuación será válido para cualquier lenguaje orientado a objetos, lo vamos a enfocar desde el punto de vista de .NET Framework, más concretamente desde el punto de vista del programador de Visual Basic .NET y C#.
LAS CLASES Y ESTRUCTURAS
Como hemos estado mencionando, en los lenguajes orientados a objetos, existe el concepto clase. Cuando hablamos de clases, también podemos extenderlo a estructuras, de hecho, para los programadores de C++ una clase no es más que una estructura que se comporta de forma diferente.
Una clase es una pieza de código en la que podemos definir una serie de datos y al mismo tiempo unos métodos (funciones o procedimientos) que nos permitirán acceder a esos datos.
Cuando definimos una clase, lo que estamos haciendo es definir una plantilla, a partir de la cual podemos crear objetos en la memoria. Por tanto, la clase es el molde con el cual podemos crear nuevos objetos. Para poder crear algo "tangible" a partir de una clase, tenemos que crear en la memoria un nuevo objeto del tipo de la clase, en estos casos lo que decimos es que instanciamos un nuevo objeto de la clase. A partir de ese momento tendremos algo real con lo que podemos trabajar: una instancia de la clase, es decir, la definición realizada en la clase se ha convertido en un objeto al que podemos acceder y que podemos empezar a utilizar, dándole nuevos valores a los datos que manipula y usando las funciones que nos permiten manipular dichos datos.
La diferencia principal entre una clase y una estructura es la forma en que se crean los objetos que representan a esas "ideas". Los objetos creados a partir de las clases son objetos por referencia, es decir, si declaramos una variable para manipular ese objeto, lo que tendremos será una referencia (o puntero) a una dirección de memoria en la que realmente está el objeto. Mientras que los objetos creados a partir de una estructura se almacenan de forma diferente, en lugar de "apuntar" a una dirección de memoria en la que se encuentra el objeto, es como si las variables declaradas como estructuras fuesen realmente el objeto permitiéndonos hacer ciertas operaciones y manipulaciones que los objetos obtenidos a partir de una clase no pueden realizar de la misma forma. Esto lo veremos después con más detalle.
NOTA: Clases
En .NET siempre usamos una clase para escribir cualquier tipo de código. Por tanto, hagamos lo que hagamos en .NET Framework, debemos hacerlo dentro de una clase. Esto no quiere decir que siempre tengamos que usar las características de la POO, ya que si simplemente queremos hacer una aplicación que muestre un mensaje en la consola, el código no tiene porqué usar la herencia, el polimorfismo o la encapsulación, simplemente escribimos el código que muestre el mensaje y asunto arreglado, pero lo que si podremos hacer es usar algunas de las "otras" ventajas que nos aporta la programación orienta a objetos.
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.
CONSTRUCTORES Y DESTRUCTORES, EL PUNTO DE INICIO Y FINAL DE LAS CLASES
Cuando creamos un objeto a partir de una clase, se sigue un proceso, el cual empieza en el momento en que decidimos crear una nueva instancia de dicha clase.
En estos casos, el compilador utiliza lo que se llama el constructor de la clase. Siempre que se crea un nuevo objeto en la memoria está involucrado el constructor de la clase.
Los constructores son procedimientos especiales (funciones que no devuelven un valor) en los que podemos escribir toda la lógica que debe usarse para la correcta creación del objeto. Por ejemplo, podemos inicializar las variables usadas, podemos asignarle algunos valores predeterminados, etc.De igual forma, cuando un objeto ya no se necesita más, se destruye mediante una llamada al destructor de la clase. En .NET la destrucción de los objetos suele hacerse de forma automatizada, es decir, a diferencia de lo que ocurre en otros entornos de programación, no es necesario destruir explícitamente un objeto para eliminarlo de la memoria, esa gestión de limpieza de objetos la realiza el recolector de basura (Garbage Collector, GC) de .NET, el cual decide cuando un objeto no se necesita más y en ese caso lo elimina dejando libre la memoria utilizada para otros menesteres.
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.
LOS MIEMBROS DE LAS CLASES: CAMPOS, PROPIEDADES Y MÉTODOS
Como hemos comentado, las clases manejan datos y proveen de funciones para acceder a esos datos. Para ser precisos, los datos se mantienen o almacenan internamente en los campos declarados en las clases. Los campos no son otra cosa que variables declaradas en la clase, habitualmente declaradas de forma privada. ¿Por qué declaradas de forma privada? Precisamente para seguir o cumplir la característica de encapsulación de la POO, es decir, los datos no deben exponerse de forma directa.
Si queremos exponer los datos, podemos usar las propiedades. Las propiedades son funciones especiales que nos permiten acceder a esos datos, aunque para ser más precisos, las propiedades realmente representan a los datos que una clase contiene, al menos de forma pública. De esa forma podemos "controlar" la forma en que se leen o asignan esos datos, ya que las propiedades realmente son funciones en las que podemos escribir código para controlar los valores asignados o leídos.
Los métodos nos permitirán realizar acciones sobre los datos, por ejemplo devolver un rango de valores o simplemente una representación amigable de la información contenida. Debido a que algunas veces los métodos devolverán algo y otras no, podemos usar tanto funciones que devuelvan o no un valor.
NOTA: Métodos
En C# los métodos siempre son funciones, que devolverán un tipo concreto o el valor especial void, que se usa para indicar que una función no devolverá ningún valor.
En Visual Basic .NET existen dos tipos de métodos distintos, las funciones (Function) que siempre devuelven un valor y los procedimientos (Sub) que no devuelven ningún valor.
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).
EL ÁMBITO DE LOS MIEMBROS DE LAS CLASES
Las buenas formas de trabajar con las clases nos indican que los campos deberían ser privados, con idea de que no estén accesibles de forma externa. Por supuesto también podemos definir otros miembros de las clases de forma privada, esto es útil cuando la funcionalidad es para uso exclusivo de otros miembros de la clase. Pero cuando queremos exponer la funcionalidad fuera de la clase podemos hacerla de varias formas, aquí es donde entran en juego el ámbito de los miembros de las clases.
El ámbito lo aplicamos para permitir el acceso desde cualquier código fuera de la clase o para restringir ese acceso. Dependiendo de cómo queramos que se acceda a los miembros de la clase podemos usar distintos modificadores de ámbito.
Veamos los que podemos usar y cuando y porqué usarlos.
La instrucción entre paréntesis será la que tendremos que usar en C#.
Private (private). Para uso privado. Cuando declaramos un miembro como privado sólo lo podremos acceder desde la propia clase. Este es el más restrictivo y el que se recomienda para los campos y las funciones de uso interno.
Protected (protected). Uso protegido. Los elementos declarados como protegidos sólo estarán accesibles, además de en la propia clase, por cualquier clase derivada.
Friend (internal). Para uso dentro de la propia aplicación. Cuando declaramos un miembro con este modificador, solo podremos acceder a él desde la propia clase o desde cualquier código que se encuentre en el mismo ensamblado (proyecto).
Protected Friend (protected internal). Una mezcla de Protected y Friend, es decir solo accesible desde las clases derivadas o desde el mismo proyecto.
Public (public). Este modificador de ámbito nos permite exponer públicamente cualquier miembro de la clase, de forma que no haya restricciones para acceder a él.
NOTA: Ámbito
Los miembros de una clase los podemos declarar sin especificar el ámbito, dependiendo del lenguaje de programación que usemos se aplicará un modificador de ámbito u otro. En C#, si no indicamos el ámbito, las declaraciones se consideran privadas, mientras que en Visual Basic .NET el ámbito predeterminado es Friend.
MIEMBROS VIRTUALES, NO REEMPLAZABLES Y ABSTRACTOS
Para ir terminando la parte "teórica" sobre la programación orientada a objetos, veamos cómo podemos darle un significado distinto a los miembros de una clase, dependiendo de cómo queramos que se comporten y por extensión cómo podemos utilizarlos tanto en la propia clase como en las clases derivadas.
Como hemos comentado, cuando una clase hereda a otra podemos modificar el comportamiento de los miembros heredados, pero estos solamente se podrán modificar si la clase base así lo contempla o lo permite. De forma predeterminada, al menos en .NET, cuando declaramos un método o una propiedad en una clase, solo podremos acceder a él desde una instancia creada (un objeto) en memoria, desde donde podemos usarlos dependerá del ámbito que le hayamos aplicado. De igual forma, el que una clase que se base en otra, pueda crear su propia versión de ese método o propiedad dependerá de que la clase base lo haya declarado como virtual (Overridable en VB .NET). Los métodos virtuales serán los que podamos sobrescribir en las clases derivadas, de forma que podamos crear nuestras propias versiones. En .NET los miembros de una clase no son virtuales de forma predeterminada. Por tanto, si queremos que la clase derivada pueda crear su propia versión de un método, debemos declararlo como virtual o "redefinible".
Si en una clase base hemos definido un método virtual, pero posteriormente queremos que no se pueda seguir redefiniendo en otras clases derivadas, debemos indicarlo usando el modificador NotOverridable, el cual se usará junto con Overrides, ya que sobrescribe un miembro de la clase base y como además lo queremos marcar como no virtual, debemos usar las dos instrucciones: Overrides NotOverridable, (en C# se indicará con override sealed).
Pero también se nos puede presentar el caso contrario, en el que queremos que un método forzosamente haya que redefinirlo en las clases derivadas, en esos casos la clase base que lo define no incluye ninguna implementación, es decir, el método no contiene código ejecutable, solo la definición, (como ocurre con las interfaces). Se dice que estos métodos son abstractos porque solo se ha definido en la forma y no se ha implementado ningún código ejecutable. En Visual Basic se definen usando el modificador MustOverride (asbtract en C#). Estos métodos abstractos solo se pueden declarar en clases abstractas (MustInherit en Visual Basic, abstract en C#) y por la necesidad de tener que redefinirlos, son implícitamente virtuales.
Las instrucciones o modificadores que nos permiten crear estos tipos de miembros son:
Overridable (virtual). Los miembros virtuales son los que las clases derivadas puedes sobrescribir para crear su propia versión. Para indicar en una clase derivada que estamos sobrescribiendo dicho elemento, usaremos Overrides (override en C#).
NotOverridable (sealed). Los miembros virtuales heredados se pueden marcar como no virtuales, (las siguientes clases derivadas no podrán sobrescribirlos), usando esta instrucción a continuación de Overrides (override).
MustOverride (abstract). Un método que se debe reemplazar en la clase derivada y que solo se define en la clase base sin ningún código ejecutable. Los métodos abstractos son virtuales por defecto y solo se pueden declarar en clases abstractas.
MIEMBROS DE INSTANCIAS Y COMPARTIDOS
En todos estos casos, los miembros de la clase siempre son miembros de instancia, es decir, solo existen en la memoria cuando se crea un nuevo objeto (se crea una nueva instancia). Pero es posible que nos interese crear miembros compartidos, es decir, miembros que pertenecen a la propia clase, no a ninguna instancia en particular. Dándonos la oportunidad de poder acceder siempre a ellos, independientemente de que hayamos creado o no un nuevo objeto en la memoria. En estos casos decimos que creamos miembros compartidos (estáticos en el argot de C#/C++), esta diferencia de "nomenclatura" dependiendo del lenguaje de programación, es porque para definir un miembro perteneciente a la clase y no a una instancia en particular, usaremos en Visual Basic la instrucción Shared (compartido), mientras que en C# se usará la instrucción static (estático).
Resumiendo, Shared (static), declara un miembro compartido, los miembros compartidos no pertenecen a ninguna instancia en particular y solamente pueden acceder a campos u otros miembros también compartidos. Desde los miembros de instancia podemos acceder tanto a miembros compartidos como de instancia.
NOTA: STATIC
En Visual Basic existe también la instrucción Static, (que no tiene equivalencia en C#), en este caso se utiliza con variables declaradas en un procedimiento y sirven para indicar que esa variable debe mantener el valor entre distintas llamadas a dicho procedimiento, a diferencia del resto de variables que solo existen mientras se ejecuta el código del procedimiento y cuyos valores se pierden al finaliza la ejecución del mismo.
CLASES ABSTRACTAS Y SELLADAS
De igual forma que podemos modificar el comportamiento de los miembros de una clase, también podemos cambiar el comportamiento predeterminado de las clases.
Como hemos comentado, las clases de .NET pueden usarse para crear nuevas clases derivadas de ellas, esta es la funcionalidad predeterminada, pero no obligatoria, es decir, si queremos podemos usar una clase por si misma o como base de otras.
Pero también podemos hacer que una clase solamente se use como clase base de otras, pero no se puedan usar para crear nuevas instancias en memoria, este es el caso de las clases abstractas. Una clase abstracta puede contener miembros abstractos, miembros normales o virtuales. Para indicar que una clase es abstracta, se usa el modificador MustInherit en Visual Basic o abstract en C#.
La contrapartida de las clases abstractas son las clases selladas o clases que no se pueden usar como clases base, en estos casos las clases las definiremos como NotInheritable en Visual Basic o sealed en C#. Como es lógico, las clases no heredables se pueden usar en ocasiones en las que no nos interese que nadie cambie el comportamiento que tiene, por tanto no se podrán declarar miembros virtuales ni abstractos, ya que no tendría ningún sentido.
Las estructuras siempre son "clases selladas", (aunque no se use un modificador para indicarlo), por tanto, no podemos usarlas como base de otras.
Conclusiones
En la segunda parte de esta serie dedicada a la programación orientada a objetos veremos cómo poner en práctica todo lo que hemos comentado, además de ver otras peculiaridades de la POO, tales como la definición de interfaces y cómo implementarlas, ocasión que también aprovecharemos para ver de forma práctica cómo usar el polimorfismo y la herencia en los lenguajes de .NET.
Nos vemos.
Guillermo