Curso de CSharp
Curso de CSharp
AVILÉS
Principado de Asturias
CURSO DE C#
INTRODUCCIÓN
Quizás el más directo competidor de C# es Java, lenguaje con el que guarda un enorme
parecido en su sintaxis y características. En este aspecto, es importante señalar que C#
incorpora muchos elementos de los que Java carece (sistema de tipos homogéneo,
propiedades, indexadores, tablas multidimensionales, operadores redefinibles, etc.) y
que según los benchmarks realizados la velocidad de ejecución del código escrito en
C# es ligeramente superior a su respectiva versión en Java
2
evitan frecuentes errores de programación y se consigue que los programas no
puedan poner en peligro la integridad de otras aplicaciones.
• Tiene a su disposición un recolector de basura que libera al programador de la
tarea de tener que eliminar las referencias a objetos que dejen de ser útiles,
encargándose de ello éste y evitándose así que se agote la memoria porque al
programador olvide liberar objetos inútiles o que se produzcan errores porque
el programador libere áreas de memoria ya liberadas y reasignadas.
• Incluye soporte nativo para eventos y delegados. Los delegados son similares
a los punteros a funciones de otros lenguajes como C++ aunque más cercanos a
la orientación a objetos, y los eventos son mecanismos mediante los cuales los
objetos pueden notificar de la ocurrencia de sucesos. Los eventos suelen usarse
en combinación con los delegados para el diseño de interfaces gráficas de
usuario, con lo que se proporciona al programador un mecanismo cómodo para
escribir códigos de respuesta a los diferentes eventos que puedan surgir a lo
largo de la ejecución de la aplicación. (pulsación de un botón, modificación de
un texto, etc.)
• Incorpora propiedades, que son un mecanismo que permite el acceso
controlado a miembros de una clase tal y como si de campos públicos se
tratasen. Gracias a ellas se evita la pérdida de legibilidad que en otros lenguajes
causa la utilización de métodos Set() y Get() pero se mantienen todas las ventajas
de un acceso controlado por estos proporcionada.
• Permite la definición del significado de los operadores básicos del lenguaje
(+, -, *, &, ==, etc.) para nuestros propios tipos de datos, lo que facilita
enormemente tanto la legibilidad de las aplicaciones como el esfuerzo necesario
para escribirlas. Es más, se puede incluso definir el significado del operador []
en cualquier clase, lo que permite acceder a sus objetos tal y como si fuesen
tablas. A la definición de éste último operador se le denomina indexador, y es
especialmente útil a la hora de escribir o trabajar con colecciones de objetos.
• Admite unos elementos llamados atributos que no son miembros de las clases
sino información sobre éstas que podemos incluir en su declaración. Por
ejemplo, indican si un miembro de una clase ha de aparecer en la ventana de
propiedades de Visual Studio.NET, cuáles son los valores admitidos para cada
miembro en ésta, etc.
COMPILACIÓN DE UN PROGRAMA EN C#
Para compilar en C# hay que ejecutar csc.exe con una serie de parámetros y el nombre
del archivo del código fuente:
/doc
Indica que procese los comentarios del código fuente en un archivo XML
/lib
Permite especificar la localización de los ensamblados referenciados por la opción
/reference
3
/out:
Permite especificar un nombre para el archivo de salida. Cuando se omite el nombre
del fichero de salida, coincide con el del archivo fuente
/r[eference]:
Opción para que una aplicación haga uso de los tipos definidos en un ensamblado
(librería) separado. Inmediatamente a continuación de los dos puntos, sin dejar espacio
en blanco, debe especificarse el ensamblado que se desea usar, por ejemplo:
/reference: System.Windows.Forms.dll
mscorblib.dll es un ensamblado automáticamente referenciado en el proceso de
compilación
/t[arget]:exe
Opción para construir una aplicación .EXE de consola como las aplicaciones DOS. Es
la opción por omisión.
/t[arget]:library
Permite construir un ensamblado DLL con un manifiesto relacionado
/t[arget]:module
Permite construir un módulo, sin manifiesto relacionado. Cuando un archivo no tiene
manifiesto relacionado, es decir, no es un ensamblado, no puede ser cargado por el
CLR. Estos archivos pueden incorporarse a un ensamblado mediante la opción
/addmodule
/t[arget]:winexe
Permite construir aplicaciones Windows
/addmodule
Incorpora módulos, cuya extensión por defecto es net-module, a un ensamblado. Los
módulos deben encontrarse en el mismo directorio que el archivo de salida. Por
ejemplo:
csc /addmodule:m1.netmodule;m2.netmodule /out:s.exe e.cs
/unsafe
Permite compilar código no seguro
ESTRUCTURA DE UN PROGRAMA EN C#
using System;
class Hola
{
public static int Main( )
{
Console.WriteLine("Hola, Mundo");
return 0;
}
}
4
Si se almacena este código en un fichero de texto plano de nombre HolaMundo.cs es
posible compilarlo abriendo una ventana de consola (MS-DOS) y, tras colocarse en el
directorio donde se haya almacenado el fichero, ejecutando:
csc HolaMundo1.cs
csc es el nombre del compilador de C#, y la orden anterior simplemente indica que
deseamos compilar el fichero de código fuente HolaMundo.cs, y tras ejecutarla el
compilador generará un fichero de nombre HolaMundo.exe que contendrá el
ejecutable de nuestra sencilla aplicación de ejemplo.
Diferencia con Java: El nombre de la clase no tiene por qué ser el mismo que el del
archivo
static indica que es un método estático; es decir, asociado a la clase dentro de la que se
define y no a los objetos que se creen a partir de la misma, por lo que para acceder a él
se usará la sintaxis nombreClase.NombreMétodo(parámetros) –en nuestro caso
Hola.Main()- y no objeto.NombreMétodo(parámetros) como corresponde a los métodos
no estáticos.
public static int Main(String[] args)
El parámetro que puede tomar el método Main() almacena la lista de argumentos con
los que se llamó a la aplicación; y como se ve, en caso de que no vayamos a utilizarla
no es necesario especificarla en la declaración de Main(). El tipo de este parámetro es
String[], que significa que es una tabla de cadenas de texto (objetos String); y su
nombre, que es el que habrá de usarse dentro del código del método Main() para
hacerle referencia, puede ser cualquiera (en el ejemplo es args)
5
Nótese que también es necesario usar la opción /t: para indicar que deseamos crear un
ejecutable de ventanas, pues por defecto se crean ejecutables de consola.
La directiva using es la que indica con que namespace vamos a trabajar. Siempre debe
ponerse al comienzo del fichero, y podemos usar más de una.
Tanto Write como WriteLine son similares a printf, ya que podemos dar una cadena
de texto y una serie de parámetros adicionales:
Console.WriteLine("La suma de {0} y {1} is {2}", 100, 130,100+130);
Se puede especificar también dentro del parámetro cadena el ancho de los campos y si
estarán justificados a la izquierda o a la derecha:
Console.WriteLine("Justificado a la izquierda en un campo de ancho 10:{0,-
10}", 99);
Console.WriteLine("Justificado a la derecha en un campo de ancho 10:{0,10}",
99);
Sintaxis:
6
{N,M:CadenaFormato}
N: el número a dar formato
M: ancho de campo y justificación
CadenaFormato: Cuantos datos numéricos van a ser mostrados. Ver tabla
Items Significado
C Muestra el número como currency (decimal) usando los símbolos y
convenciones actuales
D Muestra el número como un entero decimal
E Muestra el número usando la notación exponencial (científica).
F Muestra el número como un valor de coma fija
G Muestra el número como coma fija o entero, dependiendo que formato se ajuste
más
N Muestra el número con comas incluídas
X Muestra el número en notación Hexadecimal.
Muestra lo siguiente:
Currency - $88.80 ($888.8000)
Entero - 00088
Exponencial - 8.888000E+002
Coma fija - 888.889
General - 888.8888
Numérico - 8,888,888.80
Hexadecimal – 0058
Read
Lee el siguiente carácter del teclado. Devuelve el entero –1 si no hay entrada. Si la hay,
devuelve un int con el ascii del carácter
ReadLine
7
string input = Console.ReadLine( );
Console.WriteLine("{0}", input);
Antes se comentó que es posible declarar el método Main() de modo que tome un
parámetro de tipo String[] que contenga los argumentos con los que se llamó a la
aplicación. Es decir, de una de estas dos formas:
Ahora, cuando se ejecute el programa hay que pasarle un argumento y en función del
valor que éste tome se mostrará un mensaje de bienvenida personalizado. Por ejemplo,
si se ejecuta el programa así:
HolaMundo José
¡Hola José!
8
TIPOS BÁSICOS
En C# los tipos básicos son tipos del mismo nivel que cualquier otro tipo del lenguaje.
Es decir, heredan de System.Object y pueden ser tratados como objetos de la misma
por cualquier rutina que espere un System.Object, lo que cual es muy útil para el
diseño de rutinas genéricas, que admitan parámetros de cualquier tipo. En realidad
todos los tipos básicos de C# son simples alias de tipos del espacio de nombres System,
como se recoge en la última columna de la tabla. Por ejemplo, sbyte es alias de
System.Sbyte y da igual usar una forma del mismo u otra.
Los operadores aritméticos son como en C, con la salvedad de que al operador resto
(%) lo podemos utilizar con valores de tipo float.
9
Si se quisiera convertir el valor de la propiedad text en una caja de texto, podemos usar
el metodo int.Parse de la siguiente forma:
int enterocaja=int.Parse(caja.text);
El método inverso por así decirlo es el ToString() que convierte a cadena cualquier tipo
numérico.
OPERADORES IS Y AS
El operador “is” nos permite verificar si un objeto es compatible con un tipo específico.
En el caso que comentábamos en el párrafo anterior:
Con “as”, simplemente chequea una sola vez, al hacer la conversión, con lo cual, su
rendimiento es mayor.
10
VIDA DE UNA VARIABLE
El tiempo de vida de una variable está limitado al espacio de tiempo durante el cual
se ejecuta el método que la contiene. Si la variable está definida como miembro de una
clase, cada vez que se inicializa un objeto de esa clase, se inicia la vida de esa variable,
permaneciendo accesible para todos los métodos de esa clase hasta que el objeto sea
destruido.
Con el modificador static podemos variar el tiempo de vida de una clase. Supongamos
que tenemos una variable Entero local a una clase, si la definimos como static, la
variable existe desde que se inicia hasta que se termina el programa, mientras dicha
variable es compartida por todos los objetos de esa clase. Vamos a ver un ejemplo de
esto. En el siguiente programa, además del método Main(), contaremos con otro
llamado MuestraVariables(). La clase cuenta con dos variables del mismo tipo. Estas
dos variables tienen un tiempo de vida muy diferente. La primera la hemos declarado
sin modificador alguno, es decir, como automática (véase C++), mientras que la
segunda por el contrario, es estática, de forma que existe durante toda la ejecución del
programa.
using System;
namespace VarEstaticas
{
class Class1
{
// una variable normal
byte Valor1;
// y otra estática
static byte Valor2;
// Iniciamos la ejecución
[STAThread]
static void Main(string[] args)
{
byte Contador;
// Invocamos 10 veces el método
for(Contador=1;Contador<=10;Contador++)
MuestraVariables();
}
// Método con dos variables
static void MuestraVariables()
{
// Creamos un objeto Class1
Class1 MiObjeto=new Class1();
// Efectuamos la misma operacion en ambas variables
MiObjeto.Valor1++;
Class1.Valor2++;
// y mostramos su contenido
Console.WriteLine("Valor={0},Valor2={1}",MiObjeto.Valor1,Class1.Valor
2);
}
}
}
11
Al ejecutar el programa obtendremos un resultado como este:
Valor=1,Valor2=1
Valor=1,Valor2=2
Valor=1,Valor2=3
Valor=1,Valor2=4
Valor=1,Valor2=5
Valor=1,Valor2=6
Valor=1,Valor2=7
Valor=1,Valor2=8
Valor=1,Valor2=9
Valor=1,Valor2=10
Las variables declaradas en el interior de una clase pueden tener un tiempo de vida
asociado a los objetos creados a partir de ella, según se ha descrito anteriormente, o
bien una vida global asociada a la propia clase. Aquí vemos que para acceder a Valor1
es necesario contar con un objeto de la clase ya que dicha variable no existe de forma
aislada. Valor2, por el contrario, existe desde el principio, sin necesidad de crear objeto
alguno, y por ello se puede hacer referencia a ella usando el nombre de la propia clase.
ENUMERACIONES
Ejemplo:
DedoAfectado=Dedo.Corazon
ConsoleWriteLine((int) DedoAfectado) // Muestra el valor 2
Si, partiendo de la enumeración del punto anterior declaramos una variable de tipo
Dedo y le asignamos un valor numérico entre 0 y 4, el compilador generará un error.
No se efectúa una conversión implícita, automática para convertir el entero en la
constante que le corresponde. C# es un lenguaje con comprobación estricta de tipos.
12
Para poder asignar un valor entero a una variable de tipo Dedo, por tanto es necesario
emplear una conversión explícita:
DedoAfectado=(Dedo) 3;
ESTRUCTURAS
struct Libro
{
public string Titulo;
public string ISBN;
public string Autor;
public float Precio;
}
Libro UnLibro;
UnLibro.Titulo="Guia práctica Visual Basic .NET";
UnLibro.Autor="Francisco Charte";
UnLibro.ISBN="84-415-1290-6";
UnLibro.Precio=31.75F;
Podemos asignar una variable Libro a otra del mismo tipo, es decir, hacer una copia:
Libro UnLibro,OtroLibro;
UnLibro.Titulo="Guia práctica Visual Basic .NET";
UnLibro.Autor="Francisco Charte";
UnLibro.ISBN="84-415-1290-6";
UnLibro.Precio=31.75F;
OtroLibro=UnLibro;
Todas las estructuras cuentan con un método llamado Equals() que facilita la
comprobación de igualdad entre estructuras del mismo tipo. Esta sentencia mostrará
por la consola true o false dependiendo de que UnLibro y OtroLibro sean iguales o no.
Una de las diferencias de las estructuras de C# con respecto a C,C++ es que los
miembros no solo cuentan con un identificador y un tipo sino que además pueden
precederse de un modificador que indique su visibilidad. En Libro, todos los
miembros son public. Podemos omitir public, ya que es el modificador por defecto en
las struct.
Las otras alternativas son private e internal. Con internal tenemos una combinación
entre public y private, ya que los miembros de la estructura serían públicos para el
13
código del mismo ensamblado en el que se ha definido la estructura, pero privados
para otros ensamblados.
struct Libro
{
// Constructor para inicializar contenido
public Libro(string T, string I, string A, float P)
{
Titulo=T;
ISBN=I;
Autor=A;
Precio=P;
}
// Método para mostrar el contenido
public void Muestra()
{
Console.WriteLine("{0}, {1}, {2} , {3}", Titulo, ISBN, Autor,
Precio);
}
private string Titulo;
private string ISBN;
private string Autor;
private float Precio;
}
Las estructuras se pueden usar al trabajar con bases de datos, ya que almacenan los
registros completes de una tabla en una estructura con la misma definición que el
registro.
ARGUMENTOS IMPLÍCITOS
C# no permite, a diferencia de C++, definir argumentos con valor por defecto en una
función. Se debe resolver con sobrecarga.
14
INSTRUCCIONES ITERATIVAS
15
CLASES DE ENTRADA/SALIDA
16
Las clases Directory y DirectoryInfo
Con los métodos de la clase directory podemos hacer operaciones sobre directorios de
nuestros discos duros: crear, eliminar, mover, obtener ficheros y subdirectorios, saber
fechas de creación, modificación y último acceso, incluso para averiguar el directorio
raíz y el directorio actual.
Como podemos ver, no creamos ningún objeto de la clase Directory, ya que ésta es
estática. Por esta razón, para utilizar sus métodos, tenemos que indicar el directorio
sobre el que actuamos.
17
Si queremos usar un directorio en particular, podemos usar la clase DirectoryInfo la
cual contiene prácticamente los mismos métodos que Directory, pero antes de
utilizarla debemos crear un objeto en memoria. Éste se crea indicando el directorio que
queremos manipular. Veamos un ejemplo parecido al anterior:
if (dirinfo.Exists)
{
dirinfo.Delete();
Console.WriteLine("Borrado el directorio de prueba");
}
dirinfo = new DirectoryInfo(sdir + @"\prueba2");
if (dirinfo.Exists)
dirinfo.Delete();
else
Console.WriteLine("No existe el directorio {0}",
dirinfo.Name);
Console.ReadLine();
}
18
USANDO FLUJOS DE ENTRADA Y SALIDA BINARIOS
using System;
/* Necesitamos importar System.IO para poder usar ficheros
* y clases de lectura y escritura */
using System.IO;
void Main()
{
try {
// Generamos un FileStream para crear el fichero
// Lo abrimos para acceso de lectura y escritura
FileStream ds = new FileStream("..\test.dat",
FileMode.Create, FileAccess.ReadWrite);
// Lo encapsulamos en un BinaryWriter
BinaryWriter bw = new BinaryWriter(ds);
// Escribimos algunos datos
bw.Write("Una cadena");
bw.Write(142);
bw.Write(97,4);
bw.Write(true);
// Lo abrimos para lectura
BinaryReader br = new BinaryReader(ds);
// Volvemos al principio
br.BaseStream.Seek(0, SeekOrigin.Begin);
// Leemos los datos
Console.WriteLine(br.ReadString());
Console.WriteLine(br.ReadInt32());
Console.WriteLine(br.ReadDouble());
Console.WriteLine(br.ReadBoolean());
} catch (Exception e) {
Console.WriteLine("Excepcion:" + e.ToString());
}
}
FileStream lee y escribe bytes, lo cual es raramente conveniente, por lo que FileStream
se encapsula normalmente en otra clase que gestiona la conversión de bytes a bytes.
En este caso, usamos un BinaryWriter que toma datos primitivos de .Net y los
convierte en bytes. Estos bytes pueden ser luego pasados al FileStream.
19
BinaryWriter tiene una gran cantidad de métodos Write() sobrecargados, uno para
cada uno de los tipos primitivos. En este ejemplo, podemos ver cuatro de ellos,
escribiendo la salida como un string, un entero, un valor de punto flotante y un
booleano. Si se desea terminar el programa en este punto, debemos insertar un código
similar al siguiente después de la llamada a Write().
bw.Flush();
bw.Close();
Estas dos llamadas causan la escritura de cualquier dato que no haya sido escrito y la
llamada a Close() cerraría el flujo y el fichero. Ahora podemos usar un BinaryReader
para leer estos datos. Antes de poder usarlo hay que volver al principio del fichero.
Cuando usamos un flujo, el puntero de búsqueda marca el punto en el que tendrá
lugar la siguiente operación de lectura y escritura. Se ha estado escribiendo en el flujo,
por lo que el puntero está ahora al final del fichero preparado para hacer la siguiente
operación de escritura. Si deseamos leer algo que está en una posición anterior al
fichero, debemos reposicionar el puntero. La propiedad BaseStream obtiene una
referencia al FileStream dentro del BinaryReader y después llama al método Seek()
para cambiar la posición del FileStream. Seek() toma dos parámetros, un
desplazamiento en bytes y una posición a partir de la cual aplicar el offset. Aquí hemos
usado SeekOrigin.Begin, la cual denota el comienzo del fichero. Otros valores posibles
son SeekOrigin.End (fin de fichero) y SeekOrigin.Current (posición actual). Podemos
usar desplazamientos negativos así como positivos, por lo cual podemos
posicionarnos de manera relativa al final del fichero.
El compilador en este caso decide qué tipo usar en cada caso, de forma que el código
anterior es exactamente igual a:
Ésto supone una gran ayuda a la hora de usar la sintaxis integrada para consultas
(LINQ).
20
ARRAY IMPLÍCITAMENTE TIPADOS (C# 3.0)
Esta caracerística nos permite declarar arrays sin tener que especificar su tipo, por lo
que expresiones como:
Propiedades
Las propiedades proporcionan un modo de leer, o asignar valores a campos
privados a través de los métodos especiales de acceso de la propiedad, denominados
get y set; get utiliza la palabra reservada return para devolver el contenido del campo,
mientras que set utiliza la palabra reservada value que representa el valor a asignar al
campo. Veamos un ejemplo:
class Empleado
{
private string telefono; // campo privado telefono
//...
public string Telefono // propiedad Telefono
{
get
{
return telefono;
}
set
{
telefono=value; // value es el valor que se va a asignar
}
}
}
class usaEmpleado
{
/* Aqui va alojado el metodo Main */
public static void Main()
{
/* Llamada al constructor por defecto para crear un objeto
o instancia de la clase Empleado */
Empleado unEmpleado = new Empleado();
/* referencia a un miembro de la clase empleado */
unEmpleado.Telefono="91448723";
System.Console.WriteLine(unEmpleado.Telefono);
}
}
21
PARÁMETROS REF Y OUT
Puede que no sea siempre conveniente tener que llamar a dos funciones miembros
para obtener los valores, ya que sería mejor obtener ambos valores en una única
llamada. Sin embargo, en un método, sólo hay un valor de retorno.
Una solución es usar los parámetros de referencia (ref), de forma que los valores de los
parámetros pasados a la función miembro se puedan modificar:
using System;
class Punto
{
public Punto(int x, int y)
{
this.x=x;
this.y=y;
}
// obtiene ambos valores en una sola llamada
public void ObtenerPunto (ref int x,ref int y)
{
x=this.x;
y=this.y;
}
int x;
int y;
}
class Prueba
{
public static void Main()
{
Punto miPunto = new Punto(10,15);
int x;
int y;
// ilegal
miPunto.ObtenerPunto(ref x, ref y);
Console.WriteLine("miPunto({0},{1}",x,y);
}
}
En este código, los parámetros se han declarado usando la palabra clave ref, tal como
se observa en la llamada a la función. Este código debería funcionar, pero cuando se
compila, genera un mensaje de error que indica que se han usado valores sin inicializar
para los parámetros ref x e y. Esto significa que las variables han sido pasadas a la
función antes de la asignación de sus valores y el compilador no permitirá exponer
valores sin inicializar.
22
Una alternativa a esto es inicializar las variables cuando se declaran:
using System;
class Punto
{
public Punto(int x, int y)
{
this.x=x;
this.y=y;
}
// obtiene ambos valores en una sola llamada
public void ObtenerPunto (ref int x,ref int y)
{
x=this.x;
y=this.y;
}
int x;
int y;
}
class Prueba
{
public static void Main()
{
Punto miPunto = new Punto(10,15);
int x=0;
int y=0;
miPunto.ObtenerPunto(ref x, ref y);
Console.WriteLine("miPunto({0},{1}",x,y);
Console.ReadLine();
}
23
Ahora el programa compila correctamente, pero a costa de inicializar las variables a
cero previamente. Otra opción sería usar el parámetro out en vez de ref:
class Punto
{
public Punto(int x, int y)
{
this.x=x;
this.y=y;
}
// obtiene ambos valores en una sola llamada
public void ObtenerPunto (out int x,out int y)
{
x=this.x;
y=this.y;
}
int x;
int y;
}
class Prueba
{
public static void Main()
{
int x,y;
Punto miPunto = new Punto(10,15);
miPunto.ObtenerPunto(out x, out y);
Console.WriteLine("miPunto({0},{1})",x,y);
Console.ReadLine();
}
Los parámetros out son exactamente igual que los ref solo que se pueden pasar
variables sin inicializar y la llamada se realiza con out en lugar de ref.
Indexadores
24
Tipos anidados
Las declaraciones de tipos se pueden efectuar de forma anidada, unas dentro de otras,
para controlar su accesibilidad.
Eventos
Los eventos o sucesos se utilizan para mostrar los cambios asincrónicos de un objeto
observado. Los clientes permanecen atentos a los eventos y cuando se produce uno, se
invoca a una llamada a un método. La programación con Interfaces Gráficos de
Usuario (GUI) es una programación dirigida por eventos, ya que los componentes de
la GUI (botones, campos de texto, etc.) esperan a que el usuario ejecute alguna acción
sobre ellos, es decir, un evento. Así el espacio de nombres System.Windows.Forms
ofrece objetos visuales sobre los cuales pueden actuar los usuarios y de esta forma
comunicarse con un programa y, para ello define clases que cuentan con eventos como:
Métodos
SOBRECARGA
A veces puede resultar útil tener dos funciones que hagan la misma cosa, pero con
diferentes parámetros. Esto es especialmente común en los constructores de clase,
donde pueden existir varias formas de crear un nuevo ejemplar.
class Punto
{
// Crea un nuevo punto con los valores x e y
public Punto(int x, int y)
{
this.x=x;
this.y=y;
}
// Crea un nuevo punto desde un punto existente
public Punto(Punto p)
{
this.x=p.x;
this.y=p.y;
}
int x;
int y;
}
class Prueba
{
public static void Main()
{
Punto miPunto = new Punto(10,15);
Punto miSegundoPunto=new Punto(miPunto);
}
}
25
Esta clase tiene dos constructores; uno puede ser llamado con los valores x e y, y el
otro se puede llamar con el valor de un punto. La función Main() usa ambos; uno para
crear un ejemplar a partir de un valor de x e y, y el otro para crear un ejemplar desde
otro existente (clase punto).
SOBRECARGA DE OPERADORES
La idea es poder definir el comportamiento de un operador para una clase creada por
nosotros. Esto implica que un mismo operador puede comportarse de distinta forma
según que objeto esté implicado. Es importante reseñar que con la sobrecarga no se
sobrescribe el significado del operador, es decir, podemos seguir sumando números
con “+” aunque hayamos sobrecargado el operador. Utilizaremos la palabra clave
operador para crear el método que redefinirá al operador y este método puede recibir
un parámetro o dos, dependiendo de si se trata de un operador unario o binario. La
sintaxis es:
El tipo devuelto es un tipo cuyo objeto debe haber sido creado por nosotros y actuará
para esa clase. Si hablamos de un operador unario, el parámetro debe ser también un
objeto de esa clase (tipo de datos), y si se trata de binario, al menos uno de los
parámetros debe serlo. En operador pondremos el que estamos sobrecargando y en el
cuerpo del método las operaciones que realizará dicho operador.
Veamos un ejemplo:
class Punto
{
int x, y;
public Punto()
{
x = y = 0;
}
public Punto(int a, int b)
{
x = a;
y = b;
}
}
26
mediante override). El segundo método es el método operador de “+”. Primero
creamos un objeto de tipo Punto y después sumamos las coordenadas de nuestro
punto para obtener el resultado de la suma.
class Punto
{
int x, y;
public Punto()
{
x = y = 0;
}
public Punto(int a, int b)
{
x = a;
y = b;
}
public override string ToString()
{
return "(" + x + "," + y + ")";
}
public static Punto operator +(Punto pto1, Punto pto2)
{
Punto pto = new Punto();
pto.x = pto1.x + pto2.x;
pto.y = pto1.y + pto2.y;
return pto;
}
public static Punto operator +(Punto pto1, int valor)
{
Punto pto = new Punto();
pto.x = pto1.x + valor;
pto.y = pto1.y + valor;
return pto;
}
}
class Sobrecarga
{
static void Main()
{
Punto p1 = new Punto();
Punto p2 = new Punto(2, 5);
Punto p3 = new Punto(1, 3);
Puntualizar que con esto ocurre como con la suma ordinaria, no modificamos ninguno
de los dos operandos ya que devolvemos un nuevo objeto con las coordenadas
resultantes. Sin embargo, cuando sobrecargamos un operador podemos hacer que se
modifique un operando y devolvamos los resultados en él. En este caso no vamos a
necesitar usar ni ref ni out porque los objetos se pasan por referencia.
27
Para sobrecargar un operador unario procederemos de la misma forma, pero
proporcionando un solo parámetro al método de operador. La sintaxis es la misma,
pero hay alguna matización a nivel de funcionamiento. Debido a que sólo pasamos un
parámetro, no existe el problema del orden de los mismos, cuestión que si era
importante al sobrecargar un operador binario (supongamos una sobrecarga de una
resta). Además, en el caso de los operadores unarios del tipo ++ o – se da el caso de
que debemos modificar el valor del parámetro ya que el sentido de un incremento o
decremento unario es modificar el valor original.
En el listado anterior también podemos ver como la clase Punto tiene sobrecargada la
suma de forma que uno de los operandos no es del tipo de la clase Punto. Aquí vuelve
a ser importante el orden de los operandos. Si queremos que se acepte, al contrario,
debemos volver a sobrecargar el operador para que tenga otra firma en la cual el
primer parámetro sea un int y el segundo un punto. Una vez codificado vamos a poder
hacer p1 + 10, o 10 + p1.
De esta forma, si las dos coordenadas del punto son cero, consideramos que no es true,
y en caso contrario consideramos true al punto. Análogamente definiríamos la
sobrecarga de false.
De esta forma podemos usar objetos Punto en expresiones condicionales del tipo if
(objeto) o incluso en bucles do…while (objeto).
28
También podemos sobrecargar operadores lógicos. Reseñar que solo podemos hacerlo
con AND (&), OR(|) y NOT (!). Cláramente hemos tenido que haber sobrecargado true
y false antes de esta sobrecarga, ya que es necesario saber si un objeto es true o false
para poder usar los operadores lógicos con él.
Para finalizar, reseñar que también podemos sobrecargar los operadores && y || pero
previamente hemos de haber sobrecargado & y |, pero con la condición de que ambos
devuelvan como tipo objetos de la clase y no tipos booleanos (true/false). Esta
particularidad permitirá que la sobrecarga de || y && esté automáticamente creada.
Para eso, tenemos que tener claro cómo codificar la sobrecarga de & y | de forma
correcta. La solución es sencilla: ¿qué pasaría si en el método de operador de &
devolvemos en lugar de true un nuevo objeto Punto?:
Realmente podríamos devolver cualquier objeto que fuera true, es decir, con alguna
coordenada distinta de cero. Para el caso de false, devolveríamos un objeto Punto(0,0),
un objeto false. Por supuesto, debemos también cambiar el tipo devuelto en la
definición del método de operador. De alguna manera, estamos devolviendo un objeto
true en vez de devolver true y un objeto false en vez de un false. De esta forma, al estar
sobrecargados también los operadores true y false, la expresión p1 & p2 se evalúa
correctamente. Así, hemos implementado implícitamente la sobrecarga de && y ||,
ya que una expresión del tipo p1 && p2 se evaluará mediante el operador false para
cada uno de los operandos. Si se puede determinar el resultado de la expresión (p1 es
false) se devuelve el resultado pero si no se puede, se usa el operador & que tenemos
ya sobrecargado. En el caso de ||se usa el operador true para determinar si
acudiremos al operador | sobrecargado o ya podemos determinar el resultado de la
expresión (si p1 es true, ya no hace falta seguir).
Nos permiten añadir métodos a los tipos sobre los que no tenemos control, como por
ejemplo, tipos ya existentes en la biblioteca de clases. Esto se consigue declarando un
método estático dentro de una clase estática que haga referencia mediante this al tipo
que se va a extender. En el siguiente ejemplo, vamos a declarar una clase que contiene
29
el método TieneEspacios que devolverá true si la palabra contiene al menos un espacio
y falso en caso contrario:
El primer parámetro del método va precedido de la palabra this. Esto significa que
hará referencia al tipo que va a extender. En todas las extensiones de métodos el primer
parámetro hace referencia al tipo que se extiende, posteriormente vendrían los
parámetros propios del método.
Esta sintaxis funciona tanto para campos públicos como para propiedades.
De la misma forma podemos inicializar una colección. Supongamos una lista genérica:
30
Consideremos el siguiente ejemplo en C# 2.0:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace Colecciones
{
public class Autores
{
string autor;
List<string> libros = new List<string>();
public string Autor {
get { return autor; }
set { autor = value; }
}
public List<string> Libros { get { return libros; } }
}
class MyClass
{
static void Main(string[] args)
{
List<Autores> listaAutores = new List<Autores>();
Autores c1 = new Autores();
c1.Autor = "Dylan Combel";
c1.Libros.Add("ISBN: 555-62280-58");
c1.Libros.Add("ISBN: 555-71180-59");
listaAutores.Add(c1);
Autores c2 = new Autores();
c2.Autor = "Isabella Abolrous";
c2.Libros.Add("ISBN: 555-72390-88");
c2.Libros.Add("ISBN: 555-65412-77");
listaAutores.Add(c2);
Console.WriteLine(c1.Autor + ":\n" + c1.Libros[0] + "\n"
+ c1.Libros[1]);
Console.WriteLine(c2.Autor + ":\n" + c2.Libros[0] + "\n"
+ c2.Libros[1]);
Console.ReadKey(true);
}
}
}
Dylan Combel:
ISBN: 555-62280-58
ISBN: 555-71180-59
Isabella Abolrous:
ISBN: 555-72390-88
ISBN: 555-65412-77
31
TIPOS ANÓNIMOS (C# 3.0)
Permiten crear tipos a partir de otros ya existentes. Se trata de usar new junto a un
inicializador para crear un tipo anónimo:
Esto crea una variable p1 de tipo anónimo con dos campos, uno de tipo string y otro
int. Si quisiéramos crear esto en versiones anteriores de C# tendríamos que recurrir a
una clase con dos campos, crear su constructor y asignarle los valores que
correspondan. De esta forma, hacemos algo similar con una sola línea. Reseñar que los
miembros de este tipo anónimo son de sólo lectura.
Veamos la siguiente clase, la cual implementa un objeto Ingeniero y los métodos para
gestionar la facturación de ese ingeniero.
using System;
class Ingeniero
{
public Ingeniero(string nombre, float tarifa)
{
this.nombre=nombre;
this.tarifa=tarifa;
}
// calcula el cargo basado en la tarifa del ingeniero
public float CalculaCargo(float horas)
{
return (horas*tarifa);
}
public string NombreTipo()
{
return ("Ingeniero");
}
private string nombre;
protected float tarifa;
}
class Prueba
{
public static void Main()
{
Ingeniero ingeniero=new Ingeniero("Hank",21.20F);
Console.WriteLine("El nombre es: {0}",ingeniero.NombreTipo());
Console.ReadLine();
}
}
La clase Ingeniero servirá como clase base para este caso. Contiene el campo privado
nombre y el protegido tarifa. El modificador protected garantiza el acceso como
privado, salvo las clases que están derivadas de esta misma, que también tienen acceso
al campo. Por lo tanto, protected se utiliza para dar acceso a un campo a las clases
32
derivadas de una determinada clase, mientras que, para el resto de las clases, ese
campo está protegido
HERENCIA SIMPLE
class IngenieroCivil:Ingeniero
{
public IngenieroCivil(string nombre, float
tarifa):base(nombre,tarifa){}
//nueva función, porque difiere de la clase base
public new float CalculaCargo(float horas)
{
if (horas< 1.0F) horas=1.0F;
return (horas*tarifa);
}
public new string NombreTipo()
{
return ("Ingeniero Civil");
}
}
class Prueba
{
public static void Main()
{
Ingeniero i=new Ingeniero("Jorge",15.50F);
IngenieroCivil c = new IngenieroCivil("Juan",40F);
Console.WriteLine("El cargo de {0} es
{1}",i.NombreTipo(),i.CalculaCargo(2F));
Console.WriteLine("El cargo de {0} es
{1}",c.NombreTipo(),c.CalculaCargo(0.75F));
Console.ReadLine();
}
}
Los constructores no se heredan, por lo tanto debemos escribir uno específico para la
clase IngenieroCivil. El constructor no tiene nada en especial para hacer, por lo tanto,
simplemente llama al constructor de la clase Ingeniero usando la sintaxis base. Si la
llamada al constructor de la clase base fuera omitida, el compilador haría una llamada
al constructor de la clase base sin parámetros.
La clase IngenieroCivil tiene una forma distinta de calcular los cargos; el cargo mínimo
es de una hora, por lo cual existe una nueva versión CalcularCargo()
33
Métodos virtuales
A este tipo de métodos se les llama métodos virtuales, y la sintaxis que se usa para
definirlos es la siguiente:
Si en alguna clase hija quisiésemos dar una nueva definición del <código> del método,
simplemente lo volveríamos a definir en la misma pero sustituyendo en su definición
la palabra reservada virtual por override. Es decir, usaríamos esta sintaxis:
Nótese que esta posibilidad de cambiar el código de un método en su clase hija sólo se
da si en la clase padre el método fue definido como virtual. En caso contrario, el
compilador considerará un error intentar redefinirlo.
Por otro lado, cuando definamos un método como override ha de cumplirse que en
alguna clase antecesora (su clase padre, su clase abuela, etc.) de la clase en la que se ha
realizado la definición del mismo exista un método virtual con el mismo nombre que
el redefinido. Si no, el compilador informará de error por intento de redefinición de
método no existente o no virtual. Así se evita que por accidente un programador crea
que está redefiniendo un método del que no exista definición previa o que redefina un
método que el creador de la clase base no desee que se pueda redefinir.
34
Para aclarar mejor el concepto de método virtual, veamos un ejemplo:
using System;
public class Persona
{
public string Nombre; // Campo de cada objeto Persona que almacena
su nombre
public int Edad; // Campo de cada objeto Persona que
almacena su edad
public string NIF; // Campo de cada objeto Persona que
almacena su NIF
public virtual void Cumpleaños() // Incrementa en uno de la edad del
objeto Persona
{
Edad++;
Console.WriteLine(“Incrementada edad de persona”);
}
35
Nótese cómo se ha añadido el modificador virtual en la definición de Cumpleanyos()
en la clase Persona para habilitar la posibilidad de que dicho método puede ser
redefinido en clase hijas de Persona y cómo se ha añado override en la redefinición del
mismo dentro de la clase Trabajador para indicar que la nueva definición del método
es una redefinición del heredado de la clase. La salida de este programa confirma que
la implementación de Cumpleaños() es distinta en cada clase, pues es de la forma:
También es importante señalar que para que la redefinición sea válida ha sido
necesario añadir la partícula public a la definición del método original, pues si no se
incluyese se consideraría que el método sólo es accesible desde dentro de la clase
donde se ha definido, lo que no tiene sentido en métodos virtuales ya que entonces
nunca podría ser redefinido. De hecho, si se excluyese el modificador public el
compilador informaría de un error ante este absurdo. Además, este modificador
también se ha mantenido en la redefinición de Cumpleaños() porque toda redefinición
de un método virtual ha de mantener los mismos modificadores de acceso que el
método original para ser válida.
INTERFACES
Una interfaz es una construcción lógica que define los métodos que implementa una
clase, pero sin desarrollar dicha implementación. Con esto podemos separar la
definición de una clase de su implementación. Una vez escrito la interfaz, puede ser
implementado por varias clases al mismo tiempo y de distinta forma. Igualmente, una
clase puede implementar varios interfaces. Si varias clases implementan una interfaz,
cada una puede hacerlo de distinta forma, pero contendrán el mismo grupo de
métodos a pesar de que cada una puede añadir los suyos propios.
Toda clase que incluya una interfaz debe implementar todos sus métodos y estos son
public por defecto. No se puede incluir el modificador de acceso a ellos. Además, en
una interfaz se pueden definir las firmas de propiedades, etc… y no se pueden definir
ni constructores ni destructores.
Una vez definido la interfaz, en la clase se implementa igual que con la herencia, es
decir, detrás del nombre de la clase, dos puntos, y el nombre de la interfaz.
36
class nombre_clase:nombre_interface{ }
Si en una clase deseamos implementar más de una interfaz, estas irán separados por
comas. Si además la clase hereda de otra, la clase base tendrá que ser la primera de la
lista. El hecho de que una clase implemente métodos de un interfaz no implica que no
pueda tener los suyos propios. En el siguiente método hemos definido el interfaz con
un solo método para luego escribir el código de nuestra clase implementando dicho
método más otro adicional. En este caso, hemos escrito el código en el mismo fichero,
pero lo normal es tener los interfaces en un fichero independiente, con lo cual hemos
de compilar los ficheros juntos para obtener el ejecutable:
using System;
public interface Irevista {
double coste_pagina();
}
public class revista:Irevista {
public string nombre;
public int paginas;
public double precio;
public double coste_pagina(){
return (this.precio/this.paginas);
}
public double pagina_por_euro(){
return (this.paginas/this.precio);
}
}
public class interfaces{
También podríamos haber declarado las variables nombre, páginas y precio como
propiedades dentro de la interfaz y lo hubiéramos hecho así:
string nombre {
get;
set;
}
37
Otra cuestión importante es que una interfaz puede heredar de otro y, como podemos
suponer, la sintaxis es la misma que con las clases. Lo importante es que si una clase
utiliza un interfaz, el cual hereda de otro, la clase debe implementar todos los métodos
de las interfaces. Tanto los de la interfaz que utiliza, como los de la interfaz de la cual
hereda del otro. Si no se implementan todos los métodos, obtendremos un error de
compilación.
Debido a la herencia y a que una clase puede implementar varios interfaces, puede
darse el caso de que una interfaz tenga definido un método que tenga exactamente la
misma firma que otro que se encuentre en otra interfaz, y los dos vayan a ser
implementados por la misma clase. Para estos casos se usa la implementación explícita,
la cual hace uso del nombre del interfaz junto con el del método para identificar
unívocamente dicha implementación. Es decir, en la implementación de un método M
de una interfaz Iprueba en la clase ejemplo, deberíamos poner:
Como norma general, usaremos las interfaces cuando podamos definir el “qué hacer”
sin necesidad de definir “el cómo lo hace”, en otro caso, usaremos clases abstractas.
Un ejemplo del uso de interfaces podría aplicarse a lo que vamos a ver a continuación:
cifrado de datos. Podríamos definir una interfaz con el método encripta() y con el
método desencripta() y desarrollarlos luego en la clase correspondiente, dependiendo
de qué algoritmo de cifrado estuviésemos hablando. Es lo que hemos dicho antes:
Definimos “lo que hace” y “el como” ya lo implementaremos según el tipo de cifrado.
Además, las diferencias fundamentales que nos permiten decidir entre una clase
abstracta y una interface son:
- La clase abstracta no obliga a que una clase derivada implemente todos sus
métodos abstractos. En una interfaz es obligatorio.
- Una clase solo puede derivar de una clase base (abstracta o no), y una interfaz
puede hacerlo de más de una interface. Una clase puede heredar de otra clase y
de varios interfaces al mismo tiempo.
Por último, comentar que las interfaces proporcionan una propiedad que se puede
consultar con el operador is. Si el resultado de la expresión:
if (instancia_clase is ImiInterface)
38
DELEGADOS Y EVENTOS
En .NET para definir el tipo de un evento, se define un delegado que no es más que
una declaración en la que se asigna un nombre a ese tipo y se le facilita la lista de
parámetros. Suponiendo que fuesemos a crear un componente que generase
periódicamente un evento, como si fuese un tic de reloj, podríamos definir el delegado
siguiente:
Esta clase Reloj, cuando quisiera generar el evento lo primero que tendría que hacer es
comprobar si Tick es nulo, o por el contrario, se le ha asignado algún valor. Es posible
que el usuario de la clase haya optado por no usar el evento, en cuyo caso no
deberíamos usar entonces Tick para realizar la llamada.
39
A continuación, se efectúa dicha asignación y se llama de nuevo a Inicio().
using System;
namespace Curso.Csharp.Eventos
{
// Definimos el tipo de evento
public delegate void OnTickReloj(int Ticks);
// Esta clase cuenta con un evento
// del tipo OnTickReloj
public class Reloj
{
public event OnTickReloj Tick;
// asignamos el gestor
MiReloj.Tick += new OnTickReloj(TickReloj);
Console.WriteLine(
"Después de asignar el gestor de evento");
Console.ReadLine(); // esperamos Intro
MiReloj.Inicio(); // y repetimos
}
// Al recibir el evento
void TickReloj(int Ticks)
{
// simplemente mostramos el valor
Console.WriteLine(Ticks);
}
40
MÉTODOS ANÓNIMOS
Se basan en la indicación “in situ” del código gestor del evento de forma que no sea
necesario definir un método explícitamente. El compilador es el que se encarga
mediante inferencia de tipos de deducir la firma del método y de crearlo por nosotros.
Por ejemplo, la programación click de un botón podría quedar así:
// constructores
public Empleado(string nombre, SexoPersona sexo, string empresa,
decimal salario)
: base(nombre, sexo)
{
this.Empresa = empresa;
this.salario = salario;
}
// propiedades
public string Empresa
{
get { return empresa; }
set
{
if (value == null)
throw new Exception("Empresa obligatoria");
else
empresa = value;
}
}
public decimal Salario
{
get { return salario; }
set
{
decimal anterior = salario;
salario = value;
if (salario > anterior &&
ExclamacionAlSubirSalario != null)
{
// disparar evento
41
ExclamacionAlSubirSalario(this, new
CambioSalarioEventArgs(anterior, salario));
}
}
}
// métodos
public override string ToString()
{
return base.ToString() + Environment.NewLine +
"Empresa: " + empresa + " Salario: " +
salario.ToString("#,##0.00");
}
// eventos
public event Exclamacion_CambioSalario
ExclamacionAlSubirSalario;
}
pepe.ExclamacionAlSubirSalario +=
delegate(object s2, CambioSalarioEventArgs e2)
{
MessageBox.Show(((Empleado)s2).Nombre + " está contento");
MessageBox.Show("Antes ganaba: " + e2.SalarioAntes.ToString("#,##0.00"));
MessageBox.Show("Ahora gana: " + e2.SalarioDespues.ToString("#,##0.00"));
};
42
private void Hilo(object sender, EventArgs e)
{
new Thread(
delegate(){
int i = 1;
while (true)
{
lbl.Invoke(
(MethodInvoker)delegate{
lbl.Text = i.ToString();
});
Thread.Sleep(1000);
i++;
}
}
).Start();
}
43
MÉTODOS ANÓNIMOS: ACCESO A VARIABLES DE ÁMBITOS EXTERNOS
Supongamos que el código cliente quiere sumar los salarios de los empleados que
reciben aumentos. En este caso podríamos declarar una variable local al método que
contiene al anónimo y usarla para acumular los salarios:
En general, a las funciones que son capaces de acceder a las variables del entorno que
las rodea se les conoce como clausuras (closures). Se trata de un recurso tradicional en
la programación funcional que aparecen por primera vez en un lenguaje OO con C#.
44
PATRÓN DE DISEÑO SINGLETON PARA MULTIPROCESO
using System;
static object sync = new object();
static Singleton singleton=null;
private Singleton(){}
using System;
static object sync = new object();
static volatile Singleton singleton=null;
private Singleton(){}
45
información relevante y fuercen las reglas de negocio sobre su formato. Por ejemplo,
en USA los números de teléfono están limitados a 10 dígitos y el código postal tiene
un formato particular.
Una vez escritas estas clases, puede ocurrir que necesitemos gestionar direcciones y
números de teléfono de otros paises, por ejemplo, Holanda, con sus diferentes reglas
de negocio.
Abstract Factory resuelve este problema. Usando este patrón definimos una clase
FabricaDirecciones (un framework genérico para generar objetos que siguen un patrón
general para Dirección y NumTelefono. En tiempo de ejecución esta fábrica se asocia
con un número concreto de fábricas para distintos paises y cada uno de ellos tiene su
propia versión de las clases Dirección y NumTelefono.
En vez de tener que pasar por el trance de añadir lógica funcional a las clases,
extendemos Direccion a DireccionEsp y NumTelefono a NumTelefonoEsp. Las
instancias de estas clases concretas se crean a partir de FabricaDireccionEsp. Esto nos
da mayor libertad a la hora de extender el código sin tener que realizar grandes
modificaciones a la estructura del resto del sistema.
Vamos a ver esto con un ejemplo. El código a continuación muestra como integrar las
direcciones y los números de teléfono internacionales en nuestro Gestor de
Información personal con el patrón Abstract Factory. La interfaz FabricaDirecciones
representa la fábrica en si misma.
46
FabricaDirecciones define dos métodos de creación que generan los productos
abstractos Direccion y NumTelefono, que definen los métodos que soportan esos
productos.
47
En este ejemplo, definimos Direccion y NumTelefono como clases abstractas, pero
podrían haberse definido como interfaces si no fuera necesario definir el código
utilizado por todos los productos concretos.
48
El framework genérico para FabricaDirecciones, Direccion y NumTelefono facilita la
extensión del sistema para dar cabida a paises adicionales. Para cada país adicional
habría que definir una clase fábrica concreta adicional y la clase producto concreto
correspondiente. Este sería el código:
public class FabricaDireccionesEs : FabricaDirecciones
{
public Direccion crearDireccion()
{
return new DireccionES();
}
public NumTelefono crearNumTelefono()
{
return new NumTelefonoES();
}
}
49
Normalmente, esta información se introduce por un usuario cuando se crea la cita por
lo que se define un constructor que permite establecer el estado del nuevo objeto Cita.
Para realizar esta tarea tenemos dos opciones, si bien ninguna de ellas es especialmente
atractiva. Podemos crear constructores para cada tipo de cita que deseemos tener, o
podemos escribir un constructor enorme con mucha lógica funcional. Cualquiera de
ellas tiene inconvenientes: con muchos constructores, la lógica de llamadas puede ser
compleja; con un solo constructor, pero mucha lógica funcional, el código se hace más
complejo y difícil de depurar. Ambas soluciones nos pueden dar problemas si hay que
crear subclases de Cita.
Una de las cosas que puede hacer este patrón es forzar la construcción por etapas de
un objeto complejo. Supongamos que estamos construyendo un pedido de compra.
Necesitaremos asegurarnos de que está en un estado concreto antes de construir el
método de compra, porque ese estado impactaría en los impuestos de venta aplicados
al producto.
50
- Product (producto): El objeto resultante. Podemos definir el producto como una
interfaz (es la mejor opción) o como una clase.
Vamos a ver como podemos utilizar este patrón para construir una cita para nuestra
aplicación. Veamos el propósito de cada clase:
- ConstructorCita, ConstructorReunión: clases Builder
- Calendario: clase Director
- Cita: clase Producto
- Dirección, Contacto: clases auxiliares utilizadas para guardar la información
relevante para Cita
- InformacionRequeridaExcepcion: clase Excepción producida cuando se
necesitan más datos
51
El código a continuación define la clase Cita:
public class Cita
{
public const string CR = "\n";
private DateTime fechaInicio, fechaFin;
private string descripcion, localizacion;
private ArrayList asistentes = new ArrayList();
public DateTime FechaInicio
{
get { return fechaInicio; }
set { fechaInicio = value; }
}
public DateTime FechaFin
{
get { return fechaFin; }
set { fechaFin = value; }
}
public string Descripcion
{
get { return descripcion; }
set { descripcion = value; }
}
public ArrayList Asistentes
{
get { return asistentes; }
set
{
if (asistentes != null)
asistentes = value;
}
}
public string Localizacion
{
get { return localizacion; }
set { localizacion = value; }
}
public void AgregarAsistente(Contacto asistente)
{
if (!asistentes.Contains(asistente))
asistentes.Add(asistente);
}
public void EliminaAsistente(Contacto asistente)
{
asistentes.Remove(asistente);
}
public override string ToString()
{
string strasistentes ="" ;
foreach (Contacto a in asistentes)
{
if (strasistentes == "")
strasistentes = a.ToString();
else
strasistentes += ", " + a.ToString();
}
return " Descripcion: " + descripcion + "\nFecha de comienzo: "
+ fechaInicio + "\nFecha fin: " + fechaFin + "\nLocalización: " +
localizacion + "\nAsistentes: " + strasistentes;
}
52
Cuando nos falte información, saltará la siguiente excepción:
53
Veamos la clase constructora de citas:
public class ConstructorCita
{
public const int FECHA_INICIO_REQUERIDA = 1;
public const int FECHA_FIN_REQUERIDA = 2;
public const int DESCRIPCION_REQUERIDA = 4;
public const int ASISTENTES_REQUERIDOS = 8;
public const int LOCALIZACION_REQUERIDA = 16;
protected Cita cita;
protected int elementosRequeridos;
public void construirCita(){
cita = new Cita();
}
public void construirFechas(DateTime fechaInicio, DateTime
fechaFin)
{
Boolean posterior;
DateTime fechaActual = new DateTime();
posterior = fechaInicio.CompareTo(fechaActual) > 0 ? true :
false;
if ((fechaInicio != null) && posterior)
cita.FechaInicio = fechaInicio;
posterior = fechaFin.CompareTo(fechaActual) > 0 ? true : false;
if ((fechaFin != null) && posterior)
cita.FechaFin = fechaFin;
}
public void construirDescripcion(string nuevaDescripcion)
{
cita.Descripcion = nuevaDescripcion;
}
public void construirAsistentes(ArrayList asistentes)
{
if (( asistentes !=null) && (asistentes.Count!=0))
{
cita.Asistentes=asistentes;
}
}
public void construirLocalizacion(string nuevaLocalizacion)
{
if (nuevaLocalizacion != "")
cita.Localizacion = nuevaLocalizacion;
}
public Cita obtenerCita()
{
elementosRequeridos = 0;
if (cita.FechaInicio.ToString()=="")
elementosRequeridos += FECHA_INICIO_REQUERIDA;
if (cita.Localizacion == null)
elementosRequeridos += LOCALIZACION_REQUERIDA;
if (cita.Asistentes.Count == 0)
elementosRequeridos += ASISTENTES_REQUERIDOS;
if (elementosRequeridos > 0)
throw new
InformacionRequeridaExcepcion(elementosRequeridos);
return cita;
}
public int ElementosRequeridos
{
get { return elementosRequeridos; }
}
}
54
La clase Calendario llama a ConstructorCita, gestionando el proceso de creación a
través del método crearCita
public class Calendario
{
public Cita crearCita(ConstructorCita constructor, DateTime
fechaInicio, DateTime fechaFin, string descripcion, string localizacion,
ArrayList asistentes)
{
if (constructor == null)
{
constructor = new ConstructorCita();
}
constructor.construirCita();
constructor.construirFechas(fechaInicio, fechaFin);
constructor.construirDescripcion(descripcion);
constructor.construirAsistentes(asistentes);
constructor.construirLocalizacion(localizacion);
return constructor.obtenerCita();
}
}
Veamos cuales son las responsabilidades de cada clase:
- Calendario: Llama al método apropiado de ConstructorCita, devuelve un
objeto Cita completo a su llamador
- ConstructorCita: Contiene métodos de construcción que fuerzan las reglas de
negocio; crea el objeto Cita real
- Cita: Contiene la información sobre una cita
La clase siguiente: ConstructorReunion muestra una de las ventajas del patrón Builder.
Para incluir reglas adicionales en Cita, amplía el constructor existente. En este caso,
ConstructorReunion fuerza una restricción adicional: si una cita es una reunión de
varios días, se deben indicar las fechas de inicio y finalización.
public class ConstructorReunion : ConstructorCita
{
public Cita obtenerCita()
{
try
{
base.obtenerCita();
}
finally
{
if (cita.FechaFin.ToString() == "")
{
elementosRequeridos += FECHA_FIN_REQUERIDA;
}
if (elementosRequeridos > 0)
{
throw new
InformacionRequeridaExcepcion(elementosRequeridos);
}
}
return cita;
}
}
55
Veamos un ejemplo de utilización de este patrón. El ejemplo es muy simple, un
formulario con un botón en el que mediante Cuadros de Mensaje nos da la
información:
public static ArrayList crearAsistentes(int numero){
ArrayList grupo = new ArrayList();
for (int i = 0; i<numero; i++){
grupo.Add(new
Contacto("John",obtenerApellidos(i),"Empleado","Voyodine Cpt."));
}
return grupo;
}
public static string obtenerApellidos(int indice){
string nombre="";
switch (indice % 6){
case 0: nombre="Worfin";
break;
case 1: nombre="SmallBerries";
break;
case 2: nombre="BigBootee";
break;
case 3: nombre="Haughland";
break;
case 4: nombre="Maassen";
break;
case 5: nombre="Sterling";
break;
}
return nombre;
}
private void button1_Click(object sender, EventArgs e){
Cita ct = null;
Calendario pimCalendario = new Calendario();
ConstructorCita cCita = new ConstructorCita();
try{
ct = pimCalendario.crearCita(cCita, new DateTime(2006, 10,
3), new DateTime(), "Convención de Trekkies", "Fargo", crearAsistentes(4));
MessageBox.Show("Cita creada correctamente");
MessageBox.Show("Información de cita:" + ct.ToString());
}
catch (InformacionRequeridaExcepcion exc){
MessageBox.Show(exc.ToString());
}
ConstructorReunion ctReunion = new ConstructorReunion();
try{
ct = pimCalendario.crearCita(ctReunion, new DateTime(2006,
9, 22), new DateTime(), "Convención de Trekkies", "Fargo",
crearAsistentes(4));
MessageBox.Show("Reunión creada correctamente:" +
ct.ToString()); }
catch (InformacionRequeridaExcepcion exc){
MessageBox.Show(exc.ToString());
} try{
ct = pimCalendario.crearCita(ctReunion, new DateTime(2002,
4, 1), new DateTime(2002, 4, 2), "000 Reunión", "Butte",
crearAsistentes(2));
MessageBox.Show("Cita creada correctamente:" +
ct.ToString());}
catch (InformacionRequeridaExcepcion exc){
MessageBox.Show(exc.ToString());}
}
56
PATRONES DE DISEÑO: PATRÓN DE CREACIÓN FACTORY METHOD
También conocido como Virtual Buider, permite definir un método estándar para
crear un objeto, además del constructor propio de la clase, si bien la decisión del objeto
a crear es delegada en las subclases.
La solución es dejar que sean los propios elementos, como por ejemplo, las citas, los
responsables de proporcionar sus propios editores para gestionar cambios y adiciones.
El PIM solo neceista saber como hacer la petición al editor usando un método getEditor
o una propiedad con su getter. El método devolverá un objeto que implemente la
interfaz ItemEditor, y el PIM utiliza ese objeto para solicitar un Control como editor
gráfico. Los usuarios pueden modificar la información del elemento concreto que
desean utilizar, y el editor asegura que los cambios han sido aplicados
apropiadamente.
Aplicabilidad
57
− Necesitamos algunos constructores sobrecargados con la misma lista de
parámetros, lo que no está permitido. En lugar de esto, usaremos varios
métodos de fabricación con el mismo nombre
Descripción
Cuando comenzamos a escribir una aplicación, a menudo está claro qué tipo de
componentes se van a usar. Normalmente se tiene una idea general de las operaciones
a realizar por ciertos componentes, pero la implementación se realizará en otro
momento, por lo que pueden surgir aspectos que no se tuvieron en cuenta.
Esta flexibilidad puede ser alcanzada usando interfaces para estos componentes. El
problema es que no se puede crear un objeto a partir de una interfaz. Se necesita una
clase que las implemente para obtener un objeto. En vez de escribir una clase en la
aplicación, podemos extraer la funcionalidad del constructor e introducirla en un
método. Ese método es el método de fabricación. Esto produce un ConcreteCreator
cuya responsabilidad es crear los objetos adecuados. Este objeto creará instancias de
una implementación (ConcreteProduct) de una interfaz (Iproduct).
Implementación
58
• CreadorConcreto: La clase que hereda de ICreador y proporciona una
implementación para factoryMethod. Puede devolver cualquier objeto que
implemente la interfaz Iproducto.
Facilita la creación dinámica al definir clases cuyos objetos pueden crear copias de sí
mismos.
En un PIM quizá necesitemos poder copiar una dirección de forma que el usuario no
tenga que introducir manualmente toda la información cuando se crea un nuevo
contacto. Una forma de resolver este problema es seguir los siguientes pasos:
Podemos entonces definir un método copy que produzca un duplicado del objeto
Direccion con los mismos datos que el objeto original, el prototipo.
59
• Prototype: Proporciona un modelo de copia. Este método devuelve un objeto
de la misma clase con los mismos valores que el objeto original. El nuevo objeto
puede ser una copia profunda o superficial del original.
• Una copia superficial sólo duplica los elementos de alto nivel de una clase, lo
que proporciona una copia más rápida pero no siempre apropiada. Como las
referencias se copian del original a la copia, aún se refieren a los mismos objetos.
Los de bajo nivel son compartidos entre las copias del objeto, por lo que el
cambio en uno de ellos afecta a todas las copias.
• Las de copia profunda no sólo duplican los atributos de alto nivel, sino también
los objetos de bajo nivel. Esto suele llevar más tiempo y ser muy costoso para
objetos con una estructura arbitrariamente compleja. Esto asegura que los
cambios en una copia se aislan de las otras copias.
Variaciones
Incluyen:
60
public class Prototype
{
private int dato;
// más datos
public Prototype()
{}
public Prototype(Prototype original) : this()
{
this.dato = original.dato;
// copiar el resto de datos
}
// resto del código
}
Método clone: En C# existe un interfaz ICloneable que deben implementar los objetos
que deseemos clonar. Esta interfaz contiene el método Clone() pero no proporciona
ninguna garantía de que el objeto pueda ser clonado, ya que la interfaz no lo define.
Otro problema es que el método Clone() devuelve un object con lo que hay que realizar
una conversión de tipos al apropiado antes de utilizarlo.
Ejemplo:
La clase Direccion de este ejemplo usa el patrón prototype para crear una dirección
basada en una entrada existente. La funcionalidad principal se define en el interfaz
Icopyable:
61
public class Direccion : ICopyable {
private string tip;
public string Tipo
{
get { return tip; }
set { tip = value; }
}
private string cal;
public string Calle
{
get { return cal; }
set { cal = value; }
}
private string cidad;
public string Ciudad
{
get { return cidad; }
set { cidad = value; }
}
private string estdo;
public string Estado
{
get { return estdo; }
set { estdo = value; }
}
private string codPostal;
public string CodigoPostal
{
get { return codPostal; }
set { codPostal = value; }
}
private const string eol = "\n";
private const string coma = ",";
private const string casa = "casa";
private const string trab = "trabajo";
public Direccion(string tipoInic, string calleInic, string ciudadInic, string estadoInic, string
codigoPostalInic)
{
tip = tipoInic;
cal = calleInic;
cidad = ciudadInic;
estdo = estadoInic;
codPostal = codigoPostalInic;
}
public Direccion(string calleInic, string ciudadInic, string estadoInic, string codigoPostalInic)
: this(trab, calleInic, ciudadInic, estadoInic, codigoPostalInic) { }
public Direccion(string tipoInic) {
tip = tipoInic;
}
public Direccion() { }
public object Copy()
{
return new Direccion(cal, cidad, estdo, codPostal);
}
public override string ToString()
{
return "\t" + cal + coma + " " + eol + "\t" + cidad + coma + " " + estdo + " " + codPostal;
}
}
62
PATRONES DE DISEÑO: PATRÓN MEMENTO
Ilustración
Muchos juegos de ordenador se continúan durante mucho tiempo. Ello implica que
necesitamos guardar el estado de un jego de forma que pueda ser resumido
posteriormente. Puede ser útil guardarlo como “checkpoints” de forma que se pueda
volver a un checkpoint previo después de un movimiento fatídico.
Diseño
La grabación del estado se puede realizar de forma que sea independiente del objeto
en sí mismo, y este es un punto clave en el patrón memento. A continuación podemos
ver el UML para este patrón:
Originator (originador) es la clase que soporta objetos cuyo estado será grabado; puede
decidir cuantos estados necesitan ser grabados cada vez. Memento ejecuta el grabado
y Caretaker (similar a portero), guarda el rastro de los diferentes estados almacenados.
• Un amplio interfaz para el Originator que permite acceder a todo aquello que
es necesario que sea guardado o restaurado.
• Un reducido interfaz para el Caretaker de forma que puede guardar y pasar
referencias a memento, pero nada más.
La clase Memento guarda el estado del Originator pero no permite que las otras clases
puedan acceder a él. Por tanto preserva las fronteras de la encapsulación liberando de
esa responsabilidad al Originator.
Implementación
63
Veamos el código a continuación. Este programa muestra como unas cadenas pueden
ser grabadas y luego restauradas si el primer carácter de la línea es *. De esta forma,
los errores en los datos del Originator pueden ser corregidos.
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Collections.Generic;
using System.Collections;
// Patrón Memento
// Ilustración simple con "deshacer" (*UNDO)
class MementoPattern
{
// Cliente
static void Main()
{
// Referencias a los mementos
Caretaker[] c = new Caretaker[10];
Originator originator = new Originator();
int move = 0;
// Iterador para los movimientos
Simulator simulator = new Simulator();
// Originator
[Serializable()]
class Originator
{
List<string> state = new List<string>();
64
public Memento SetMemento()
{
Memento memento = new Memento();
return memento.Save(state);
}
[Serializable()]
// Serializa por copia profunda a memoria y vuelve
class Memento
{
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
class Caretaker
{
public Memento Memento { get; set; }
}
string[] lines = {
"The curfew tolls the knell of parting day",
"The lowing herd winds slowly o'er the lea",
"Uh hum uh hum",
"*UNDO",
"The plowman homeward plods his weary way",
"And leaves the world to darkness and to me."};
65
/* Salida
The curfew tolls the knell of parting day
=======================
The curfew tolls the knell of parting day
The lowing herd winds slowly o'er the lea
=======================
The curfew tolls the knell of parting day
The lowing herd winds slowly o'er the lea
Uh hum uh hum
=======================
The curfew tolls the knell of parting day
The lowing herd winds slowly o'er the lea
The plowman homeward plods his weary way
=======================
The curfew tolls the knell of parting day
The lowing herd winds slowly o'er the lea
The plowman homeward plods his weary way
And leaves the world to darkness and to me.
=======================
*/
En el siguiente ejemplo, vamos a crear el juego del tres en raya (o TicTacToe) que nos
puede servir como aplicación de este patrón. En este ejemplo, consideraremos un tutor
de Tres en Raya que enfrenta a un jugador humano contra el ordenador. El jugador
aprende las mejores estrategias por ensayo-error y por tanto debería ser capaz de
volver atrás y probar diferentes movimientos si no está ganando.
Esta es una situación ideal para este tipo de patrón. El Originator será el tablero, y su
estado es grabado en cada turno. El caretaker mantiene un array de mementos de
forma que podamos volver atrás más de un turno. En el código del ejemplo, si vemos
una posible salida, se muestra que después de un movimiento fatal, el jugador vuelve
atrás dos mementos y continua con una mejor estrategia.
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Collections;
// Memento Pattern
// Simula un tres en raya donde en el juego se puede volver atrás
// cualquier número específico de movimientos
class MementoPattern
{
// Cliente
static void Main()
{
Console.WriteLine("Practiquemos el Tres en Raya");
Console.WriteLine("Los comandos son:\n1-9 para una posición\n" +
"U-n donde n es el número de movimientos a
deshacer" +
"\nQ para finalizar");
Game game = new Game();
game.DisplayBoard();
66
int move = 1;
// Iterador para los movimientos
Simulator simulator = new Simulator();
// Chequea un deshacer
if (command[0] == 'U')
{
int back = Int32.Parse(command.Substring(2, 1));
if (move - back > 0)
game.Restore(c[move - back].Memento);
else
Console.WriteLine("Demasiados movimientos a deshacer");
move = move - back - 1;
}
// sino, juega
else
game.Play(Int32.Parse(command.Substring(0, 1)));
// Originator
[Serializable()]
class Game
{
// nueve espacios
char[] board = { 'X', '1', '2', '3', '4', '5', '6', '7', '8', '9'
};
public char Player { get; set; }
public Game()
{
Player = 'X';
}
67
Memento memento = new Memento();
return memento.Save(board);
}
[Serializable()]
// Serializa por copia profunda en memoria y vuelve
class Memento
{
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
class Caretaker
{
public Memento Memento { get; set; }
}
class Simulator : IEnumerable
{
string[] moves = { "5", "3", "1", "6", "9", "U-2", "9", "6", "4",
"2", "7", "8", "Q" };
public IEnumerator GetEnumerator()
{
foreach (string element in moves)
yield return element;
}
}
68
/* Salida
Practiquemos tres en raya
Los comandos son:
1-9 para una posición
U-n donde n es el número de movimientos a deshacer
Q para finalizar
1 | 2 | 3
---------
4 | 5 | 6
---------
7 | 8 | 9
Movimiento 1 para X: 5
1 | 2 | 3
---------
4 | X | 6
---------
7 | 8 | 9
Movimiento 2 para O: 3
1 | 2 | O
---------
4 | X | 6
---------
7 | 8 | 9
Movimiento 3 para X: 1
X | 2 | O
---------
4 | X | 6
---------
7 | 8 | 9
Movimiento 4 para O: 6
X | 2 | O
---------
4 | X | O
---------
7 | 8 | 9
Movimiento 5 para X: 9
X | 2 | O
---------
4 | X | O
---------
7 | 8 | X
Movimiento 6 para O: U-2
X | 2 | O
---------
4 | X | 6
---------
7 | 8 | 9
Movimiento 4 para O: 9
X | 2 | O
---------
4 | X | 6
---------
7 | 8 | O
Movimiento 5 para X: 6
X | 2 | O
---------
4 | X | X
---------
7 | 8 | O
Movimiento 6 para O: 4
69
X | 2 | O
---------
O | X | X
---------
7 | 8 | O
Movimiento 7 para X: 2
X | X | O
---------
O | X | X
---------
7 | 8 | O
Movimiento 8 para O: 7
X | X | O
---------
O | X | X
---------
O | 8 | O
Movimiento 9 para X: 8
X | X | O
---------
O | X | X
---------
O | X | O
Movimiento 10 para O: QGracias por jugar
*/
Generalmente se desarrolla un grupo de clases para las listas, cada una de las cuales
puede proporcionar una forma específica de mostrar las listas. Sin embargo, esta
solución suele descartarse rápidamente, ya que hay muchas combinaciones de las
formas de mostrar una lista y de almacenar su información.
70
elementos, heredamos de ListImpl. La belleza de esta solución radica en que puede
“mezclar y emparejar” las clases, logrando un nivel de compatibilidad mucho mayor.
Este patrón resuelve el problema desacoplando los dos aspectos del componente. Con
dos cadenas de herencia separadas (una para implementación y otra para
funcionalidad) es mucho más fácil mezclar y asociar los elementos de cada lado. Esto
proporciona una flexibilidad general con un menor coste de codificación.
Es útil para cualquier sistema que deba mostrar flexibilidad en tiempo de ejecución.
Por ejemplo, los sistemas de interfaz gráfica que deben ser portables entre plataformas.
Dichos sistemas necesitan que la implementación subyacente sea aplicada cuando la
aplicación se inicia en un sistema operativo distinto. Los sistemas que cambian la
representación de sus datos dependiendo de la localización (por ejemplo, modificando
la representación de las fechas, idioma o unidad monetaria) a menudo son buenos
candidatos para usar el patrón bridge. De igual forma, este patrón suele ser efectivo
para entidades de negocio que pueden ser asignadas a diferentes fuentes de bases de
datos.
Un ejemplo conceptual del patrón es una centralita de soporte técnico. Hay varias
líneas preestablecidas que conectan a los usuarios con el personal de soporte técnico.
Naturalmente, la respuesta será diferente dependiendo de la experiencia del técnico.
La respuesta también variará en función de la pregunta.
71
Implementación
Cuando se diseña una aplicación que usa este patrón, es importante definir
apropiadamente qué responsabilidades pertenecen a la abstracción funcional y cuales
a la clase de implementación interna. Asimismo, se debe considerar con cuidad lo que
representa el verdadero modelo base de la implementación del modelo Bridge. Un
problema común proviene del desarrollo de la implementación alrededor de una o dos
posibles variaciones. El peligro es que el desarrollo futuro del patrón revelará que
algunos de los supuestos elementos del comportamiento principal realmente
representan variaciones específicas basadas en la abstracción y/o la implementación.
Ejemplo
En el ejemplo, vamos a utilizar una sencilla lista de tareas pendientes, con capacidad
para insertar y eliminar cadenas. Para Bridge, un elemento se definen en dos partes:
abstracción e implementación. La implementación es la clase que hace el trabajo real,
72
en este caso, almacenar y recuperar entradas en la lista. El comportamiento general se
define en la interfaz IlistImpl:
73
La abstracción representa las operaciones de la lista que están disponibles para el
mundo exterior. La clase BaseList proporciona las capacidades generales de las listas:
Como podemos ver, todas las operaciones son delegadas a la variable que rerefencia
la implementación de la lista. Cuando se realizan peticiones a List, las operaciones son
delegadas “por el puente” al objeto ListImpl asociado.
Es fácil ampliar las características proporcionadas por BaseList creando una nueva
subclase de BaseList e introduciendo una nueva funcionalidad. La clase NumberedList
muestra la potencia del patrón redefiniendo el método GetByIndex, siendo capaz de
proporcionar una numeración para los elementos de la lista.
74
Podemos ver a continuación una extensión del ejemplo:
public class OrnamentedList : BaseList
{
private char itmType;
public char ItemType
{
get { return itmType; }
set {
if (value > ' ')
itmType = value; }
}
public override string GetByIndex(int Index)
{
return itmType + " " + base.GetByIndex(Index);
}
}
Aplicación de consola:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Ejemplo de patrón Bridge");
Console.WriteLine("\nEste ejemplo divide un comportamiento complejo en dos");
Console.WriteLine("clases - la abstracción y la implementación.");
Console.WriteLine("\nEn este caso, hay dos clases que pueden proporcionar la");
Console.WriteLine("abstracción - BaseList y OrnamentedList. La BaseList");
Console.WriteLine("proporciona funcionalidad básica, mientras que la Ornamented");
Console.WriteLine("se expande en el modelo añadiendo un carácter de lista.");
Console.WriteLine("\nLa clase OrderedListImpl proporciona la capacidad de almacenamiento");
Console.WriteLine("subyacente para la lista, y puede ser emparejada de forma flexible");
Console.WriteLine("cualquiera de las clases que proporcionen la abstracción.");
Console.WriteLine("\nCreando el objeto OrderedListImpl.");
IListImpl implementation = new OrderedListImpl();
Console.WriteLine("\nCreando una lista base.");
BaseList listaUno = new BaseList();
listaUno.Implementor = implementation;
Console.WriteLine("\nAñadiendo elementos a la lista.");
listaUno.Add("Uno");
listaUno.Add("Dos");
listaUno.Add("Tres");
listaUno.Add("Cuatro");
Console.WriteLine("\nCreando una Lista Ornamentada");
OrnamentedList listaDos = new OrnamentedList();
listaDos.Implementor = implementation;
listaDos.ItemType = '+';
Console.WriteLine("\nCreando una lista Numérica");
NumberedList listaTres = new NumberedList();
listaTres.Implementor = implementation;
Console.WriteLine("\nMostrando la primera lista (Lista Base)");
for (int i = 0; i < listaUno.Count; i++)
Console.WriteLine("\t" + listaUno.GetByIndex(i));
Console.WriteLine("\nMostrando la segunda lista (Lista Ornamentada)");
for (int i = 0; i < listaDos.Count; i++)
Console.WriteLine("\t" + listaDos.GetByIndex(i));
Console.WriteLine("\nMostrando la tercera lista (Lista Numerada)");
for (int i = 0; i < listaTres.Count; i++)
Console.WriteLine("\t" + listaTres.GetByIndex(i));
Console.ReadLine();
}
75
SOLID
Cohesión
Acoplamiento
Fue acuñado por Robert C. Martin en un artículo del mismo título y popularizado a
través de su libro “Agile Software Development: Principles, Patterns and Practices”.
SRP tiene que ver con el nivel de acoplamiento entre módulos dentro de la ingeniería
del software. En términos prácticos establece que:
Una clase debe tener una y solo una única causa por la cual puede ser modificada.
76
Si una clase tiene dos responsabilidades, entonces asume dos motivos por los cuales
puede ser modificada. Por ejemplo, supongamos una clase Factura la cual dentro de
un contexto determinado ofrece un método para calcular el importe total.
Separando responsabilidades
Para separarlas, debemos primero identificarlas. Los pasos que realiza CalcularTotal
son:
77
Identificamos, por tanto, tres responsabilidades. Ya que una responsabilidad no es una
acción, sino un motivo de cambio, debemos extraer ambas responsabilidades en otras
clases: IVA y Deduccion:
78
interface Modem
{
void marcar(int numero);
void colgar();
void mandar(char[] datos);
char[] recibir();
}
Las expresiones lambda proporcionan una sintaxis concisa para escribir métodos
anónimos. La especificación C# 3.0 describe estas expresiones como un superconjunto
de métodos anónimos.
En C# 2.0, podemos escribir un delegado para usarlo como método anónimo como
podemos ver en este ejemplo:
Una expresión lambda puede usar dos argumentos, especialmente cuando estamos
usando los “Standard Query Operators”, es decir, operadores estándar de consulta.
Primero declararemos el siguiente delegado que usa dos argumentos:
public delegate int MiDelegado(int m,int n);
79
Ahora podemos instanciar el delegado utilizando una expresión lambda tal como la
siguiente:
MiDelegado miDelegado = (x,y) => (x*y);
Inicialmente definimos las variables que contendrán los nombres de los ficheros de
entrada y salida, así como la clave. El programa va a comprobar que le estemos
pasando 4 parámetros: la acción a realizar, el fichero de entrada, la clave y el fichero
de salida. Para ello usamos if(args.Length==4). Si han sido pasados los cuatro
parámetros, tomamos decisiones. Si hemos mandado una “e”, llamamos a la función
EncriptaDES() , y en caso contrario, llamamos a DesencriptaDES().
80
using System;
using System.IO;
using System.Security;
using System.Security.Cryptography;
using System.Text;
class encripta{
static void Main(string[] args){
string entrada;
string salida;
string clave;
if (args.Length==4){
entrada=args[1];
salida=args[3];
clave=args[2];
if (args[0]=="e")
EncriptaDES(entrada,salida,clave);
else if (args[0]=="d")
DesencriptaDES(entrada,salida,clave);
else{
Console.WriteLine("encripta v0.5");
Console.WriteLine("ERROR: opción no válida. Debe
ser e/d");}}
else{
Console.WriteLine("encripta v0.5");
Console.WriteLine("Parámetros no válidos");}}
static void EncriptaDES(string fichero_entrada,string
fichero_encriptado,string clave){
FileStream fsEntrada = new
FileStream(fichero_entrada,FileMode.Open,FileAccess.Read);
FileStream fsEncriptado=new
FileStream(fichero_encriptado,FileMode.Create,FileAccess.Write);
DESCryptoServiceProvider DES = new DESCryptoServiceProvider();
DES.Key=ASCIIEncoding.ASCII.GetBytes(clave);
DES.IV=ASCIIEncoding.ASCII.GetBytes(clave);
ICryptoTransform encriptadorDES=DES.CreateEncryptor();
CryptoStream cryptostream = new
CryptoStream(fsEncriptado,encriptadorDES,CryptoStreamMode.Write);
byte[] Bytes_entrada= new byte[fsEntrada.Length];
fsEntrada.Read(Bytes_entrada,0,Bytes_entrada.Length);
cryptostream.Write(Bytes_entrada,0,Bytes_entrada.Length);
cryptostream.Close();
fsEntrada.Close();
fsEncriptado.Close(); }
static void DesencriptaDES(string fichero_entrada,string
fichero_encriptado,string clave){
DESCryptoServiceProvider DES=new DESCryptoServiceProvider();
DES.Key=ASCIIEncoding.ASCII.GetBytes(clave);
DES.IV=ASCIIEncoding.ASCII.GetBytes(clave);
FileStream fs_encriptado=new
FileStream(fichero_entrada,FileMode.Open,FileAccess.Read);
ICryptoTransform descifrador= DES.CreateDecryptor();
CryptoStream cryptostreamdesc= new
CryptoStream(fs_encriptado,descifrador,CryptoStreamMode.Read);
StreamWriter fsDesencriptado=new
StreamWriter(fichero_encriptado);
fsDesencriptado.Write(new
StreamReader(cryptostreamdesc).ReadToEnd());
fsDesencriptado.Flush();
fsDesencriptado.Close();
}
}
81
MANEJO DE EXCEPCIONES
using System;
public class Hola
{
public static int Main(string[] args)
{
try
{
Console.WriteLine(args[0]);
}
catch (Exception e)
{
Console.WriteLine("Excepcion {0}",e.StackTrace);
}
return 0;
}
}
Cuando un error en tiempo de ejecución ocurre en una aplicación C#, el S.O. lanza una
excepción. Si usamos una construcción del tipo try-catch como vemos en el ejemplo
introductorio, podemos capturar dicha excepción. Si cualquiera de las sentencias en la
parte del try causa que el S.O. lance una excepción, la ejecución será transferida al
bloque match.
82
Otro ejemplo de excepciones:
using System;
public class Hola
{
public static int Main(string[] args)
{
try
{
int i, j;
string temp;
Console.WriteLine ("Introduzca el primer entero, por
favor");
temp = Console.ReadLine( );
i = Int32.Parse(temp);
Console.WriteLine ("Introduzca el segundo entero, por
favor");
temp = Console.ReadLine( );
j = Int32.Parse(temp);
int k = i / j;
Console.WriteLine("El resultado de dividir {0} por {1} es
{2}", i, j, k);
}
catch(Exception e)
{
Console.WriteLine("Se ha producido un error: {0}", e);
}
return 0;
}
}
using System.Diagnostics;
[DebuggerDisplay("Nombre completo de ciudadano = {honorifico + ' ' +
primero + ' ' + segundo + ' ' + apellido}")]
public class Ciudadano{
private string honorifico;
private string primero;
private string segundo;
private string apellido;
public Ciudadano(string honorif, string primer, string segun, string
apell)
{
honorifico = honorif;
primero = primer;
segundo = segun;
apellido = apell;
}
}
83
Cuando se ejecute el código en el depurador, se utilizará una versión personalizada
que incluirá el nombre completo.
Al arrancar Visual Studio.Net (por defecto en Inicio -> Programas -> Microsoft Visual
Studio .NET 7.0 -> Microsoft Visual Studio .NET) obtendremos una pantalla como la
siguiente:
84
[Imagen 1. Pantalla inicial de Visual Studio.NET]
Lo primero que hemos de hacer es crear un nuevo proyecto donde almacenar los
archivos de la aplicación. Para ello pulsamos Archivo -> Nuevo -> Proyecto y
obtendremos una ventana desde la que podremos seleccionar tanto el lenguaje con el
que vamos a trabajar como el tipo de proyecto que vamos a desarrollar, el nombre del
mismo y el directorio base en el que se almacenarán los archivos del proyecto. Así, con
las opciones que nosotros seleccionaremos el aspecto final de esta ventana será el
siguiente:
85
[Imagen 2. Ventana de selección de tipo de proyecto]
Como se ve, nosotros hemos seleccionado en la ventana Tipos de Proyecto que vamos
a realizar un proyecto escrito en C# (opción Proyectos de Visual C#), en la ventana
Plantillas que vamos a desarrollar una aplicación de venantas (opción Aplicación para
Windows), en el cuadro de texto Nombre que vamos a darle el nombre EditorEjemplo
a nuestro proyecto, y en el cuadro de texto Ubicación que el directorio base donde
instalaremos el proyecto será c:\C# Nótese que debajo de Ubicación aparece un
mensaje que nos informa cual será el directorio donde finalmente se almacenarán los
archivos de nuestro proyecto, y como se ve, este directorio es el resultado de
concatenar el nombre de nuestro proyecto con la ruta del directorio base que hayamos
especificado.
Una vez configuradas todas estas opciones, sólo queda pulsar el botón Aceptar para
que se cree toda la infraestructura adecuada para que podamos empezar a trabajar en
nuestro proyecto. La ventana que tras ello se obtiene tiene un aspecto similar al
siguiente:
86
[Imagen 3: Ventana principal de diseño de Visual Studio.NET]
Como se puede ver en el dibujo, se distinguen tres partes muy importantes en nuestra
área de trabajo:
• La ventana de diseño (central), donde iremos colocando los elementos que
formarán parte de la interfaz de nuestra aplicación y desde la que podemos
modificar el código de estos con sólo hacer doble click con el ratón sobre ellos.
• La ventana de herramientas (toolbox) donde se recogen los objetos más
comunes que se utilizan a la hora de crear aplicaciones de ventanas. Estos
objetos, a los que llamaremos controles, se pueden ir colocando sobre la ventana
de diseño sin más que ir seleccionándolo en la barra de herramientas y
arrastrándolos sobre la posición deseada de la ventana de diseño.
• La ventana de propiedades (properties), desde donde aparecerán las
propiedades del objeto que en cada momento haya seleccionado haciendo click
sobre él con el ratón. A través de esta ventana podemos obtener una ayuda
rápida sobre sus propiedades y modificarlas con simples pulsaciones de ratón.
Por defecto las propiedades aparecen ordenadas en ella por categorías,
apareciendo juntas las relacionadas entre sí; sin embargo, podemos ordenarlas
alfabéticamente pulsando el segundo botón situado en la zona superior de esta
ventana (empezado a contar por la izquierda)
Diseño de la interfaz
87
Ahora que ya estamos familiarizados con el entorno con el que vamos a trabajar,
podemos empezar a diseñar la interfaz de nuestra aplicación. Para ello, de la barra de
herramientas iremos seleccionando los controles que necesitemos y los iremos
arrastrando sobre la ventana de diseño (ver Imagen 4). A medida que los vayamos
usando se explicarán a fondo estos controles, y por el momento sólo diremos que son:
+ Un RichTextBox, que será el área sobre el que mostrará el texto el editor.
+ Un MainMenu, que contendrá el menú principal de nuestra aplicación
+ Un OpenFileDialog, un SaveFileDialog, un FontDialog y un PrintDialog,
que se corresponden respectivamente con las ventanas estándar utilizadas en
Windows para la apertura y cierre de ficheros, selección de fuente e impresión.
+ Un PrintDocument, que será el objeto que utilicemos para imprimir el texto
de nuestro editor y que tomará como información los datos que recoja el
PrintDialog previamente introducido.
Como podemos ver, no todos los controles que arrastremos sobre la ventana de diseño
se mostrarán sobre ésta, sino que algunos lo harán en un área en blanco situado debajo
de ella. Esto se debe a que son controles que, o no tienen una componente visual, o no
merece la pena mostrarla en la zona de diseño.
Si tras arrastrar un control éste no tiene el tamaño que a nos interesa, podemos
modificar este tamaño “tirando” de él mediante los cuadrados que aparecen en sus
extremos (igual que en cualquier programa de tratamiento de imágenes) Nótese como
a medida que vaya modificando el tamaño o posición de un control se irán
actualizando automáticamente los valores de las propiedades correspondientes a su
alto (propiedad Height), ancho (Width), posición en el eje OX de su esquina superior
izquierda (Left) y posición en OY de su esquina superior izquierda (Top)
Todos los controles que hemos creado cuentan con una serie de valores por defecto
para sus propiedades, como se puede observar echándoles un vistazo en la ventana de
88
propiedades. Muchos de estos valores son los más utilizados generalmente y nos
vienen bien; sin embargo, hay otros que no nos interesan tal y como están por lo que
tendremos que cambiarlos (por ejemplo, el texto por defecto de nuestro editor de texto
no debería ser richTextBox1) Para ello, iremos seleccionando cada uno de los objetos de
nuestra aplicación y modificando los valores de sus propiedades en la ventana de
propiedades como se indica:
Tras esto, le cambiamos el valor de su propiedad Anchor de manera que todos los
rectángulos que aparecen en la ventana asociada a la misma, que aparecerá al pulsar
sobre la fecha junto a ella incluida, queden marcados. Esta propiedad indica respecto
a qué lados de su ventana contenedora se “anclará” el control, y seleccionar todos los
rectángulos como se ha indicado indica que deseamos que sean todos, con lo que
siempre que se redimensione la ventana nuestro control se ajustará automáticamente
al nuevo tamaño que se le dé.
Finalmente, vamos a cambiar el valor de la propiedad Font, que indica el tipo de fuente
que se usará el control para mostrar su texto, por otra fuente que puede ser más bonita:
la Arial de tamaño 10. Para ello, podemos teclear manualmente Arial 10 pt como valor
de esa propiedad o, lo que es más cómodo, pulsar el botón con puntos suspensivos
que aparece a su lado para seleccionar gráficamente de la forma en que acostumbra a
hacer en Windows.
Por último, vamos modificar el valor de su propiedad Filter Esta propiedad indica
cuáles son los archivos que la ventana nos dejará seleccionar. El valor de esta
propiedad ha de ser una cadena de texto de la forma
descripciónArchivo1|filtroArchivo1|descripciónArchivo2|filtro2... Nosotros le daremos el
valor Archivos de texto|*.txt|Todos los archivos |*.* De este modo, nuestra ventana en
primer lugar sólo nos dejará seleccionar archivos de extensión .txt, pues el tipo de
archivo indicado en primer lugar; y en caso de que el usuario quiera seleccionar otro
siempre podrá hacerlo modificando la lista desplegable incluida en la ventana de
selección de archivo gracias a que como segundo tipo de archivo admitido permitimos
cualquier archivo (*.*)
1 Nótese que (Name) aparece entre paréntesis ya que en realidad no es una propiedad del objeto, sino el
nombre del objeto. Sin embargo, por comodidad se ha optado por incluirlo en la ventana de propiedades.
89
Al SaveFileDialog le hacemos cambios muy similares caso anterior. Ahora cambiamos
el Title por Seleccione el archivo donde guardar, el (Name) por dlGuardarArchivo y el Filter
por Archivos de texto |*.txt|Todos los archivos|*.*
Para terminar con el diseño la interfaz de nuestra aplicación nos queda por diseñar
únicamente el menú principal. De las propiedades del objeto MainMenu sólo hemos
de cambiar su nombre por menu, siendo lo verdaderamente interesante el diseño de
los submenús que en el se contendrán, lo cual se hace de forma muy sencilla y vistosa
sin más que ir rellenando los recuadros Escriba Aquí adecuados con los nombres de los
submenús que deseemos ir creando hasta obtener la siguiente estructura:
90
La siguiente imagen muestra gráficamente el proceso de relleno de recuadros “Escriba
aquí” antes comentado:
A cada submenú le daremos un nombre igual al texto que mostrará, aunque precedido
de la cadena menu (por ejemplo, menuArchivo, menuGuardarComo...) La única excepción
a este caso serán las barras separadoras, que se incluyen escribiendo – en el Escriba
Aquí correspondiente al lugar donde se desee que aparezcan y que no usaremos para
nada aparte de para organizar un poco mejor el contenido de nuestros menús, por lo
que no tiene mucho sentido que le demos un nombre fácil de recordar.
+ En el texto que muestran los menús (y otros controles como los botones) le podemos
incluir un carácter & antes de cualquier letra que será interpretado como carácter a
usar para acceder al mismo mediante la combinación Alt+tecla Además, el carácter que
haya a la derecha de este & aparecerá subrayado para indicar que es el usado para
acceder rápidamente al control con dicha combinación2.
2 Si se incluyen más de un & los siguientes al primero son ignorados. Si se desea que & forme parte del
texto del control habrá que incluir dos & seguidos (&&)
91
+ Podemos indicar que un submenú es el que se activará por defecto cuando hagamos
doble click sobre el nombre del menú padre que lo contiene. Para ello, hemos de dar
el valor true a la propiedad DefaultItem del mismo. Este tipo de submenús se
caracterizan porque su texto aparece en negrita3.
+ Por defecto todos los submenús aparecen activados; es decir, de modo que puedan
ser seleccionados. Sin embargo, en el caso del submenú Reabrir no nos interesa que
esto sea posible hasta que se haya abierto algún fichero, por lo que hemos de modificar
su propiedad Enabled y darle el valor false inicialmente.
Windows es un sistema guiado por eventos, y cualquier aplicación gráfica escrita para
Windows consiste básicamente en escribir código de respuesta a eventos. Por ejemplo,
cuando el usuario pulsa un botón se lanza un evento con el que se indica que dicho
botón ha sido pulsado, y es el programador el que ha de determinar qué acción se ha
de realizar en respuesta a dicha pulsación.
En C# para escribir este código de respuesta, en la clase de cada control se define una
serie de miembros denominados eventos a los que les podemos asociar una o más
funciones a ejecutar en caso de que se produzca el evento apropiado. Por ejemplo, la
clase de los submenús (MenuItem) tiene un evento Click que se declararía así en la
definición de la clase:
class MenuItem: Menu
{
// ... (Otras definiciones)
public event EventHandler Click;
// ... (Otras definiciones)
}
Nótese que tras el nombre de la clase MenuItem se ha colocado el nombre de otra clase
(Menu) separado por dos puntos. Esto significa que la clase MenuItem que estamos
declarando deriva de la clase Menu. Es decir, básicamente esto significa que incluye
todas las definiciones de métodos, propiedades y demás miembros de la clase Menu
más los que definamos en ella; y que si definimos miembros de igual signatura que
miembros definidos en Menu las nuevas definiciones de estos sustituirán a las
heredadas de Menu. Nótese que también la clase principal de nuestra aplicación deriva
de clase Form, que incluye definiciones para el comportamiento básico de una ventana.
La definición de evento del ejemplo anterior significa que Click es un evento al que le
podemos asociar como código respuesta funciones cuya signatura (definición de valor
de retorno, nombre y parámetros de la función) sea compatible con la indicada por el
delegado EventHandler Un delegado podemos verlos como una declaración de valor
3 Si se pone a true dos o más submenús de un mismo menú padre sólo se activará con el doble click el
primero. Sin embargo, ambos aparecerán en negrita.
92
de retorno y parámetros de función a la que se le asocia un nombre. Por ejemplo, en el
caso concreto de EventHandler, éste delegado está definido de la siguiente forma:
// ...
miMenu.Click += new EventHandler(miCódigoRespuesta);
// ...
Nótese que para asociar un código de respuesta a un evento se utiliza el operador +=.
Este operador puede usarse tantas veces como se desee sobre un mismo evento, con lo
se asociarían diversos códigos de respuesta al mismo, que se ejecutarían uno detrás
tras otro en caso de producirse el evento.
93
También podemos eliminar de un evento códigos de respuesta ya asociados. Para ello
se usa el operador -=, y un ejemplo de cómo quitar el código asociado al evento en el
ejemplo anterior es:
miMenu.Click -= new EventHandler(miCódigoRespuesta);
A partir de esta instrucción, cada vez que se pulsase miMenu no se realizará ninguna
acción (a no ser que hayan asociado otros códigos de respuesta al evento Click)
Este será el método que usaremos en nuestro editor de texto para asociar código de
respuesta, y a continuación se describe cómo escribir este código para cada uno de los
controles de nuestra aplicación para conseguir la funcionalidad deseada en la misma:
menuNuevo:
Nótese que también realizamos otras acciones en el código del método. Éstas consisten
en cambiar el título de la ventana principal por EditorEjemplo, ya que en este título
almacenaremos información sobre el nombre del fichero abierto en cada momento,
pero al crear uno nuevo no tiene nombre, y desactivar el submenú Reabrir, ya que no
tiene sentido reabrir ningún archivo porque estamos creando uno nuevo.
Es significativo el uso de la palabra reservada this para hacer referencia al objeto al que
pertenece el método que estamos escribiendo. Recordemos que los códigos de
respuesta los estamos escribiendo dentro de la de la clase que representa la ventana de
94
la aplicación (que deriva de Form), por lo que this hace referencia a la ventana nuestro
editor y Text es la propiedad de la ventana que almacena su título.
menuAbrir
En este caso hay que mostrar al usuario el fdAbrirArchivo para recoger información
sobre el nombre del archivo a abrir para, tras ello, abrirlo y guardar como contenido
de la caja de texto del editor la información en este almacenada. Esto lo hacemos con
un el siguiente código de respuesta (recordemos que para escribirlo basta hacer doble
click con el ratón sobre el submenú de título Abrir... y Visual Studio se encargará de
generar automáticamente el esqueleto del mismo):
95
nosotros indicamos que vamos a cargar un fichero en formato de texto plano5 usando
el valor PlainText de la enumeración RichTextBoxStreamType. Téngase en cuenta que
un objeto de clase RichTextBox también es capaz de almacenar texto en formato RTF6
(valor RichText de la enumeración RichTextBoxStreamType)
Tras cargar el contenido del fichero de texto en nuestra caja de texto, sólo queda
modificar el título de la ventana de propiedades para que contenga el nombre del
fichero abierto entre corchetes (nótese el uso de + como operador de concatenación de
cadenas) y, dado que ahora sí tenemos un fichero abierto, activar la posibilidad de
reabrirlo.
MenuReabrir
El funcionamiento de este menú es muy similar al anterior, sólo que ahora no hemos
de preguntar al usuario cuál es el fichero a abrir sino que podemos obtenerlo a partir
del nombre del último fichero abierto. Por ello, su código de respuesta es:
El código de este método es mucho más sencillo que en el caso anterior, ya que ahora
no hemos de actualizar la barra de título con el nombre del fichero abierto porque era
el que ya había; y no hemos de activar el menú Reabrir porque ya estaba activado (de
hecho, es el botón pulsado)
Para obtener el nombre del fichero hemos usado el método Substring() definido para
cualquier cadena (clase String) que devuelve la subcadena comprendida entre el
elemento cuyo índice coincide con el valor de su primer parámetro y cuya longitud es
la indicada en el segundo. Como el primer elemento de la barra de título siempre será
un corchete ya que lo que se indica en ella es el nombre del fichero abierto, nos
saltaremos el primer elemento del texto del título y obtendremos la subcadena que
comienza en el índice 1 (las cadenas se indexan desde 0 en C#) Para obtener la longitud
de la subcadena a obtener usamos el método LastIndexOf() de la clase String que
devuelve el valor del índice de la última aparición de la subcadena que se le pasa como
parámetro.
menuGuardar
Al pulsar este submenú hemos de guardar el contenido del texto que se esté editando
dentro del fichero que se está editando, de modo que todos los cambios hechos en la
5 Este formato consiste en almacenar en el fichero sólo los caracteres que componen el texto a mostrar.
6 Este formato consiste en almacenar en el fichero también información que indiquen características de
formato del texto, márgenes, etc.
96
caja de texto se hagan permanentes en el fichero. Para ello, hemos de comprobar si el
fichero que estamos escribiendo es un fichero nuevo o no, ya que en el primer caso
habría que solicitar al usuario un nombre de fichero donde guardar esta información
y en el segundo caso no. Esto lo hacemos así:
protected void menuGuardar_Click (object sender, System.EventArgs e)
{
String nombreFichero;
if (this.Text == "EditorEjemplo")
menuGuardarComo.PerformClick();
else
{
nombreFichero = this.Text.Substring(1, this.Text.LastIndexOf("]") - 1);
rtbTexto.SaveFile(nombreFichero, RichTextBoxStreamType.PlainText);
}
}
Véase pues, que lo que se hace para detectar si el fichero es nuevo consiste en
comprobar si el texto de la barra de título de la ventana del editor coincide con el valor
de ésta por defecto, caso en que ello querría decir que sí es nuevo y, por consiguiente,
habría que solicitar el nombre del fichero donde guardar; es decir, estaríamos en un
caso equivalente al de haber pulsado el submenú Guardar como, por lo que lo que
haremos será simular que este se ha pulsado llamando al método PerformClick() del
mismo. En caso de que el fichero no sea nuevo, obtendremos su nombre de forma
similar a como se hizo en el código de respuesta a la pulsación de menuReabrir y
guardaremos el contenido de la caja de texto en dicho fichero llamando al método
SaveFile() de la caja de texto. Este método funciona de forma recíproca al LoadFile()
ya visto, sólo que en este caso en vez de cargar el texto del fichero en la caja de texto lo
que se hace es cargar el contenido de la caja de texto en el fichero.
menuGuardarComo
Cuando se selecciona este botón hay que mostrar al usuario una de las típicas ventanas
de Windows en las que se solicita un nombre de fichero donde guardar información.
Sólo en caso de que se haya seleccionado un nombre de fichero en esta ventana y se
haya pulsado su botón OK se guardará el contenido de la caja de texto de nuestro
editor en el archivo indicado. Para hacer esto se ejecuta el siguiente código:
97
Del código puede deducirse fácilmente que lo único que se hace es llamar al método
ShowDialog() del dlGuardarArchivo para que muestre la ventana antes comentada,
comprobar que se pulse el botón OK de la misma viendo si el valor de retorno de este
método es el valor OK de la enumeración DialogResult y, en ese caso, guardar el texto
contenido en rtbTexto en el fichero indicado llamando al método SaveFile() de dicho
objeto RichTextBox. Finalmente, se cambia el título de la barra de la ventana del editor
para actualizar la información de la misma sobre el nombre del fichero que se está
editando y se habilita el submenú Reabrir dando el valor true a su propiedad Enabled,
ya que ahora sí se tiene un nombre de fichero para reabrir.
menuImprimir
El código de respuesta a este evento es, por tanto, tan simple como:
98
tipo PrintPageEventArgs con el que se llamó a éste método. DrawString() imprime la
cadena que se le pasa como parámetro usando la fuente y el color indicados dentro del
área del papel que se le especifica en su último parámetro. Para especificar el texto y
el color se usan directamente los valores almacenados en las propiedades Text y Font
de nuestro rtbTexto, mientras que para especificar el color a usar es necesario generar
un SolidBrush a partir del color almacenado en la propiedad ForeColor de dicha caja
de texto, ya que esto es lo que DrawString() espera. Finalmente, para especificar el área
de impresión se ha optado por utilizar un objeto RectangleF que almacena
información sobre el rectángulo correspondiente a los márgenes a usar (posición en
OX de su esquina superior izquierda, posición en OY, ancho y alto) Los valores
pasados a éste objeto en su constructor son los que se obtienen por defecto a través de
la propiedad MarginBounds del objeto PrintPageEventArgs pasado como argumento
al método.
menuSalir
Cuando se pulse el botón Salir lo que hay que hacer es, como su propio nombre indica,
abortar la ejecución del editor. Esto se hace con una única instrucción: llamando al
método Close() de la ventana principal del editor para cerrarla. Es decir, así:
menuFuente
99
menuCréditos
En este caso hemos usado una variante del método Show() diferente a la vista
anteriormente en la que como segundo parámetro pasamos la cadena de texto que será
utilizada como título de la ventana a mostrar.
Una vez escrito todo los códigos de respuesta indicados, nuestra aplicación ya estará
lista para funcionar. Podemos probarla seleccionando en el menú principal de Visual
Studio Depurar -> Inicio, siendo el aspecto de la misma durante su ejecución:
100
GENERAR AUTOMÁTICAMENTE LAS PROPIEDADES EN VISUAL STUDIO
El inconveniente de esta opción es que el color del texto se vuelve gris, y aunque
cambiemos la propiedad ForeColor, sigue manteniendo el mismo. La forma de
conseguir el efecto deseado es recurrir a la propiedad ReadOnly poniéndola a true. De
esta forma, podemos cambiar ambos colores en tiempo de ejecución.
CONTROL MONTHCALENDAR
Uno de los eventos de más utilidad en este componente es DateSelected, el cual salta
al seleccionar como mínimo una fecha. La selección de fechas viene dada por
SelectionStart y SelectionEnd. En el caso de que solo utilicemos una, con la primera
propiedad tendremos suficiente.
CONTROL DATETIMEPICKER
Este control consta además de un checkbox con el cual vamos a decidir si queremos o
no seleccionar una fecha.
CONTROL FLOWLAYOUTPANEL
101
la siguiente. También podemos recortar el control con el fin de ajustar sus
componentes en el interior.
Supongamos que nuestra aplicación consta de varios formularios, los cuales se van a
instanciar en función de una serie de condicionantes. Es decir, se abrirá uno u otro
formulario según se necesite y vamos a imaginar que todos esos nombres de form los
tenemos almacenados en un tipo de datos cadena dentro de una tabla en una BD.
102
FORMULARIOS CON FORMA
En .NET podemos definir un formulario con una forma dada por una imagen, es decir,
no tiene porque tener la forma rectangular habitual. Esto se consigue colocando una
imagen de fondo y definiendo uno de sus colores como transparente mediante la
propiedad Transparency Key. El código para realizar esto sería el siguiente, el cual
colocaríamos en el evento Load del formulario:
Si además queremos poder cerrar el formulario, tan solo hemos de agregar un botón o
cualquier otro control que nos lo permita y programar en él la salida del formulario.
TABCONTROL
103
TabPage.Parent = TabControl;
Por ejemplo, si queremos que un control se ajuste al ancho del formulario, pero
permanezca en la misma posición de izquierda y arriba, sólo tenemos que darle el
valor Top, Left y Right a la propiedad Anchor del control en cuestión.
Por otro lado, si queremos que un control se ajuste al tamaño completo del formulario,
pero que la separación con respecto a los controles que estén a su izquierda, derecha,
arriba y abajo permanezcan inalterables, asignaremos los cuatro valores de la
propiedad Anchor: Top, Left, Right y Botton.
Es decir, podemos hacer que un control se ajuste de forma automática al tamaño del
formulario y a la posición (o separación) con respecto a otros controles. Todo esto lo
veremos a continuación.
Una característica que tienen los formularios es poder indicarle el tamaño mínimo y
máximo que queremos que tenga. Esto se consigue mediante las propiedades:
MinimumSize y MaximumSize. No es necesario indicarle el tamaño a las dos
propiedades, por ejemplo si lo que queremos es que nuestro formulario no pueda ser
menor de 300x200, asignaremos el valor 300 a MinimumSize.Width y 200 a
MinimumSize.Height.
Aquí tenemos dos capturas del formulario de prueba, una con el tamaño asignado en
tiempo de diseño y el otro al cambiar el tamaño:
104
Tamaño original.
Estos son los valores asignados a Anchor de cada uno de los controles del formulario:
(por defecto, están asignados los valores Top y Left, por tanto no es necesario asignar
estos valores a los controles que queremos que estén anclados sólo en esas dos
posiciones, tal es el caso de la etiqueta que muestra el texto Fichero:)
105
La propiedad Anchor de la ventana de propiedades.
Los iconos en la barra de tarea de Windows (al lado del reloj), se suelen usar para
aplicaciones que normalmente no están siempre visibles (o mostradas), por tanto los
formularios de los ejemplos aquí mostrados se "ocultarán" al minimizarlos, de forma
que sólo se queden en la barra de tareas (junto al reloj) cuando estén minimizados.
/*
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
106
namespace NotifyIcon2CS
{
///
/// Summary description for Form1.
///
public class Form1 : System.Windows.Forms.Form
{
// Se declaran como variables los objetos NotifyIcon y el ContextMenu
NotifyIcon NotifyIcon1 = new NotifyIcon();
ContextMenu ContextMenu1 = new ContextMenu();
107
// Asignar los valores para el NotifyIcon
NotifyIcon1.Icon = this.Icon;
NotifyIcon1.ContextMenu = this.ContextMenu1;
NotifyIcon1.Text = Application.ProductName;
NotifyIcon1.Visible = true;
// Asignamos los otros eventos al formulario
this.Resize += new EventHandler(this.Form1_Resize);
this.Activated += new EventHandler(this.Form1_Activated);
this.Closing += new
System.ComponentModel.CancelEventHandler(this.Form1_Closing);
// Asignamos el evento DoubleClick del NotifyIcon
this.NotifyIcon1.DoubleClick += new EventHandler(this.Restaurar_Click);
}
private void Form1_Resize(object sender, System.EventArgs e)
{
// Cuando se minimice, ocultarla, se quedará disponible en la barra de
tareas
if( this.WindowState == FormWindowState.Minimized )
this.Visible = false;
}
// la declaramos fuera de la función, para que mantenga su valor
bool PrimeraVez = true;
//
private void Form1_Activated(object sender, System.EventArgs e)
{
if( PrimeraVez )
{
PrimeraVez = false;
Visible = false;
}
}
Los parámetros de la línea de comandos, son los que se especifican como argumentos
de un ejecutable, por ejemplo:
Cada parámetro se separa mediante un espacio, (la barra o el signo menos es opcional)
108
- La propiedad CommandLine devuelve TODA la línea de comando, incluyendo el
nombre del ejecutable.
Hay que tener en cuenta que se entiende que cada parámetro está separado del anterior
mediante un espacio, aunque esto último no es aplicable al nombre del ejecutable, el
cual puede contener espacios tanto en el nombre del ejecutable como en el path del
mismo.
Para asignar todos los parámetros a un TextBox (o a un array del tipo String):
LIMPIADO DE PANTALLA EN C#
using System;
using System.Runtime.InteropServices;
namespace nsLimpiarPantalla
{
public class LimpiarConsola
{
private const int STD_OUTPUT_HANDLE = -11;
private const byte EMPTY = 32;
109
[StructLayout(LayoutKind.Sequential)]
struct COORD
{
public short x;
public short y;
}
[StructLayout(LayoutKind.Sequential)]
struct SMALL_RECT
{
public short Left;
public short Top;
public short Right;
public short Bottom;
}
[StructLayout(LayoutKind.Sequential)]
struct CONSOLE_SCREEN_BUFFER_INFO
{
public COORD dwSize;
public COORD dwCursorPosition;
public int wAttributes;
public SMALL_RECT srWindow;
public COORD dwMaximumWindowSize;
}
[DllImport("kernel32.dll", EntryPoint="GetStdHandle",
SetLastError=true, CharSet=CharSet.Auto,
CallingConvention=CallingConvention.StdCall)]
private static extern int GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll",
EntryPoint="FillConsoleOutputCharacter", SetLastError=true,
CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
private static extern int FillConsoleOutputCharacter(int
hConsoleOutput, byte cCharacter, int nLength, COORD dwWriteCoord, ref int
lpNumberOfCharsWritten);
[DllImport("kernel32.dll",
EntryPoint="GetConsoleScreenBufferInfo", SetLastError=true,
CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
private static extern int GetConsoleScreenBufferInfo(int
hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);
[DllImport("kernel32.dll", EntryPoint="SetConsoleCursorPosition",
SetLastError=true, CharSet=CharSet.Auto,
CallingConvention=CallingConvention.StdCall)]
private static extern int SetConsoleCursorPosition(int
hConsoleOutput, COORD dwCursorPosition);
public LimpiarConsola()
{
hConsoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
}
110
CONSOLE_SCREEN_BUFFER_INFO strConsoleInfo = new
CONSOLE_SCREEN_BUFFER_INFO();
COORD Home;
Home.x = Home.y = 0;
GetConsoleScreenBufferInfo(hConsoleHandle, ref strConsoleInfo);
FillConsoleOutputCharacter(hConsoleHandle, EMPTY,
strConsoleInfo.dwSize.x * strConsoleInfo.dwSize.y, Home, ref
hWrittenChars);
SetConsoleCursorPosition(hConsoleHandle, Home);
}
}
}
Para poder usar esta clase, debemos añadirla a nuestro proyecto mediante el
namespace nsLimpiarPantalla:
using nsLimpiarPantalla;
111
COMO VALIDAR SI UNA URL ES CORRECTA
using System.Net;
private bool ValidarURL(string url)
{
try
{
HttpWebRequest HttpWReq =
(HttpWebRequest)WebRequest.Create(url);
HttpWebResponse HttpWResp =(HttpWebResponse)
HttpWReq.GetResponse();
HttpWResp.Close();
return true;
}
catch (Exception ex)
{
string duh = ex.Message;
return false;
}
}
FUNCIONES GENÉRICAS
112
La línea void intercambia<X>(ref X a,ref X b) le indica al compilador que se está
comenzando una definición genérica. X es el tipo de datos de los valores que serán
intercambiados. En main se llama a la función intercambia() utilizando dos tipos
diferentes de datos: enteros y flotantes. Puesto que es una función genérica, el
compilador crea automáticamente dos versiones de ella – una que intercambia valores
enteros y otra con valores flotantes. Podemos definir más de un tipo genérico siempre
que los separemos por comas. Veamos otro ejemplo:
Las funciones genéricas son equivalentes a las sobrecargadas excepto que son más
restrictivas. Cuando las funciones se sobrecargan, dentro de ellas pueden ocurrir
acciones diferentes, pero una genérica debe realizar la misma función general para
todas las versiones. Aunque una función plantilla puede sobrecargarse a sí misma, si
es necesario también es posible sobrecargarla de forma explícita. En este caso, la
función sobrecargada redefine (“u oculta”) la función genérica relativa a esa versión
específica. Veamos una versión diferente del primer ejemplo:
113
Cuando llamamos a intercambia(i,j) se invoca a la versión sobrecargada explícitamente
en el programa. Esta sobrecarga permite confeccionar una versión de una función
genérica para que se adapte a una situación especial. Sin embargo, en general, si
necesitamos tener diferentes versiones para una función para diferentes tipos de datos,
deberíamos usar funciones sobrecargadas y no plantillas.
CLASES GENÉRICAS
Además de las funciones genéricas podemos definir una clase genérica, esto es, una
clase que define todos los algoritmos usados por ella, pero el tipo de datos que,
realmente, va a ser manipulado se especificará como un parámetro cuando se creen
los objetos de esa clase.
Son útiles cuando una clase contiene una lógica generalizable. Por ejemplo, el mismo
algoritmo que almacena una cola de enteros funciona también para una cola de
caracteres. Lo mismo podríamos decir con una lista enlazada. El compilador generará
automáticamente el tipo adecuado de objeto en función del tipo que se especifique
cuando se crea el objeto.
<T> es el nombre del tipo de resguardo o parámetro de tipo que se especificará cuando
se instancie la clase. Si es necesario, puede definirse más de un tipo de datos genéricos
usando una lista separada por comas.
Una vez que se ha creado una clase genérica se puede crear una instancia específica de
esa clase usando la siguiente forma general:
Tipo es el nombre del tipo de datos sobre el que trabajará la clase. Los miembros de
una clase genérica son, por sí mismos, genéricos.
Una clase plantilla puede tener más de un tipo genérico de datos (parámetro de tipo).
Simplemente hay que declarar todos los tipos de datos requeridos por la clase en una
lista separada por comas.
114
Veamos el siguiente ejemplo:
class miclase<Tipo1,Tipo2>
{
Tipo1 i;
Tipo2 j;
public miclase(Tipo1 a, Tipo2 b)
{
i=a;
j=b;
}
public void Mostrar()
{
Console.WriteLine("{0} {1}\n",i,j);
}
}
class MainClass
{
public static void Main(string[] args)
{
miclase<int, double> ob1= new miclase<int,
double>(10,0.23);
miclase<char, string> ob2= new miclase<char,
string>('X',"Esto es una prueba");
ob1.Mostrar();
ob2.Mostrar();
string fin= Console.ReadLine();
return;
}
}
}
}
PARÁMETROS DE TIPO
Cuando una clase genérica es instanciada, lo hace con un parámetro de tipo, enlazando
la implementación de clase incompleta y sin forma con un tipo real, creando una
instancia de clase genérica concreta, como se muestra en el siguiente ejemplo:
Aunque podemos nombrar nuestros parámetros de tipo como deseemos, por convenio
se suele definir la letra “T” como el nombre estándar para el primer parámetro de tipo.
Si nuestra clase requiere parámetros adicionales, el convenio dicta que continuemos
con las letras del alfabeto siguientes: “U”, “V”… Si llegamos a la “Z” mediante
parámetros de tipo, tendríamos un problema de diseño tan grande que posiblemente
los genéricos por sí solos no lo podrían solucionar.
115
Para situaciones en las cuales el propósito del parámetro de tipo no es suficientemente
explicativo por si mismo, Microsoft recomienda usar un nombre descriptivo para el
parámetro de tipo predecido de la T mayúscula, como se muestra en el siguiente
código:
Y para instanciar una clase que requiere múltiples parámetros de tipo, separaremos
los tipos con comas como sigue:
Para evitar esto, debemos usar restricciones en los parámetros de tipo. Estas
restricciones especifican ciertos requerimientos que el parámetro de tipo ha de
cumplir. Por ejemplo, podemos especificar que cualquier tipo pasado a nuestra clase
genérica deba implementar un constructor (sin parámetros) por defecto.
Todas las restricciones en las clases genéricas se indican con la palabra clave “where”
y podemos usar múltiples restricciones en el mismo parámetro separándolas con una
coma. Una muestra de cómo podemos especificar una restricción se puede ver en el
siguiente código:
116
La restricción precedente indica que el parámetro de tipo especificado por
TItemDetalle debe implementar un constructor por defecto. Las siguientes líneas
muestran otras formas de especificar restricciones:
117
La siguiente tabla muestra todas las restricciones que podemos aplicar a los
parámetros de tipo genéricos.
Restricción
Descripción
where T:struct
where T:class
Indica que T debe ser un tipo referencia, incluyendo cualquier clase, delegado
o interface.
where T:new()
where T: (interface)
118
Un patrón de diseño muy utilizado en la POO es el patrón factory o fábrica. En este
patrón, el código solicita nuevas instancias de un tipo dado desde esa fábrica de tipos
más que instanciarlos directamente. Esto da a la fábrica control completo sobre el
proceso de instanciación, además de la posibilidad de hacer cosas como cachés de tipos
preinstanciaos, ejecutar llamadas a Remoting o servicios Web para obtener datos de
soporte, y mucho más.
Para ver como podemos hacer un uso rápido de genéricos y restricciones de parámetro
de tipo, echemos un vistazo al siguiente código, el cual crea una clase fábrica que
puede servir tanto al tipo Cliente como al tipo ClienteEspecial:
El código precedente restringe al tipo TCliente requiriendo que sea o derive del tipo
Cliente además de implementar un constructor por defecto. Ya que el argumento
TCliente ha sido restringido con new(), el código puede explícitamente crear una
nueva instancia de ese tipo. A través de la potencia de los genéricos, este nuevo
ejemplar no es otra cosa que el tipo exacto especificado. No se requiere entonces
ninguna conversión de tipos en la clase fábrica, lo cual es un gran beneficio para los
desarrolladores OOP.
DELEGADOS GENÉRICOS
Otras de las entidades que pueden ser definidas de forma genérica son los delegados.
Supongamos que se desea definir un delegado que permita llamar a cualquier método
que reciba un único parámetro de un tipo T y no devuelva nada. La siguiente sentencia
cumple con ese objetivo:
119
A partir de la declaración anterior se pueden crear y utilizar objetos de tipo delegado:
Para facilitar la aplicación de una misma acción a todos los elementos de un array, la
clase System.Array ofrece un método ForEach<T>() que recibe, además del array sobre
el que actuar, una instancia de delegado de tipo Action<T>. Eso nos permite escribir
código como el siguiente:
120
Se podrá comprobar que al ejecutar el bucle a continuación, los elementos del array se
imprimen en un orden diferente cada vez:
Representa a un delegado que opera sobre un objeto del tipo T y devuelve un booleano
dependiendo de si cumple o no una condición. System.Array ofrece varios métodos
que permiten recorrer los elementos de un array buscando cuáles de ellos cumplen con
una condición llegada “desde fuera”:
INTERFACES GENÉRICOS
Un interface genérico funciona de una forma muy similar a como lo hace una clase
genérica. El interface en sí mismo acepta uno o más parámetros de tipo de a misma
forma que lo hace una clase genérica. En vez de utilizar el parámetro de tipo en la
definición de la clase, el parámetro de tipo es entonces usado en las sentencias de
declaración de miembros de las que consta el interface.
using System;
using System.Collections.Generic;
using System.Text;
namespace Generics1
{
public interface IManejadorDatos<TTipoFila> where TTipoFila : new()
{
void AgregarRegistro (TTipoFila registro);
void BorrarRegistro(TTipoFila registro);
void ActualizarRegistro(TTipoFila registro);
void SeleccionarRegistro(TTipoFila registro);
List<TTipoFila> SeleccionaRegistros();
}
}
Lo que hace el anterior interface es indicar que cualquier clase que implemente
IManejadorDatos<TTipoFila> debe proporcionar los métodos estándar Agregar,
Borrar, Actualizar y Seleccionar típicamente asociados con objetos de acceso a datos
121
independientemente del tipo de datos subyacente que sea almacenado. Este es un
interfaz extremadamente útil que nos va a permitir una gran flexibilidad.
COLECCIONES GENÉRICAS
Clase Dictionary
Esta clase está diseñada para almacenar valores asociados a un valor de búsqueda.
Tradicionalmentese ha usado esta clase para almacenar valores con cadenas, tales
como nombres, palabras clave o GUIDs, pero además ahora podemos usar un objeto
de cualquier tipo como clave y otro objeto de cualquier tipo como valor. Ahora
podemos especificar la clave y el tipo del valor al mismo tiempo de la creación del
objeto usando parámetros de tipo, como se muestra en el siguiente fragmento de
código:
// Diccionario de Prueba
Dictionary<string, int> diasMes = new Dictionary<string, int>();
diasMes["Enero"] = 31;
diasMes["Febrero"] = 28;
diasMes["Marzo"] = 31;
Console.WriteLine("Marzo tiene " + diasMes["Marzo"].ToString() + " días.");
Este ejemplo crea y manipula una clase Dictionary<> en la que todas las claves son
cadenas, y todos los valores son números, almacenando el número de dias en cada
mes.
Clase List
122
El código precedente produce la siguiente salida:
Conviene fijarse en el hecho de que ahora podemos escribir código como el siguiente
sin tener que escribir ninguna de nuestras propias clases para ello:
listaClientes[0].Nombre
Antes, para obtener una lista como la mostrada en este ejemplo, los desarrolladores
solían pasar muchas horas creando sus propias clases de colección fuertemente
tipadas. Ahora podemos crearlas en tiempo de ejecución sin pérdida de rendimiento
y sin necesidad de hacer conversión de tipos de forma que el código es más sencillo y
reutilizable.
Clase Queue
La clase Queue no es más que una cola FIFO (First In First Out). Esta clase nos permite
especificar el tipo de datos de los elementos en una cola al momento de la
instanciación, como se muestra en el siguiente ejemplo:
Clase Stack
Al contrario que la clase Queue<>, la clase Stack<> es una colección LIFO (Last-in
First-out), es decir, una pila. El siguiente código muestra como colocar elementos en
una pila genérica y como obtenerlos. Para asegurarnos de saber cómo trabaja la pila,
podemos jugar con el cambio de orden en el que colocamos los ítems para ver los
resultados:
INFERENCIA DE TIPOS
Permite llamar a un método genérico sin especificar el tipo (de lo que se encarga el
compilador). A continuación, creamos una función genérica a la que llamamos
primero indicando el tipo, y luego usando inferencia de tipos.
123
static void FuncionGenerica<T>(T t)
{
Console.WriteLine(t.GetType().ToString());
}
FuncionGenerica<int>(5);
FuncionGenerica(5);
124
ESTRUCTURAS DE DATOS
Una interfaz para dicho tipo de árbol, podría venir definida así:
125
IMPLEMENTACIÓN DE ÁRBOLES DE BÚSQUEDA BINARIA MEDIANTE
ENLACES
Vamos a utilizar una clase BinaryTreeNode para representar un nodo del árbol. Cada
objeto de esta clase, mantiene una referencia al elemento almacenado en dicho nodo,
así como referencias a cada uno de los nodos hijos.
/// <summary>
/// Representa el nodo de un árbol binario
/// </summary>
/// <typeparam name="T">Tipo del dato a almacenar en el nodo</typeparam>
public class BinaryTreeNode<T>{
/// <summary>
/// Elemento a almacenar en el nodo
/// </summary>
private T element;
/// <summary>
/// Propiedad que recoge el elemento a almacenar en el nodo
/// </summary>
public T ElementT {
get { return element; }
set { element = value; }
}
// Hijos izquierdo y derecho del nodo
protected BinaryTreeNode<T> left, right;
// Crea un nuevo nodo del árbol con los datos especificados
public BinaryTreeNode(T Obj) {
element = Obj;
left = null;
right = null;
}
/// <summary>
/// Número de hijos del nodo
/// </summary>
public int NumChildren {
get
{
int children = 0;
if (left != null)
children = 1 + left.NumChildren;
if (right != null)
children = 1 + right.NumChildren;
return children;
}
}
/// <summary>
/// Hijo izquierdo del nodo
/// </summary>
public BinaryTreeNode<T> LeftNode{
get { return left; }
set { left = value; }
}
/// <summary>
/// Hijo derecho del nodo
/// </summary>
public BinaryTreeNode<T> RightNode {
get { return right; }
set { right = value; }
}
}
126
Nos basamos en una clase LinkedBinaryTree que implementa el interfaz IBinaryTree:
/// <summary>
/// Implementación de Arbol Binario enlazado
/// </summary>
/// <typeparam name="T">Tipo de datos de los elementos a almacenar en el árbol</typeparam>
public class LinkedBinaryTree<T> : IBinaryTree<T>
{
/// <summary>
/// Variable privada que recoge el número de nodos del árbol
/// </summary>
protected int count;
/// <summary>
/// Nodo raíz del árbol
/// </summary>
protected BinaryTreeNode<T> root;
/// <summary>
/// Contructor sin argumentos
/// </summary>
public LinkedBinaryTree()
{
count = 0;
root = null;
}
/// <summary>
/// Constructor que recoge un elemento como argumento
/// </summary>
/// <param name="Element">Elemento de tipo <b>T</b> que es raíz del árbol</param>
public LinkedBinaryTree(T Element)
{
count = 1;
root = new BinaryTreeNode<T>(Element);
}
/// <summary>
/// Constructor que recoge un elemento raíz y dos árboles hijos (izquierdo y derecho)
/// </summary>
/// <param name="Element">Elemento de tipo <b>T</b> que es raíz del árbol</param>
/// <param name="LeftSubtree">Árbol hijo izquierdo</param>
/// <param name="RightSubtree">Árbol hijo derecho</param>
public LinkedBinaryTree(T Element, LinkedBinaryTree<T> LeftSubtree, LinkedBinaryTree<T>
RightSubtree)
{
root = new BinaryTreeNode<T>(Element);
count = 1;
if (LeftSubtree != null)
{
count += LeftSubtree.Size;
root.LeftNode = LeftSubtree.root;
}
else
root.LeftNode = null;
if (RightSubtree != null)
{
count += RightSubtree.Size;
root.RightNode = RightSubtree.root;
}
else
root.RightNode = null;
127
}
/// <summary>
/// Número de nodos del árbol
/// </summary>
public int Size
{
get { return count; }
}
/// <summary>
/// Elimina el árbol hijo izquierdo
/// </summary>
public void RemoveLeftSubtree()
{
if (root.LeftNode != null)
count -= root.LeftNode.NumChildren - 1;
root.LeftNode = null;
}
/// <summary>
/// Elimina el árbol hijo derecho
/// </summary>
public void RemoveRightSubtree()
{
if (root.RightNode != null)
count -= root.LeftNode.NumChildren - 1;
root.RightNode = null;
}
/// <summary>
/// Elimina todos los elementos del árbol
/// </summary>
public void RemoveAllElements()
{
count = 0;
root = null;
}
/// <summary>
/// Busca un elemento de tipo <b>T</b> den el árbol
/// </summary>
/// <param name="TargetElement">Elemento a buscar en el árbol</param>
/// <returns>Devuelve un elemento de tipo <B>T</B> encontrado en el árbol. Sino, devuelve el valor por
defecto del tipo <B>T</B></returns>
public T Find(T TargetElement)
{
BinaryTreeNode<T> current = findAgain(TargetElement, root);
if (current == null)
return default(T);
return current.ElementT;
}
/// <summary>
/// Iterador para recorrer el árbol en InOrden
/// </summary>
/// <returns>Devuelve un iterador (IEnumerator) basado en el recorrido InOrden</returns>
public IEnumerator<T> IteratorInOrder()
{
List<T> templist = new List<T>();
128
inorder(root, templist);
return templist.GetEnumerator();
}
public bool Contains(T TargetElement)
{
throw new NotImplementedException("Método no implementado");
}
/// <summary>
/// Indica si el árbol está vacío o no
/// </summary>
/// <returns>Devuelve <b>true</b> si está vacío, <b>false</b> en caso contrario</returns>
public bool IsEmpty()
{
return (root == null);
}
public IEnumerator<T> IteratorPreOrder()
{
throw new NotImplementedException("Método no implementado");
}
// Realiza un recorrido en PostOrden del árbol binario comenzando por la raíz
public IEnumerator<T> IteratorPostOrder()
{
throw new NotImplementedException("Método no implementado");
}
// Realiza un recorrido por niveles del árbol binario usando una cola
public IEnumerator<T> IteratorLevelOrder() {
throw new NotImplementedException("Método no implementado");
}
/// <summary>
/// Método privado para recorrido InOrden del árbol
/// </summary>
/// <param name="Node">Nodo de partida</param>
/// <param name="TempList">Lista temporal de nodos</param>
protected void inorder(BinaryTreeNode<T> Node, List<T> TempList) {
if (Node != null)
{
inorder(Node.LeftNode, TempList);
TempList.Add(Node.ElementT);
inorder(Node.RightNode, TempList);
}
}
/// <summary>
/// Método privado recursivo para búsqueda de elemento en nodo
/// </summary>
/// <param name="TargetElement">Elemento a buscar</param>
/// <param name="Next">Próximo nodo a buscar</param>
/// <returns>Devuelve el nodo</returns>
private BinaryTreeNode<T> findAgain(T TargetElement, BinaryTreeNode<T> Next) {
if (Next == null)
return null;
if (Next.ElementT.Equals(TargetElement))
return Next;
BinaryTreeNode<T> temp = findAgain(TargetElement, Next.LeftNode);
if (temp == null)
temp = findAgain(TargetElement, Next.RightNode);
return temp;
}
}
129
Nuestra clase LinkedBinarySearchTree ofrece dos constructores: uno para crear un
árbol LinkedBinarySearchTree vacío y otro para crear un árbol con un elemento
concreto como raíz. Ambos constructores únicamente hacen referencia a los
constructores equivalentes de la superclase:
/// <summary>
/// Crea un árbol de búsqueda binaria vacío
/// </summary>
public LinkedBinarySearchTree() : base(){}
/// <summary>
/// Crea un árbol de búsqueda binaria utilizando un elemento especificado como raíz
/// </summary>
/// <param name="Element"></param>
public LinkedBinarySearchTree(T Element) : base(Element){ }
130
Por contra, RemoveElement elimina un elemento especificado de tipo IComparable de
un árbol de búsqueda binaria, o genera una NullReferenceException en caso de que
no exista. A diferencia de un árbol binario normal, no podemos simplemente eliminar
un nodo haciendo que la referencia puentee al nodo que hay que eliminar. En lugar de
ello, debemos “promover” a otro nodo para que sustituya al que está siendo eliminado.
El método protegido denominado replacement devuelve una referencia a un nodo que
sustituirá al que se ha especificado para la eliminación. Existen tres casos distintos para
la selección del nodo de sustitución:
/// <summary>
/// Elimina del árbol de búsqueda binaria el primer elemento que se corresponda
/// con el elemento objetivo especificado y devuelve una referencia al mismo.
/// Genera NullReferenceException si no se encuentra el elemento objetivo
/// especificado dentro del árbol de búsqueda binario
/// </summary>
/// <param name="TargetElement">Elemento a eliminar</param>
/// <returns>Devuelve una referencia al elemento eliminado</returns>
public T RemoveElement(T TargetElement)
{
T result = default(T);
if (IsEmpty())
if (((IComparable)TargetElement).Equals(root.ElementT))
{
result = root.ElementT;
root = replacement(root);
count--;
}
else
{
BinaryTreeNode<T> current, parent = root;
bool found = false;
if (((IComparable)TargetElement).CompareTo(root.ElementT) < 0)
current = root.LeftNode;
else
current = root.RightNode;
while (current != null && !found)
{
if (TargetElement.Equals(current.ElementT))
{
found = true;
count--;
result = current.ElementT;
if (current == parent.LeftNode)
{
parent.LeftNode = replacement(current);
}
else
{
parent.RightNode = replacement(current);
131
}
}
else
{
parent = current;
if (((IComparable)TargetElement).CompareTo(current.ElementT) < 0)
current = current.LeftNode;
else
current = current.RightNode;
}
}
if (!found)
throw new NullReferenceException("No se ha encontrado el elemento indicado en el árbol
binario");
}
return result;
}
/// <summary>
/// Reemplaza un nodo con una referencia a otro
/// </summary>
/// <param name="node">Nodo a reemplazar</param>
/// <returns>Devuelve una referencia a un nodo que se ha especificado en node</returns>
protected BinaryTreeNode<T> replacement(BinaryTreeNode<T> node)
{
BinaryTreeNode<T> result = null;
if ((node.LeftNode == null) && (node.RightNode == null))
result = null;
else if ((node.LeftNode != null) && (node.RightNode == null))
result = node.LeftNode;
else if ((node.LeftNode==null) && (node.RightNode !=null))
result = node.RightNode;
else
{
BinaryTreeNode<T> current = node.RightNode;
BinaryTreeNode<T> parent = node;
while (current.LeftNode != null)
{
parent = current;
current = current.LeftNode;
}
if (node.RightNode == current)
current.LeftNode = node.LeftNode;
else
{
parent.LeftNode = current.RightNode;
current.RightNode = node.RightNode;
current.LeftNode = node.LeftNode;
}
result = current;
}
return result;
132
/// <summary>
/// Elimina del árbol de búsqueda binaria los elementos que se
/// correspondan con el elemento objetivo especificado
/// </summary>
/// <param name="TargetElement"></param>
public void RemoveAllOcurrences(T TargetElement)
{
RemoveElement(TargetElement);
while (Contains((T)TargetElement))
RemoveElement(TargetElement);
El código completo:
/// <summary>
/// Árbol de búsqueda binaria
/// </summary>
/// <typeparam name="T">Tipo de cada uno de los elementos que va a recoger el árbol</typeparam>
public class LinkedBinarySearchTree<T> : LinkedBinaryTree<T>, IBinarySearchTree<T> where T :
IComparable
{
/// <summary>
/// Crea un árbol de búsqueda binaria vacío
/// </summary>
public LinkedBinarySearchTree() : base()
{
/// <summary>
/// Crea un árbol de búsqueda binaria utilizando un elemento especificado como raíz
/// </summary>
/// <param name="Element"></param>
public LinkedBinarySearchTree(T Element) : base(Element)
{
/// <summary>
/// Añade el elemento especificado al árbol de búsqueda binaria seleccionando
/// la posición apropiada de acuerdo con su valor clave. Los elementos iguales
/// se añaden a la derecha
/// </summary>
/// <param name="Element">Elemento a</param>
public void AddElement(T Element)
{
BinaryTreeNode<T> temp = new BinaryTreeNode<T>(Element);
IComparable<T> comparableElement = (IComparable<T>)Element;
if (IsEmpty())
root = temp;
else
{
BinaryTreeNode<T> current = root;
bool added = false;
while (!added)
{
if (comparableElement.CompareTo(current.ElementT) < 0)
if (current.LeftNode == null)
{
133
current.LeftNode = temp;
added = true;
}
else
current = current.LeftNode;
else
if (current.RightNode == null)
{
current.RightNode = temp;
added = true;
}
else
current = current.RightNode;
}
}
count++;
}
/// <summary>
/// Elimina del árbol de búsqueda binaria el primer elemento que se corresponda
/// con el elemento objetivo especificado y devuelve una referencia al mismo.
/// Genera NullReferenceException si no se encuentra el elemento objetivo
/// especificado dentro del árbol de búsqueda binario
/// </summary>
/// <param name="TargetElement">Elemento a eliminar</param>
/// <returns>Devuelve una referencia al elemento eliminado</returns>
public T RemoveElement(T TargetElement)
{
T result = default(T);
if (IsEmpty())
if (((IComparable)TargetElement).Equals(root.ElementT))
{
result = root.ElementT;
root = replacement(root);
count--;
}
else
{
BinaryTreeNode<T> current, parent = root;
bool found = false;
if (((IComparable)TargetElement).CompareTo(root.ElementT) < 0)
current = root.LeftNode;
else
current = root.RightNode;
while (current != null && !found)
{
if (TargetElement.Equals(current.ElementT))
{
found = true;
count--;
result = current.ElementT;
if (current == parent.LeftNode)
{
parent.LeftNode = replacement(current);
}
else
{
parent.RightNode = replacement(current);
}
}
134
else
{
parent = current;
if (((IComparable)TargetElement).CompareTo(current.ElementT) < 0)
current = current.LeftNode;
else
current = current.RightNode;
}
}
if (!found)
throw new NullReferenceException("No se ha encontrado el elemento indicado en el árbol
binario");
}
return result;
}
/// <summary>
/// Reemplaza un nodo con una referencia a otro
/// </summary>
/// <param name="node">Nodo a reemplazar</param>
/// <returns>Devuelve una referencia a un nodo que se ha especificado en node</returns>
protected BinaryTreeNode<T> replacement(BinaryTreeNode<T> node)
{
BinaryTreeNode<T> result = null;
if ((node.LeftNode == null) && (node.RightNode == null))
result = null;
else if ((node.LeftNode != null) && (node.RightNode == null))
result = node.LeftNode;
else if ((node.LeftNode==null) && (node.RightNode !=null))
result = node.RightNode;
else
{
BinaryTreeNode<T> current = node.RightNode;
BinaryTreeNode<T> parent = node;
while (current.LeftNode != null)
{
parent = current;
current = current.LeftNode;
}
if (node.RightNode == current)
current.LeftNode = node.LeftNode;
else
{
parent.LeftNode = current.RightNode;
current.RightNode = node.RightNode;
current.LeftNode = node.LeftNode;
}
result = current;
}
return result;
/// <summary>
/// Elimina del árbol de búsqueda binaria los elementos que se
/// correspondan con el elemento objetivo especificado
/// </summary>
135
/// <param name="TargetElement"></param>
public void RemoveAllOcurrences(T TargetElement)
{
RemoveElement(TargetElement);
while (Contains((T)TargetElement))
RemoveElement(TargetElement);
public T RemoveMin()
{
throw new NotImplementedException();
}
public T RemoveMax()
{
throw new NotImplementedException();
}
public T FindMin()
{
throw new NotImplementedException();
}
public T FindMax()
{
throw new NotImplementedException();
}
#endregion
}
MULTIPROCESO
Con la propiedad IsAlive podemos saber si un proceso aún está en ejecución. Con el
método Join() de la clase Thread unimos los subprocesos para que esperen a su
136
finalización y así se finalice el hilo principal (nuestro programa). Veamos el siguiente
listado:
using System;
using System.Threading;
class Hilos
{
public int contador;
public Thread hilo;
Como se puede observar, usamos el constructor del thread y el delegado con el nombre
del método a ejecutar por el subproceso, en este caso, el método ejecución.
Thread.Sleep() duerme la ejecución durante el tiempo expresado entre paréntesis. En
137
el programa principal, creamos los tres subprocesos y esperamos a que finalicen
mediante la comprobación realizada mediante IsAlive. De esta forma es sencillo
comprobar y sincronizar los subprocesos para esperar a que todos terminen. En este
caso usamos un simple OR cortocircuitado de forma que mientras haya alguno de los
subprocesos activos no finalice el programa.
h1.hilo.Join();
h2.hilo.Join();
h3.hilo.Join();
El método Join() puede aceptar como parámetro el tiempo máximo que debe esperar
hasta que termine el subproceso “unido”.
FINALIZAR UN SUBPROCESO
138
Se puede suprimir esta excepción directamente con el método Thread.ResetAbort()
como puede verse en el siguiente ejemplo:
using System.Threading;
namespace Thread1
{
class Program
{
static void Main(string[] args)
{
Thread.CurrentThread.Name = "PRINCIPAL";
ImprimirMensaje("Aplicación iniciada");
Thread worker = new Thread(new ThreadStart(DoWork));
worker.Name = "TRABAJADOR";
worker.Start();
Console.WriteLine("Presione Enter para anular el subproceso");
Console.ReadLine();
worker.Abort();
Console.WriteLine("Señal de anulación de subproceso enviada");
Console.ReadLine();
}
catch (Exception e)
{
ImprimirMensaje("Atrapada: " + e.ToString());
}
}
}
}
SUSPENDER UN SUBPROCESO
139
termine de ejecutarse para dar vez a otro subproceso, éste no continuará hasta que el
primero se haya reanudado.
El método Sleep hace que el subproceso repose. Con la indicación del intervalo en
milésimas de segundo, el subproceso dejará de ejecutar en esa línea durante la
duración especificada. También se puede pasar un valor 0 como el argumento que hará
que el subproceso se suspenda. Si se especifica System.Threading.Timeout.Infinite
como valor, el subproceso se bloqueará de forma indefinida.
BLOQUEAR UN SUBPROCESO
El método Join sirve como modo de bloquear el subproceso actual hasta que el
subproceso especificado se haya completado. Esto permite que el subproceso espere a
la finalización de otro método. De ahí proviene el término Join, cuando el subproceso
actual espera a otro para “alcanzarlo”. El listado siguiente ilustra el uso del método
Join:
using System.Threading;
namespace Thread1
{
class Program
{
static void Main(string[] args)
{
Thread worker = new Thread(new ThreadStart(DoWork));
worker.Start();
// realiza el método Join para bloquear
// este subproceso hasta que el trabajador se haya
// completado
worker.Join();
Console.WriteLine("Esta línea no se ejecutará hasta que 'worker' haya terminado");
Console.ReadLine();
140
El uso del método Join reemplazaría a la utilización de un bucle while que esperaba
hasta que la propiedad IsAlive del subproceso de ejecución se evaluaba como false.
Esta solución con join es más eficaz y segura.
ADO.NET
TextBox1.DataBindings.Add("Text",datasource,"nombredelcampo");
En este caso, asociamos al cuadro de texto TextBox1 el contenido del campo dado por
datasource (puede ser entre otras cosas un datatable o un dataset). Text es la propiedad
a la cual asociamos el contenido del campo.
Para movernos por los registros, usamos la propiedad Position del objeto
BindingContext, y del elemento al que hagamos referencia (tabla o dataset). Por
ejemplo, si queremos ponernos en el primer campo de nuestra tabla:
this.BindingContext[tabla].Position=1;
this.BindingContext[dataset,”tabla”].Position=1:
this.BindingContext[tabla].Count;
141
Si necesitáramos saber que clave o claves ejercen como primarias en una BD, sería tan
simple como:
DataColumn[] ClavesPrimarias;
ClavesPrimarias = MiDataset.Tables[0].PrimaryKey;
cadena=ClavesPrimarias[0].ToString();
Esto nos sirve para obtener el índice 0, pero podríamos obtener el que quisiéramos.
Supongamos que tenemos creada una vista sobre una tabla con unos criterios de
ordenación y filtrado. En un momento dado podemos desear conocer el número de
fila en la tabla original partiendo de una fila de nuestra vista.
El proceso para ello es simple: consiste en declarar una datarow que haga referencia a
una de las filas de la vista. Utilizando el método IndexOf de la colección de Rows del
datatable obtenemos el índice de dicha fila en la tabla original:
DataRow dr;
int IndiceTabla;
dr = dv[indicedelavista].Row;
IndiceTabla=dt.Rows.IndexOf(dr);
ESTILOS EN DATAGRIDVIEW
142
Mediante la propiedad DataGridViewColumn.ValueType, podemos conocer el tipo de
datos que internamente manipula una columna determinada del control, lo cual nos
puede ser útilo si queremos utilizar un estilo distinto para las columnas del
DataGridView en función del tipo de datos con el que trabaja.
Supongamos que se desea cambiar el color de fondo de una fila en función de un valor
tomado en cualquiera de los campos que muestra. Para conseguir este efecto
recurrimos al evento CellFormatting del grid, dentro de cuyo código comprobamos el
valor de la celda que necesitamos. Si se cumple esa condición, se recorre la colección
de celdas de la fila actual y se cambian sus propiedades de estilo correspondientes.
Veamos como sería:
}
}
}
Este código cambia el color de las filas donde se cumpla la condición de que el campo
situación (alojado en la columna clmSituacion) sea igual a 4.
Mientras un combo está dibujándose y tomando los primeros valores de una base de
datos, su SelectedValue es “System.Data.DataRowView”. Es importante tener esto en
cuenta al programar los eventos de cambio de índice o item, sobre todo si se está
teniendo en cuenta para una consulta.
143
CONCURRENCIA
Sin embargo, bloquear un registro de esta forma y esperar una interacción del usuario
antes de bloquearlo tiene el inconveniente de que destroza la capacidad de
concurrencia en el servidor de base de datos. Si otro proceso tratase de acceder al
registro bloqueado, se detendría por un tiempo indefinido esperando el desbloqueo
del registro. Se puede poner un límite de tiempo de espera, pero con ésto sólo se logra
ocasionar un error en el programa que espera que se libere el registro.
Es más, SQL Server realiza, cuando lo considera oportuno, una operación llamada
“escalado de bloqueos” (lock scalation). Se produce cuando hay muchos registros
bloqueados en una tabla extendiéndose a una página de datos completa e incluso a
toda la tabla. Si esto sucede, no solo se bloquearán los procesos que traten de acceder
a nuestro registro, sino los que accedan a otros de la misma tabla aún cuando no
estuvieran en uso.
Para evitar estos problemas, se recurre a realizar el control de acceso al registro desde
la parte cliente. Existen dos enfoques alternativos: concurrencia optimista y
concurrencia pesimista.
Con la optimista, primero se leen los datos, se trabaja con ellos y al ir a grabarlos se
comprueba primero si lo que hay en la base de datos coincide con lo leido inicialmente.
De no ser así, se muestra un error al usuario o se corrige el valor de forma automática
si es posible.
144
Con la pesimista, se usa algún mecanismo para bloquear el acceso a los registros de
forma que nadie más pueda acceder a un registro ocupado por un usuario. Por
ejemplo, se añade al registro un campo booleano y se pone a true cuando se lee para
trabajar con él. Si mientras tanto alguien quiere modificarlo, se le muestra un mensaje
diciendo que no se puede. El mecanismo optimista tiene soporte directo en las
distintas tecnologías mediante DataSet y DataAdapter, LinQ to SQL o Entity
Framework. Todas ellas permiten traer datos a memoria, trabajar con ellos y lanzar
una excepción al grabar si los datos se han modificado mientras tanto en el servidor.
En cuanto al pesimista, volviendo a la idea del campo BIT, cuando leamos dicho
registro, consultamos su valor. Si es true, asumimos que el registro está en uso e
informamos debidamente al usuario. En caso contrario lo ponemos a true y
presentamos el registro en pantalla. Estas dos operaciones las haremos dentro de una
transacción para garantizar (mediante el correspondiente bloqueo en el servidor) que
dos procesos no leen y cambian simultáneamente el valor, cosa que frustraría el
objetivo de nuestro mecanismo. Lógicamente, al terminar de trabajar con dicho
registro, devolveremos a false el citado campo.
145
Sin embargo, este mecanismo tiene un inconveniente: si un usuario abandona el
trabajo de forma anómala sin haber liberado el registro, éste permanecerá bloqueado
y nadie podrá acceder a él. Para aliviar este problema, podemos tomar alguna de las
medidas a continuación.
En primer lugar, sustituir el campo BIT por un DATETIME. Cuando contiene NULL
significa que el registro no está bloqueado. Para bloquear el registro, se introduce en
ese campo la fecha y hora actual. Cuando alguien quiere acceder al registro y el
programa lo ve bloqueado, se compara la fecha y hora actual con la del bloqueo. Si la
diferencia es mayor que un umbral preestablecido (por ejemplo, 15 minutos) se
presupone que el usuario que tenía bloqueado el registro lo ha abandonado y se libera
automáticamente.
Otra opción es sustituir el campo BIT por el usuario que realizó el bloqueo. Cuando
un segundo usuario intenta acceder al registro, se le informa que está siendo editado
por … y se le da la opción de desbloquear el registro si el usuario al que se muestra
tiene los permisos para ello. Es útil en caso que el programa lo maneje un número
reducido de usuarios que pueden comunicarse fácilmente unos con otros.
Por último, si no se quiere llenar el registro original con campos adicionales, se pueden
ubicar éstos en una tabla auxiliar enlazada con la principal. En este caso, al liberar el
bloqueo basta con borrar el registro completo de la tabla de bloqueos en lugar de poner
NULL o false en campos auxiliares.
LINQ hace posible escribir una consulta sobre cualquier colección que implemente la
interfaz IEnumerable e IQueryable. Esto es, en definitiva casi cualquier colección
de .NET incluyendo string[], int[] y colecciones genéricas (List<T>).
using System;
using System.Linq;
public class Consulta1
{
public static void Main()
{
int[] numeros = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var pNums = from n in numeros
where n < 5
select n;
Console.WriteLine("Numeros < 5:");
foreach (var x in pNums)
{
Console.WriteLine(x);
}
Console.ReadLine();
}
}
146
En este ejemplo, usamos las expresiones de consulta sobre un array de enteros para
seleccionar los que son menores que 5. Lo más interesante es que con esta sintaxis
básica podemos realizar consultas sobre objetos de naturaleza bien distinta, como
pueden ser archivos XML o bases de datos.
y así terminar con una variable q que contiene un nombre y un teléfono sin tener que
diseñar una clase específica.
Las expresiones de LINQ se pueden usar para realizar consultas sobre bases de datos
relacionales sin tener que dejar la sintaxis de C#. Esta facilidad, llamada Dlinq nos
permite realizar consultas sobre una base de datos con la sintaxis genérica vista
anteriormente, con todas las ventajas que esto conlleva, de tipado fuerte o poder usar
las verificaciones que realiza el compilador. Esto se consigue usando tipos que
representan a la base de datos y que transforman las consultas de C# a SQL. En el
siguiente listado vemos un ejemplo de esto. En este caso, el tipo DataContext se
encarga de traducir las consultas dentro de C# a consultas SQL sobre la base de datos:
using System;
using System.Linq;
using System.Data.Linq.Mapping;
using System.Data.Linq;
// Esta clase almacena una tabla SQL
[Table(Name="SQL_CLIENTES")]
public class Cliente
{
[Column(DbType = "smallint not null", IsPrimaryKey = true)]
public short COD_CLIENTE;
[Column(DbType = "varchar(100) not null")]
public string NOMBRE_CLIENTE;
[Column(DbType="char(9) not null")]
public string CIF;
}
public class DlinqEjemplo
{
public static void Main()
{
DataContext db = new DataContext(CadenaConexion");
Table<Cliente> Clientes = db.GetTable<Cliente>();
var consulta =
from c in Clientes
where c.CIF == "XXXXXXXXX"
select c;
foreach (var c in consulta)
Console.WriteLine("Nombre = {0}, CIF={1}",
c.NOMBRE_CLIENTE, c.CIF);
Console.ReadLine();
}
}
147
Supongamos que tenemos un archivo XML llamado agenda.xml con el siguiente
contenido:
La extensión que nos permite hacer consultas con XML es XLinq e introduce varios
tipos para representar archivos XML. En nuestro ejemplo, probaremos con
XDocument y haremos una consulta sobre este archivo para buscar los contactos cuya
categoría sea igual a “trabajo”:
using System.Xml.Linq;
XDocument doc = new XDocument("Agenda.xml");
var contactos = from c in doc.Descendants("contacto")
where c.Attribute("categoria").ToString() ==
"trabajo"
select c;
Las consultas, al igual que el bucle foreach se pueden realizar sobre cualquier objeto
que implemente IEnumerable. Por ejemplo, podríamos importar un ensamblado y
hacer una consulta sobre sus métodos para seleccionar aquellos que devuelvan un
entero.
148
SELECCIONAR Y CRUZAR INFORMACIÓN CON DATOS (LINQ)
new{IDCliente=1,nombre="pepe",apellido="gonzalez",Pais="España",edad=
21},
new{IDCliente=2,nombre="juan",apellido="rodriguez",Pais="España",edad
=33},
new{IDCliente=3,nombre="andrea",apellido="piera",Pais="Uruguay",edad=
35}
};
var ordenes = new[] {
new{IDCliente=1,ordenNumero=23,monto=400},
new{IDCliente=2,ordenNumero=24,monto=1},
new{IDCliente=2,ordenNumero=25,monto=132},
new{IDCliente=3,ordenNumero=80,monto=850},
new{IDCliente=1,ordenNumero=81,monto=14}
};
La expresión es similar a la que usaríamos con SQL pero utilizando la palabra new
para indicar más de un campo de retorno. Si deseamos ahora manipular los elementos
de la colección bastará con iterar sobre el contenido con un foreach:
En este ejemplo vamos a calcular el total de dinero a pagar por cliente y mostrar la
última orden emitida. Dos cosas a destacar:
1. La palabra clave “into” guarda el resultado del cruce en una variable, a la que
es posible hacer referencia más adelante
2. La palabra clave “let” realiza algo parecido pero con las expresiones que están
después del signo igual
149
AGRUPAR ELEMENTOS CON LINQ
La misma consulta puede realizarse por medio de los métodos de extensión. En este
caso la sintaxis es un poco más compleja debido a las expresiones Lambda.
150
Cuando se quieren emplear varios campos en un criterio de ordenación se debe usar
OrderBy y ThenBy:
En este apartado veremos como LINQ to SQL captura las relaciones “uno a uno” y
“uno a muchos” cuando la fuente de datos son las tablas de una BD SQL.
FILMACTOR
IDACTOR ACTOR
IDFILM ID
OSCARWINNER NAME
SEX
FILM DIRECTOR
ID ID
TITLE NAME
IDGENRE
OSCAR
IDDIRECTOR
YEAR GENRE
ID
RATING
NAME
151
Vamos a mostrar cómo las relaciones entre tablas quedan expresadas como relaciones
entre objetos, lo que se denomina clases de entidad.
var filmsDeSpielberg1 =
from f in cineDB.FILMs
where f.DIRECTOR.NAME == "Steven Spielberg"
select new { f.TITLE, Genero = f.GENRE.NAME };
var filmsDeSpielberg2 =
from d in cineDB.DIRECTORs
from f in d.FILMs
where d.NAME == "Steven Spielberg"
select new { f.TITLE, f.GENRE.NAME };
Gracias a este mapeo podemos construir consultas declarativas como las del listado
anterior en el siguiente:
var filmsdeSpielberg =
from f in cineDB.FILMs
where f.DIRECTOR.NAME == "Steven Spielberg"
select new { f.TITLE, Genre = f.GENRE.NAME };
Console.WriteLine("Titulo y genero de los films de Spielberg\n");
foreach (var f in filmsdeSpielberg)
{
Console.WriteLine("{0} {1}", f.TITLE, f.Genre);
}
Observar que en la tabla original FILM no hay una columna de nombre Director que
tenga como valor un objeto de tipo Director, ni una columna Genre que tenga como
valor un objeto del tipo Genre. Sin embargo, en el listado hemos podido escribir
f.Director.Name y f.Genre.Name; esto es posible gracias a las relaciones de tipo
EntityRef que se usan en el código de mapeo. Si echamos un vistazo al código del
diseñador del dbml, veremos algo similar a esto:
La clase Film mapea la tabla con el mismo nombre; nótese que las propiedades Director
y Genre usan respectivamente las variables _Director y _Genre. Al aplicarle la
propiedad Entity a un valor de estos tipos EntityRef se obtienen objetos de tipo
Director y Genre.
152
Un aspecto relevante a destacar es que EntityRef se encarga de mantener la identidad
de los objetos. A pesar de que en el listado siguiente se hacen dos consultas diferentes,
el objeto Director obtenido en ambos casos será el mismo (su comparación devolverá
true). El mecanismo de EntityRef garantiza que ambas consultas producen el mismo
objeto de tipo Director que corresponde a una misma fila en la tabla correspondiente.
Al cambiar a mayúscula el nombre del director y hacer un SubmitChanges() se está
cambiando la fila de la tabla Director correspondiente a Steven Spielberg:
var spielberg =
(from d in cineDB.DIRECTORs
where d.NAME == "Steven Spielberg"
select d).First();
var jurassicPark =
(from f in cineDB.FILMs
where f.TITLE == "Jurassic Park"
select f).Single();
jurassicPark.DIRECTOR.NAME = jurassicPark.DIRECTOR.NAME.ToUpper();
cineDB.SubmitChanges();
El código de mapeo establece también relaciones uno a muchos entre Director y Film
y entre Genre y Film mediante el tipo EntitySet:
[Table(Name="dbo.DIRECTOR")]
public partial class DIRECTOR : INotifyPropertyChanging,
INotifyPropertyChanged
{
private static PropertyChangingEventArgs emptyChangingEventArgs = new
PropertyChangingEventArgs(String.Empty);
[Table(Name="dbo.GENRE")]
public partial class GENRE : INotifyPropertyChanging,
INotifyPropertyChanged
{
153
Como puede comprobarse, las variables _FILMS de los tipos Director y Genre son de
tipo EntitySet<FILM>. Esto permite que se pueda hacer una consulta como la
siguiente:
Usando la propiedad Films de Director se averigua qué directores han dirigido algún
film del género “Mafia”
Si se accede a la BD para listar los films dirigidos por Coppola se podrá observar que
Godfellas está entre ellos.
154
RELACIONES MUCHOS A MUCHOS
En este ejemplo, dicha relación se expresa con la tabla FilmActors. LINQ expresa este
mapeo incluyendo una propiedad FilmActors en Films y otra en Actors. A través de
ellas, a partir de un actor podemos obtener todos los films en los cuales éste trabaja y
dado un film, aquellos actores que trabajan en dicho film. El siguiente listado nos da
los films dirigidos por Clint Eastwood y sus actores, y los films en los que actúa Dustin
Hoffman:
Hubiera sido deseable que incluyese una relación muchos a muchos de forma directa
entre film y actor, es decir, tener en el tipo film una propiedad Actors de tipo
EntitySet<ACTOR> y a su vez en el tipo Actor una de tipo EntitySet<FILM>.
155
En el código a continuación podemos ver como ampliar dichas definiciones para este
propósito:
public partial class FILM : INotifyPropertyChanging, INotifyPropertyChanged
{
private EntitySet<ACTOR> _Actors = null;
private void onAdd(ACTOR a)
{
foreach (FILMACTOR fa in _FILMACTORs)
if (fa.IDACTOR == a.ID && fa.IDFILM == _ID) return;
// Añadimos una nueva relación FilmActor al EntitySet
FILMACTOR fActor = new FILMACTOR();
fActor.IDFILM = ID;
fActor.IDACTOR = a.ID;
fActor.ACTOR = a;
fActor.FILM = this;
_FILMACTORs.Add(fActor);
return;
}
156
De esta forma, podríamos expresar la consulta anterior de esta forma:
El código siguiente daría excepción en ejecución ya que LinQ no está preparado para
transformar esta consulta en sentencias SQL. El problema es que la nueva propiedad
f.Actors se está usando dentro de la cláusula from:
157
PRIORIZAR RENDIMIENTO, MANTENIBILIDAD Y PRODUCTIVIDAD
CUANDO ELEGIMOS PASAR DATOS ENTRE CAPAS
Debemos tener en cuenta varios factores cuando elegimos pasar datos entre capas:
158
Deberíamos evitar jerarquías complejas de objetos y optimizar nuestro diseño
de clases con el fin de minimizar el consumo de memoria y reducir la cantidad
de datos que necesitamos que sean serializados cuando el objeto se pasa entre
clases.
• Connection
• Command
• Data Reader
• Data Adapter
• IDBConnection
• IDataReader
• IDBCommand
• IDBDataAdapter
Los proveedores de datos que contiene dicha biblioteca son específicos de una base de
datos particular a la que deberían poder conectarse. Los proveedores de datos
disponibles en ADO.NET son:
• ProviderType (Enum)
• DatabaseConnectionState (Enum)
• StoredProcedureParameterDirection (Enum)
• DBManager (Class)
• DBHelper (Class)
159
Vamos a comenzar con el tipo de datos enum que contendrá los tipos de proveedor de
datos:
Ahora necesitamos una clase fábrica (que implementa el patrón de diseño factory) que
devuelva un objeto de tipo DbProviderFactory o DbDataAdapter dependiendo del
proveedor de datos que estemos usando. Esta clase contiene métodos factory que son
típicamente estáticos. Estos métodos en la clase DBFactory aceptan una referencia a la
enumeración ProviderType que denota que se está usando ese tipo de proveedor de
datos.
160
El código siguiente es el de nuestra clase DBFactory. Contiene dos métodos estáticos
llamados GetProvider y GetAdapter los cuales aceptan un tipo de proveedor de base
de datos dado por la enum ProviderType:
using System.Data.Common;
using System.Data.SqlClient;
using System.Data.OleDb;
using System.Data.Odbc;
using System.Data.OracleClient;
using System.Collections.Generic;
using System.Text;
161
Ahora veremos como implementar una clase llamada DatabaseHelper, la cual será
responsable de ejecutar las operaciones contra la base de datos. La clase DataHelper
encapsula las diferentes llamadas a la base de datos para ejecutas las operaciones
CRUD (Create, update, read, delete). La clase DBManager que veremos después actúa
como envoltorio de esta clase. Tenemos varios métodos en la clase DataHelper para
añadir parámetros, ejecutar consultas, procedimientos almacenados, etc. Este código
ilustra como se establece la conexión a la base de datos en función del proveedor
elegido y del objeto comando creado:
162
También dispondremos de cuatro métodos para ejecutar operaciones CRUD. Estos
métodos son:
ExecuteScalar()
ExecuteReader()
ExecuteNonQuery()
ExecuteDataSet()
parameter.ParameterName = name;
parameter.Value = String.Empty;
parameter.DbType = DbType.String;
parameter.Size = 50;
switch (parameterDirection)
{
case StoredProcedureParameterDirection.Input:
parameter.Direction =
System.Data.ParameterDirection.Input;
break;
case StoredProcedureParameterDirection.Output:
163
parameter.Direction =
System.Data.ParameterDirection.Output;
break;
case StoredProcedureParameterDirection.InputOutput:
parameter.Direction =
System.Data.ParameterDirection.InputOutput;
break;
case StoredProcedureParameterDirection.ReturnValue:
parameter.Direction =
System.Data.ParameterDirection.ReturnValue;
break;
}
return objCommand.Parameters.Add(parameter);
}
parameter.ParameterName = name;
parameter.Value = value;
parameter.DbType = DbType.String;
parameter.Size = 50;
switch (parameterDirection)
{
case StoredProcedureParameterDirection.Input:
parameter.Direction =
System.Data.ParameterDirection.Input;
break;
case StoredProcedureParameterDirection.Output:
parameter.Direction =
System.Data.ParameterDirection.Output;
break;
case StoredProcedureParameterDirection.InputOutput:
parameter.Direction =
System.Data.ParameterDirection.InputOutput;
break;
case StoredProcedureParameterDirection.ReturnValue:
parameter.Direction =
System.Data.ParameterDirection.ReturnValue;
break;
}
return objCommand.Parameters.Add(parameter);
}
parameter.ParameterName = name;
parameter.DbType = dbType;
parameter.Size = size;
switch (parameterDirection)
{
164
case StoredProcedureParameterDirection.Input:
parameter.Direction =
System.Data.ParameterDirection.Input;
break;
case StoredProcedureParameterDirection.Output:
parameter.Direction =
System.Data.ParameterDirection.Output;
break;
case StoredProcedureParameterDirection.InputOutput:
parameter.Direction =
System.Data.ParameterDirection.InputOutput;
break;
case StoredProcedureParameterDirection.ReturnValue:
parameter.Direction =
System.Data.ParameterDirection.ReturnValue;
break;
}
return objCommand.Parameters.Add(parameter);
}
parameter.ParameterName = name;
parameter.Value = value;
parameter.DbType = dbType;
parameter.Size = size;
switch (parameterDirection)
{
case StoredProcedureParameterDirection.Input:
parameter.Direction =
System.Data.ParameterDirection.Input;
break;
case StoredProcedureParameterDirection.Output:
parameter.Direction =
System.Data.ParameterDirection.Output;
break;
case StoredProcedureParameterDirection.InputOutput:
parameter.Direction =
System.Data.ParameterDirection.InputOutput;
break;
case StoredProcedureParameterDirection.ReturnValue:
parameter.Direction =
System.Data.ParameterDirection.ReturnValue;
break;
}
return objCommand.Parameters.Add(parameter);
}
165
}
objCommand.Transaction = objConnection.BeginTransaction();
}
int i = -1;
166
try
{
if (objConnection.State ==
System.Data.ConnectionState.Closed)
{
objConnection.Open();
}
i = objCommand.ExecuteNonQuery();
}
catch
{
throw;
}
finally
{
if (connectionstate ==
DatabaseConnectionState.CloseOnExit)
{
objConnection.Close();
}
}
return i;
}
o = objCommand.ExecuteScalar();
167
}
catch
{
throw;
}
finally
{
objCommand.Parameters.Clear();
if (connectionstate ==
DatabaseConnectionState.CloseOnExit)
{
objConnection.Close();
}
}
return o;
}
168
}
catch
{
}
finally
{
objCommand.Parameters.Clear();
}
return reader;
}
169
}
}
return ds;
}
objCommand.Dispose();
}
if (objConnection.State ==
System.Data.ConnectionState.Closed)
{
objConnection.Open();
}
reader = objCommand.ExecuteReader();
}
catch
{
throw;
}
finally
{
objCommand.Parameters.Clear();
}
return reader;
}
170
{
dbCommand.Parameters[parameterName].Value = (value ==
null) ? DBNull.Value : value;
}
Nótese que la mayor parte de los métodos de la clase DatabaseHelper han sido
marcados como “internal” para prevenir que sean llamados fuera de su espacio de
nombres.
Bien, ahora vamos a crear la clase envoltorio DBManager, la cual encapsula las
llamadas a la clase DBHelper. La clase DBManager extiende la clase abstracta
DBManagerBase, la cual contiene la definición de los métodos Open() y Close() y otras
propiedades públicas que son genéricas y pueden ser usadas por cualquier clase que
actúe como envoltorio. Veamos la clase DBManagerBase:
public abstract class DBManagerBase
{
protected DatabaseHelper databaseHelper = null;
protected DbDataReader dbDataReader = null;
protected DataSet dataSet = null;
protected ProviderType providerType;
protected String connectionString = String.Empty;
protected bool isOpen = false;
public bool IsOpen
{
get
{
return isOpen;
}
set
{
isOpen = value;
}
}
public string ConnectionString
{
get
{
return connectionString;
}
set
{
connectionString = value;
}
171
}
public DbConnection Connection
{
get
{
return databaseHelper.Connection;
}
}
public DbCommand Command
{
get
{
return databaseHelper.Command;
}
}
public ProviderType DBProvider
{
set
{
providerType = value;
}
get
{
return providerType;
}
}
public DataSet DBSet
{
get
{
return dataSet;
}
}
public DbDataReader DBReader
{
get
{
return dbDataReader;
}
}
protected void Open(string connectionString)
{
databaseHelper = new DatabaseHelper(connectionString,
DBProvider);
}
protected void Close(){
if (dbDataReader != null)
if (!dbDataReader.IsClosed)
dbDataReader.Close();
databaseHelper.Dispose();
}
public void BeginTransaction(){
databaseHelper.BeginTransaction();
}
public void CommitTransaction(){
databaseHelper.CommitTransaction();
}
public void RollbackTransaction(){
databaseHelper.RollbackTransaction();
}
}
172
La clase DBManagerBase contiene los métodos más comunes requeridos. Podemos
abrir o cerrar una conexión, comenzar, validar o echar atrás transacciones, etc.
La clase DBManager que extiende la clase abstracta DBManagerBas contiene una lista
de métodos que pueden ser usados para ejecutar procedimientos, consultas y devolver
datasets o datareaders. Podemos optar por mantener nuestra conexión abierta después
de que se ha llamado al método ExecuteReader de forma que podemos usar la
conexión viva en las subsiguientes operaciones que necesitemos ejecutar en nuestra
base de datos. Los nombres de métodos en la clase DBManager están relacionados con
las operaciones que deben realizar.
Disponemos del método AddParameter que puede ser usado para añadir parámetros
a nuestro procedimiento almacenado, tal que cuando invoquemos al procedimiento le
podamos pasar los parámetros. La cadena de conexión que necesitaremos usar para
conectar a nuestra base de datos puede ser ajustada usando la propiedad
ConnectionString.
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Configuration;
using System.Data.Common;
using System.Data.SqlClient;
using System.Data.OleDb;
using System.Data.Odbc;
using System.Data.OracleClient;
using System.IO;
namespace ApplicationFramework.DataAccessLayer
{
public sealed class DBManager : DBManagerBase
{
public void OpenConnection()
{
connectionString =
ConfigurationManager.AppSettings["ConnectionString"].ToString();
base.Open(connectionString);
}
public void OpenConnection(String connectionString)
{
base.Open(connectionString);
base.IsOpen = true;
}
public void CloseConnection()
{
if (base.isOpen)
base.Close();
173
base.IsOpen = false;
}
public int AddParameter(string name, object value)
{
return databaseHelper.AddParameter(name, value);
}
public int AddParameter(string name,
StoredProcedureParameterDirection parameterDirection)
{
return databaseHelper.AddParameter(name, parameterDirection);
}
public int AddParameter(string name, object value,
StoredProcedureParameterDirection parameterDirection)
{
return databaseHelper.AddParameter(name, value,
parameterDirection);
}
public int AddParameter(string name,
StoredProcedureParameterDirection parameterDirection, int size, DbType
dbType)
{
return databaseHelper.AddParameter(name, parameterDirection,
size, dbType);
}
public int AddParameter(string name, object value,
StoredProcedureParameterDirection parameterDirection, int size, DbType
dbType)
{
return databaseHelper.AddParameter(name, value,
parameterDirection, size, dbType);
}
public object GetParameter(string name)
{
return databaseHelper.GetParameter(name);
}
public DbDataReader ExecuteReader(string query)
{
this.dbDataReader = databaseHelper.ExecuteReader(query);
return this.dbDataReader;
}
public DbDataReader ExecuteReader(string query, CommandType
commandtype)
{
this.dbDataReader = databaseHelper.ExecuteReader(query,
commandtype, DatabaseConnectionState.CloseOnExit);
return this.dbDataReader;
}
public IDataReader ExecuteReader(string storedProcedureName, params
object[] parameters)
{
this.dbDataReader =
(DbDataReader)databaseHelper.ExecuteReader(storedProcedureName,
parameters);
return this.dbDataReader;
}
public DbDataReader ExecuteReader(string query, CommandType
commandtype, DatabaseConnectionState connectionstate)
{
this.dbDataReader = databaseHelper.ExecuteReader(query,
commandtype, connectionstate);
return this.dbDataReader;
174
}
public DbDataReader ExecuteReader(string query,
DatabaseConnectionState connectionstate)
{
this.dbDataReader = databaseHelper.ExecuteReader(query,
connectionstate);
return this.dbDataReader;
}
public object ExecuteScalar(string query)
{
return databaseHelper.ExecuteScalar(query);
}
public object ExecuteScalar(string query, CommandType commandtype)
{
return databaseHelper.ExecuteScalar(query, commandtype);
}
public object ExecuteScalar(string query, DatabaseConnectionState
connectionstate)
{
return databaseHelper.ExecuteScalar(query, connectionstate);
}
public object ExecuteScalar(string query, CommandType commandtype,
DatabaseConnectionState connectionstate)
{
return databaseHelper.ExecuteScalar(query, commandtype,
connectionstate);
}
public DataSet ExecuteDataSet(string query)
{
this.dataSet = databaseHelper.ExecuteDataSet(query);
return this.dataSet;
}
public DataSet ExecuteDataSet(string query, CommandType
commandtype)
{
this.dataSet = databaseHelper.ExecuteDataSet(query,
commandtype);
return this.dataSet;
}
public int ExecuteNonQuery(string query, CommandType commandtype)
{
return databaseHelper.ExecuteNonQuery(query, commandtype);
}
public int ExecuteNonQuery(string query, CommandType commandtype,
DatabaseConnectionState databaseConnectionState)
{
return databaseHelper.ExecuteNonQuery(query, commandtype,
databaseConnectionState);
}
175
Podemos usar la clase DBManager como se muestra a continuación:
Microsoft .NET permite el acceso a Index Server mediante OleDb, de forma que
podemos crear consultas SQL sobre los índices de los documentos que nos permitan
crear datasets sobre esos resultados como cualquier otro origen de datos. En el IDE,
desde el explorador de servidores se puede crear una conexión con dicho servicio.
176
Un ejemplo de consulta SQL sobre Index Server podría ser el siguiente:
Rank es el campo que establece el rango de coincidencia del patrón dado por
FREETEXT(). Si añadimos más palabras a esta función, la consulta nos devolverá
aquellos documentos que tengan al menos una de las palabras definidas en el patrón.
La función SCOPE nos permite limitar nuestra consulta a uno varios directorios
particulares, y definir si queremos o no que incluya subdirectorios.
Para ver como se accede a ficheros, en este caso de texto, desde C# vamos a verlo con
un ejemplo. Primero vamos a crear un archivo con el nombre “abc.txt” y dentro de él
vamos a escribir la cadena “Hola fichero”:
En el caso de que quisiéramos añadir una línea a dicho fichero sin borrar lo anterior,
haríamos lo siguiente:
177
Ahora queremos leer el contenido del fichero y ponerlo en una cadena (en el fichero
anterior, pondríamos “Hola fichero. Estoy aquí”):
System.IO.File.Delete("abc.txt");
Propiedades:
178
Usando la clase UTF8Encoding y su método GetBytes obtenemos los bytes a escribir
en los streams correspondientes, tanto de pantalla como de fichero. Finalmente
escribimos en los streams mediante el método Write de los mismos (public override
void Write(byte [] src, int src_offset, int count)). El primer parámetro es,
evidentemente, la cadena en bytes, el segundo es el desplazamiento (cero en nuestro
caso) y el tercero es la longitud de los bytes a escribir (linea.Length).
FileMode es una enumeración que puede tener uno de los siguientes valores: Open
especifica la apertura de un fichero. Create crea un archivo nuevo y si ya existía lo
sobrescribe. OpenOrCreate abre el archivo si existe o lo crea si no existe. CreateNew
especifica que se crea un archivo nuevo. Append abre el archivo si existe y añade la
salida al final de él. Sólo se puede usar con FileAccess.Write. Si se intenta leer se lanza
la excepción ArgumentException. Finalmente, Truncate abre un archivo que existe y
lo trunca a 0 bytes. El parámetro acceso de la enumeración FileAccess especifica si se
va a abrir el archivo en modo lectura, escritura, o ambos (Read, Write, ReadWrite).
De los muchos constructores de los que dispone la clase FileStream hay tres que poseen
un parámetro de tipo enumerado FileShare. Este parámetro se debe indicar si
queremos compartir el archivo. Puede ser None (ningún otro proceso podrá abrir el
archivo mientras ya esté abierto), Read, Write y ReadWrite (con los que permitiremos
a otros procesos o al propio programa que lo tiene abierto, leer y/o escribir
repetidamente sobre el archivo en cuestión).
Para leer con FileStream tenemos ReadByte() y Read(). El primero lee únicamente un
byte y el segundo un número de bytes de un origen o bufer y con un desplazamiento
(en su caso). Con ReadByte() obtenemos un único byte como valor entero. Si estamos
al final del archivo, devolverá un -1. Con Read() leemos un bloque de bytes de la forma
comentada:
179
De esta forma leemos la cantidad de número_de_bytes de buffer a partir de
buff[desplazamiento]. Nos devolverá un entero que significa el número de bytes que
se han leído correctamente.
using System;
using System.IO;
class leebytes {
public static void Main() {
int leido;
FileStream fichero;
try {
fichero = new FileStream("texto.txt", FileMode.Open);
} catch (FileNotFoundException e){
Console.WriteLine("Error: Fichero no encontrado." + e.Message);
return;
}
do{
try{
leido = fichero.ReadByte();
}
catch (Exception e)
{
Console.WriteLine("Error: " + e.Message);
return;
}
if (leido != -1)
Console.Write((char)leido);
} while (leido != -1);
fichero.Close();
Console.ReadLine();
}
Las clases TextReader, TextWriter son implementadas por otras clases de secuencias
basadas en caracteres (que usamos en el primer ejemplo de este apartado):
- StreamReader: Lee caracteres de un stream de bytes
- StreamWriter: Escribe caracteres en un stream de bytes
- StringReader: Lee caracteres de un stream
- StringWriter: Escribe caracteres en un stream
180
BinaryWriter empaqueta los datos y controla su escritura. Su constructor principal es
BinaryWriter(Stream flujodedatos). Veamos un ejemplo de esto:
using System;
using System.IO;
class binario
{
public static void Main()
{
BinaryReader brStream;
BinaryWriter bwStream;
bool logico = true;
int x = 10;
double euro = 166.386;
Console.WriteLine();
Console.WriteLine("Datos a escribir en el fichero:");
Console.WriteLine("logico = {0}, x = {1}, euro ={2}", logico, x,
euro);
try
{
bwStream = new BinaryWriter(new
FileStream("datos_binarios.txt", FileMode.Create));
}
catch (IOException e){
Console.WriteLine("Error: No se puede crear el fichero: " +
e.Message);
return;
}
try{
Console.WriteLine("Escribiendo en el fichero...");
bwStream.Write(logico);
bwStream.Write(x);
bwStream.Write(euro);
}
catch (IOException e){
Console.WriteLine("Error de escritura: " + e.Message);
}
bwStream.Close();
try{
brStream = new BinaryReader(new
FileStream("datos_binarios.txt", FileMode.Open));
}
catch(FileNotFoundException e){
Console.WriteLine("Error: Fichero no encontrado. " +
e.Message);
return;
}
try{
logico = brStream.ReadBoolean();
x = brStream.ReadInt32();
euro = brStream.ReadDouble();
}
catch (IOException e){
Console.WriteLine("Error de lectura: " + e.Message);
}
brStream.Close();
Console.WriteLine();
Console.WriteLine("Datos leídos del fichero:");
Console.WriteLine("logico ={0}, x={1}, euro={2}", logico, x, euro);
}
}
181
Para redireccionar la salida de nuestro programa usaremos los métodos SetIn(),
SetOut() y SetErr() de Console. El primero admite como parámetros un StreamReader
y los otros dos un StreamWriter. Si queremos redireccionar la salida, es tan fácil como
crear un StreamWriter y luego llamar a SetOut():
Restar estas dos fechas produce una instancia de TimeSpan, la cual tiene una
propiedad TotalSeconds que nos da los segundos (ver TimeSpan en la ayuda). Es
evidente que tenemos propiedades como TotalMinutes, TotalHours, etc... para calcular
diferentes fracciones de tiempo.
En este apartado vamos a crear un archivo XML desde C#. Primeramente vamos a
obtener un ejemplo de fichero XML desde MySQL. Dentro de la BD Test, vamos a crear
una tabla llamada prueba con dos campos: Código (int) y Descripción (varchar(50)).
En ella insertaremos cuatro registros con este formato: Código: x, Descripción: Código
x (en letra).
Para exportar estos datos a XML tan sólo hemos de teclear lo siguiente desde una
consola:
lo que nos mostrará un archivo XML con datos de la BD test. Si redireccionamos esta
salida a un fichero, ya tendremos un archivo para poder trabajar con él.
182
Nos quedaría algo similar a esto:
<?xml version="1.0"?>
<mysqldump xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<database name="test">
<table_structure name="prueba">
<field Field="Codigo" Type="int(11)" Null="YES" Key="" Extra="" />
<field Field="Descripcion" Type="varchar(50)" Null="YES" Key="" Extra="" />
<options Name="prueba" Engine="InnoDB" Version="10" Row_format="Compact"
Rows="4" Avg_row_length="4096" Data_length="16384" Max_data_length="0" Index_length="0"
Data_free="0" Create_time="2007-04-22 08:56:46" Collation="latin1_swedish_ci" Create_options=""
Comment="InnoDB free: 4096 kB" />
</table_structure>
<table_data name="prueba">
<row>
<field name="Codigo">1</field>
<field name="Descripcion">CÓDIGO UNO</field>
</row>
<row>
<field name="Codigo">2</field>
<field name="Descripcion">CÓDIGO DOS</field>
</row>
<row>
<field name="Codigo">3</field>
<field name="Descripcion">CÓDIGO TRES</field>
</row>
<row>
<field name="Codigo">4</field>
<field name="Descripcion">CÓDIGO CUATRO</field>
</row>
</table_data>
</database>
</mysqldump>
183
Veamos ahora el siguiente listado:
using System;
using System.Xml;
namespace XML
{
class crearXML
{
XmlDocument docxml;
XmlNode nodoxml;
XmlElement elemxml;
XmlElement elemxml2;
XmlText textoxml;
nodoxml=
docxml.CreateNode(XmlNodeType.XmlDeclaration,"","");
docxml.AppendChild(nodoxml);
elemxml=docxml.CreateElement("","RAIZ","");
textoxml=docxml.CreateTextNode("Texto del elemento
raíz");
elemxml.AppendChild(textoxml);
docxml.AppendChild(elemxml);
elemxml2=docxml.CreateElement("","Elemento1","");
textoxml=docxml.CreateTextNode("Texto del elemento 1");
elemxml2.AppendChild(textoxml);
docxml.ChildNodes.Item(1).AppendChild(elemxml2);
try{docxml.Save(@"C:\ejemplo.xml");}
catch(Exception e){Console.WriteLine(e.Message);}
}
}
}
184
El resultado lo vemos a continuación abriendo el archivo con un navegador:
<RAIZ>
Texto del elemento raíz
<Elemento1>Texto del elemento 1</Elemento1>
</RAIZ>
Bien, ahora vamos a manipular este fichero XML. Con la clase XmlTextReader
creamos un stream o flujo de datos asociado a un xml, al cual podemos tratar
mediante las propiedades y métodos de dicha clase. Veamos el siguiente listado:
using System;
using System.Xml;
namespace XML
{
class leeXML
{
public static int Main(string[] args)
{
if (args.Length==0)
{
Console.WriteLine("Sintaxis: LeeXML <ficheroXML>");
return 1;
}
else if (args.Length>1)
{
Console.WriteLine("Demasiados argumentos");
Console.WriteLine("Sintaxis: LeeXML <ficheroXML>");
return 1;
}
XmlTextReader fichero = new XmlTextReader(args[0]);
while (fichero.Read())
{
if (fichero.MoveToContent()==XmlNodeType.Element)
{
Console.WriteLine("Elemento = " +
fichero.Name);
if (fichero.IsEmptyElement &&
fichero.HasAttributes)
{
for (int i=0; i<fichero.AttributeCount;
i++)
{
fichero.MoveToAttribute(i);
Console.WriteLine(" Atributo = " +
fichero.Name);
}
fichero.MoveToElement();
}
}
}
return 0;
}
}
}
185
GENERANDO DOCUMENTACIÓN XML
Podemos usar un tipo especial de comentarios para generar código XML. Lo hacemos
precediendo al código ///. Hay una serie de etiquetas XML muy útiles:
Etiqueta Propósito
<summary> … </summary> Proporciona una breve descripción.
<remarks> … </remarks> Proporciona una descripción detallada. Puede
contener párrafos anidados, listas y otro tipo de etiquetas.
<para> … </para> Para añadir estructura a la definición en <remarks>.
Permite que los párrafos sean desalineados.
<list type="…"> … </list> Añade una lista estructurada a una descripción
detallada. Los tipos soportados son “bullet”,”number” y “table”. Etiquetas adicionales
(<term>...</term> y <description>...</description> son usadas dentro de la lista para
definir más profundamente la estructura.
<example> … </example> Proporciona un ejemplo de cómo un método,
propiedad u otra librería debe ser usada. En ocasiones incluye el uso de una etiqueta
anidada <code>.
<code> … </code> Indica que el texto encerrado entre ellas es código de
aplicación.
186
<exception> … </exception> Proporciona una descripción para una clase de
excepción.
<permission> … </permission> Documenta la accesibilidad de un miembro.
<param name="name"> …</param> Proporciona una descripción para un método
parámetro
<returns> … </returns> Documenta el valor de retorno y el tipo de un
método.
<value> … </value> Describe una propiedad.
187
INTERACCIÓN CON MICROSOFT EXCEL
Para poder realizar una aplicación en la cual podamos interactuar con Excel,
primeramente, hemos de agregar la referencia a la Microsoft Excel Object Library
correspondiente (por ejemplo, para la Office 2003, la versión es la 11).
Una vez agregada, ya podemos introducir la línea: using Excel; de forma que podamos
usar las clases de dicho espacio de nombres. Los pasos que generalmente vamos a
seguir son:
1. Declaramos la aplicación, libro de trabajo y hoja/s con los que vamos a trabajar:
Librodetrabajo=oXL.Workbooks.Add(Type.Missing);
Hoja=(Excel.Worksheet)Librodetrabajo.Sheets[1];
El siguiente paso consiste en obtener un rango de esa hoja para trabajar con el, eso se
hace mediante:
Excel.Range rg = Hoja.get_Range("A1","E1");
rg.Select();
Una vez seleccionado dicho rango (en este caso, A1:E1), es cuando procedemos a
ajustar sus propiedades. Comentar, antes de seguir, que la función get_range es propia
de C#. Para hacer lo mismo en .NET tendríamos que recurrir a Range. Volviendo a
get_Range, otra forma de seleccionar ese rango es la siguiente:
get_Range(“A1:E1”,Type.Missing).
rg.Font.Name="Arial";
rg.Font.Bold=true;
rg.Font.Size=10;
rg.WrapText=true;
rg.HorizontalAlignment=Excel.Constants.xlCenter;
rg.Interior.ColorIndex=40;
rg.Borders.Weight=3;
rg.Borders.LineStyle=Excel.Constants.xlSolid;
rg.Cells.RowHeight=38;
rg=Hoja.get_Range("A1",Type.Missing);
rg.Cells.ColumnWidth = 7;
rg.Value2 = "Valor de la celda";
La propiedad WrapText sirve para ajustar el texto a la celda, siempre que esté a true.
El resto de propiedades se ven de una forma más o menos clara, con lo cual no vamos
a profundizar en ellas. De esta forma, como podemos ver, configuramos a nuestro
188
gusto la hoja en cuestión. El siguiente paso ahora consiste en grabar esos cambios en
el archivo deseado. Veamos el siguiente código:
oXL.ActiveWorkbook.SaveAs(@"c:\prueba.xls",Excel.XlFileFormat.xlWorkbookNor
mal,Type.Missing,Type.Missing,Type.Missing,Type.Missing,Excel.XlSaveAsAcces
sMode.xlNoChange,Type.Missing, Type.Missing, Type.Missing,
Type.Missing,Type.Missing);
Huelga decir cual es el motivo de la inserción de esta línea. Indicar que el número de
argumentos puede variar según la versión de la Object Library, y que no está
sobrecargado, con lo cual debemos utilizar tantos como nos pida la propia función.
Supongamos que se desea exportar un dataset a una hoja de Excel. El ejemplo a
continuación nos muestra la manera de hacerlo:
using System.Data.SqlClient;
using Microsoft.Office.Interop.Excel;
private void button1_Click(object sender, System.EventArgs e) {
SqlConnection oConexion = new SqlConnection();
oConexion.ConnectionString = "Integrated Security=SSPI;Persist Security
Info=False;" + "Data Source=localhost;Initial Catalog=Northwind"; SqlCommand
oComando = new SqlCommand();
oComando.Connection = oConexion;
oComando.CommandType = CommandType.Text;
oComando.CommandText = "SELECT EmployeeID, (FirstName + ' ' + LastName)
AS Nombre FROM Employees";
SqlDataAdapter oAdaptador = new SqlDataAdapter();
oAdaptador.SelectCommand = oComando;
DataSet oDataSet = new DataSet();
oConexion.Open();
oAdaptador.Fill(oDataSet, "Employees");
oConexion.Close();
Microsoft.Office.Interop.Excel.Application oExcel = new
Microsoft.Office.Interop.Excel.Application();
oExcel.Visible = true;
oExcel.WindowState = XlWindowState.xlNormal;
Workbooks oWBooks = (Workbooks) Workbooks;
_Workbook oWBook =
(_Workbook)(oWBooks.Add(XlWBATemplate.xlWBATWorksheet)); Sheets oSheets
= (Sheets)oWBook.Worksheets;
_Worksheet oWSheet = (_Worksheet)(oSheets.get_Item(1));
int nContador = 0;
foreach (DataRow oRow in oDataSet.Tables["Employees"].Rows) {
nContador++;
Range oRange = oWSheet.get_Range("A" + nContador, "A" + nContador);
oRange.Value2 = oRow["Nombre"]; } }
189
Una de tantas formas de hacer algo así es utilizar ORCA. Orca es un programa de
Microsoft que permite editar nuestro archivo msi de forma que podamos cambiar
muchas de las propiedades de dicho archivo, y en nuestro caso, añadir un acceso
directo a un script de desinstalación. Para poder descargarse Orca, debemos recurrir a
un kit completo de desarrollo de Microsoft llamado Microsoft Platform SDK.
190
Nos aparece un cuadro de diálogo donde debemos rellenar una serie de campos, algunos de
ellos obligatorios:
Campo Valor
Shortcut _51AB71ACD3B646EAA292EB4A3766037E
Directory _D072DA12046B46AA89C9C22AFA43519A
Name UNINST~1|Uninstall Application
Compone C__2972A49A7A6C4FB7BA9ECA73384DA0D
nt A
Target [SystemFolder]/msiexec.exe
Argument /i{7BFFC693-5BB6-4BB3-A311-
s 95FF0E6B8405}
Descriptio
n
Hotkey
Icon
IconIndex
ShowCm
1
d
WkDir TARGETDIR
Shortcut es el acceso directo en si. Para él podemos usar un GUID cualquiera, o usar
GUIDGEN. El que damos como ejemplo, puede servir. En Directory va el GUID del
directorio donde queremos colocar el acceso. Aquí simplemente, podemos copiar
cualquiera de las ubicaciones de los diferentes accesos que ya ha creado Visual
Studio .NET. Por ejemplo, si queremos colocarlo en el menú de programas,
simplemente copiamos el GUID del anterior Shortcut ya existente. En Component
damos el GUID del fichero principal que sirve de sustento a los accesos directos. Por
último, Arguments es /i y a continuación el GUID del archivo de instalación que
podemos obtener desde el propio proyecto de instalación en la propiedad
ProductCode.
Con esto, al instalar nuestra aplicación veremos un acceso directo que nos permitirá
su desinstalación o reparación según el caso.
En .NET podemos conocer que usuario está logueado en el momento en que se ejecuta
la aplicación e incluso en que grupo de usuarios está ubicado. Este proceso lo vamos a
ejecutar con la ayuda de las clases WindowsIdentity y WindowsPrincipal que se
encuentran en el namespace System.Security.Principal.
WindowsIdentity wi = WindowsIdentity.GetCurrent();
WindowsPrincipal wp = new WindowsPrincipal(wi);
191
WindowsIdentity representa a un usuario de Windows. Con el método GetCurrent()
obtenemos el usuario que está actualmente autenticado en nuestro equipo. Si
llamamos a la propiedad name de wi, obtendremos el nombre del usuario.
if (wp.IsInRole("Administradores"))
if (System.Diagnostics.Debugger.IsAttached) ...
Esto devuelve true cuando está conectado el debugger, lo cual ocurre cuando se lanza
la ejecución con Debug desde dentro de Visual Studio. Tiene la ventaja de que, sin salir
del IDE, se puede comprobar como se va a comportar el programa cuando lancemos
el ejecutable. Para ello, solo tenemos que lanzarlo con “Iniciar sin depurar”
192
NANT
Con Nant se pueden escribir uno o más archivos de comandos que se encargan de
resolver situaciones como las siguientes:
Estas tareas las podemos realizar de forma automática, por ejemplo, al comienzo del
día, el primer lunes de cada semana, etc… Veamos un pequeño ejemplo de un archivo
NAnt:
193
Veamos un ejemplo de uso de NAnt. Primero vamos a crear el siguiente código en C#:
namespace PruebaAnt {
static class Program {
static void Main() {
System.Console.Write("Hola, vengo de marte...");
}
}
}
Nant – buildfile:miarhivo.default
El ejemplo anterior ejecuta primero el “target” (tarea a realizar) llamado “run”, ya que
esto se especifica en la segunda línea mediante el elemento “default”. Esta tarea
depende (depends) de “compila”, por lo que se ejecutará lo que está allí antes de hacer
nada con ese bloque. Una vez finalizado con la dependencia (compilación) se volverá
y ejecutará el resto del bloque. Para el caso que la ejecución falle y el atributo
failonerror se configure a verdadero, entonces se abortará completamente
mostrándose finalmente el error al usuario.
Utilizando propiedades
Aunque la propiedad puede llamarse con un nombre único, la utilización del punto
como separación nos permite agrupar varios nombres bajo un grupo lógico. La
definición de una propiedad puede darse dentro de un objetivo (target) lo que
alcanzará solamente dicho bloque. Sin embargo, también podemos definir una o más
a nivel general (fuera de todo “target”), lo que hará que los valores sean visibles desde
todo el proyecto.
Para hacer referencia a la propiedad, hay que indicar la misma empleando la sintaxis
${variable}. El siguiente ejemplo escribe el proyecto y la unidad a la consola:
Existe una excepción a la inmutabilidad que radica en que podemos cambiar el valor
de una propiedad cuando ejecutamos NAnt usando la siguiente sintaxis:
195
Si lo deseamos podemos partir un archivo en varios más pequeños; por ejemplo
podríamos agrupar todos los objetivos relacionados con la BD en uno llamado
basededatos.build y en otro poner lo relacionado con compilar:
Estas líneas deben ser incluidas debajo de la etiqueta raíz (proyecto) y es recomendable
no escribir un archivo gigante sino partirlo en varios.
Tareas
A continuación, se explican alguna de las tareas que podemos realizar con NAnt:
<delete>
<fileset>
<include name=”*.exe” />
<include name=”*.pdb” />
</fileset>
</delete>
solution: Compila una solución Visual Studio sin necesidad de cargar el IDE. En este
caso, compila test.sln omitiendo el proyecto a.csproj:
197
Además de las funciones predefinidas, podemos crear las nuestras propias con C#.
Veamos un ejemplo:
<?xml version="1.0"?>
<project name="Mi proyecto" default="run">
<target>
<setenv>
<variable name="proyecto.ruta"
value="${proyecto.ruta}" />
<variable name="archivo"
value="${proyecto.ruta}${vss.ruta}\version.txt" />
</setenv>
<echo message="La version ${micodigo::escribeVersion()} fue
creada!!" />
</target>
198
Funciones predefinidas
Función Descripción
assembly::get-full-name Obtiene el nombre completo de un
ensamblado
assembly::get-version Obtiene la versión de un ensamblado
bool::parse Convierte un texto a una expresión
booleana true/false
datetime::get-day Obtiene el día del mes representado por
la fecha dada
directory::exists Verifica si el directorio existe
directory::get-creation- Obtiene la fecha de creación de un
time directorio
framework::exists Verifica si .NET está instalado
framework::get-version Obtiene la versión instalada del
framework
string::contains Indica si una cadena de texto está
contenida en otra
Un ejemplo de uso:
<if test=”${bool::parse(“compilar)}”>
<solution configuration=”Release”
solutionfile=”${proyecto.directorio\miproyecto2.sln”
outputdir=”${compilar.ruta.salida}”>
<webmap>
<map url=http://localhost/miproyecto/miproyecto2.csproj
path=”${wwwroot.ruta\miproyecto2.csproj />
</webmap>
</solution>
Además podemos usar NAnt con intellisense en VS. Basta con poner el archivo
nant.xsd en el directorio ..\CommonX\Packages\schemas\xml. Una vez hecho esto
se puede abrir el archivo en Visual Studio pero tendremos que adicionar dentro del
elemento Project la etiqueta xlmns con el siguiente valor para que sea reconocido:
199
<project xlmns=”http://nant.sf.net/release/0.85/nant.xsd>
…
</project>
Toda la premisa del diseño basado en dominios (Domain Driven Design) se basa en la
evolución de un modelo de dominio como piedra angular de la actividad de diseño.
Un modelo de dominio consiste en varias abstracciones de nivel de dominio, las cuales
se basan en interfaces que revelan sus objetivos. Cuando se habla de abstracciones, van
incluidos tanto datos como el comportamiento asociado. Todo el propósito tras DDD
consiste en el manejo de la complejidad a la hora de modelar estas abstracciones de
forma que se pueda usar de forma fácil por otros clientes. En el proceso de extensión,
el diseñador necesita asegurar que los supuestos básicos o restricciones de
comportamiento no se vulneran y que las abstracciones en los interfaces respetan el
marco de trabajo contractual básico (precondiciones, postcondiciones e invariantes).
Un ejemplo:
interface IValueDateCalculator
{
DateTime calculateValueDate(DateTime tradeDate);
}
Este interface cumple con todos los criterios de no revelación de intenciones. Además,
la pregunta que surge es si proporciona todas las restricciones necesarias que demanda
el implementador. Por ejemplo, cabe preguntarse cómo especificar que el valor
calculado debe ser una fecha de negocio posterior a la comercial (tradeDate) y que al
menos debe ser al menos tres días hábiles anteriores a la fecha comercial de entrada.
Los interfaces puros no permiten especificar tales criterios. Los atributos (o
anotaciones en Java) tampoco son de ayuda partiendo de que no son heredados por
las implementaciones.
Una alternativa puede ser hacer una clase abstracta con todas las restricciones sobre la
que basar la implementación:
200
abstract class ValueDateCalculator
{
public DateTime calculateValueDate(DateTime tradeDate)
{
DateTime valueDate = doCalculateValueDate(tradeDate);
if (valueDate < tradeDate) {
throw new ArgumentException (“…”);
}
if ((valueDate – tradeDate).Days < 3) {
throw new ArgumentException(“…”);
}
// chequeo de otras postcondiciones
return valueDate;
}
Este modelo chequea todas las restricciones necesarias a satisfacer una vez que la
implementación calcula la fecha sobreescribiendo el método plantilla. Por otra parte,
con interfaces puros (el primer modelo), para hacer cumplir todas las restricciones, se
dispone de las siguientes alternativas:
Las clases abstractas proporcionan una evolución fácil del modelo de dominio. El
proceso de modelado de dominio es iterativo y evolutivo. Por lo tanto, una vez
publicadas las APIs, se necesita asegurar cierta inmutabilidad desde el momento en el
que todas ellas serán potencialmente usadas por distintos clientes. Algunas escuelas
de pensamiento adoptan diferentes técnicas con el fin de alcanzar su inmutabilidad.
El equipo de desarrollo de Eclipse usa extensión de interfaces (The Extension Object
Design Pattern) y evoluciona sus diseños nombrando interfaces extendidas con un
sufijo numérico.
Una vez más, para dar soporte a la evolución “suave” de las APIs de dominio, las
interfaces deben estar respaldadas con una implementación de clases abstracta y los
implementadores programar dicha clase y no el interfaz. La pregunta que surge es
para qué es bueno entonces un interfaz.
201
¿Son inservibles los interfaces en DDD?
Ciertamente no. Se usarán interfaces puros para manejar las siguientes situaciones:
202