.NET |
Programación Orientada a Objetos en .NETPublicado el 31/Oct/2006
|
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 OBJETOSEn 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 POORecordemos 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:
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 POOTal 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 ESTRUCTURASComo 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
INTERFACESCuando 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
CONSTRUCTORES Y DESTRUCTORES, EL PUNTO DE INICIO Y FINAL DE LAS CLASESCuando 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. 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
LOS MIEMBROS DE LAS CLASES: CAMPOS, PROPIEDADES Y MÉTODOSComo 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
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 CLASESLas 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.
NOTA: Ámbito
MIEMBROS VIRTUALES, NO REEMPLAZABLES Y ABSTRACTOSPara 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:
MIEMBROS DE INSTANCIAS Y COMPARTIDOSEn 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
CLASES ABSTRACTAS Y SELLADASDe 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.
ConclusionesEn 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.
|