Cualquier cosa que valga la pena se hace en equipo

Cualquier cosa que valga la pena se hace en equipo

domingo, 8 de agosto de 2010

clases abstractas, Interfaces y enumeraciones PARTE II

LA IMPLEMENTACION
Creemos un proyecto de consola:

Y generemos en el 4 módulos denominados AnalizaArgumentos, Analizador, OcurrenciaPalabra y WordCount.-
El código de las clases que se montan en cada modulo esta en las paginas siguientes.-
Luego para utilizarlo genere archivos de texto e invoque en la línea de comandos a los mismos con las opciones que desee.-


Por ejemplo colocando esto en la ventana de propiedades del proyecto en la opción DEBUG:
-a -o Texto.txt Conecciones.txt -fSalida.txt


Esto es interpretado por el programa asi:
-a: Un switch que significa ORDENADO ALFABETICAMENTE


-o: Un switch que significa ORDENADO por OCURRENCIA y luego ALFABETICAMENTE


Texto.txt Conecciones.txt: Son dos archivos de texto, que estan en el paso de ejecucion (o en el directorio de trabajo en el que estamos ya que en el ejemplo marcamos C:\M3)


-fSalida.txt: Es un archivo de salida de la información que genera WordCount. La alternativa por defecto es la pantalla de texto.-






La clase Abstracta Analizador
/*=====================================================================
Archivo: Analizador.cs
Esta clase tiene el cuerpo basico para trabajar como un analizador de
* argumentos para un programa que admite parametros en su invocacion
* desde la linea de comandos. Los parametros, se asume, comienzan con
* llaves (switchs) que indican alguna particularidad del mismo.
* Un ejemplo valido seria
* c:\prog1.exe -eArchivo.txt -sSalida.txt
* Donde prog1.exe es el programa que se ejecuta y tanto -eArchivo.txt
* como -sSalida.txt son los argumentos
* -e (antes de Archivo.txt) y -s (antes de Salida.txt) son las llaves.
*

=====================================================================*/

//
using System;
using System.Globalization;


///////////////////////////////////////////////////////////////////////////////


public abstract class Analizador {
private String[] separadores; // Por defecto: "/", "-"
private String[] argumentos; // Estos son los argumentos validos
//que el analizador debe reconocer
private Boolean argSensiblesAMayOMin; // Indica si los argumentos deben ser sensibles a Mayusculas/Minusculas


//
protected enum SwitchStatus { NoError, Error, ShowUsage };


// Constructor 1
public Analizador(String[] switchSymbols)
: this(switchSymbols, false, new string[] { "/", "-" }) {
}


// Constructor 2
public Analizador(String[] switchSymbols, Boolean caseSensitiveSwitches)
: this(switchSymbols, caseSensitiveSwitches, new string[] { "/", "-" }) {
}


// Constructor 3, sin valores por defecto
public Analizador(String[] _argumentos, Boolean _argSensiblesAMayOMin, String[] switchChars)
{
this.argumentos = _argumentos;
this.argSensiblesAMayOMin = _argSensiblesAMayOMin;
this.separadores = switchChars;
}


// -
public abstract void ModoDeUso(String errorInfo);



protected virtual SwitchStatus OnSwitch(String switchSymbol, String switchValue) {
return(SwitchStatus.Error);
}



// Cada clase derivada debe implementar el metodo OnNonSwitch
//observe que sino por defecto un switch se considera un error
protected virtual SwitchStatus OnNonSwitch(String value) {
return(SwitchStatus.Error);
}


// Informamos a la clase derivada despues que se hizo todo el analisi
// -
protected virtual SwitchStatus OnDoneParse() {
// -
return(SwitchStatus.Error);
}


// Solo sabemos analizar argumentos de la linea de comandos
public Boolean Parse() {
return(Parse(Environment.GetCommandLineArgs()));
}


// Este es el analizador
public Boolean Parse(String[] args) {
SwitchStatus ss = SwitchStatus.NoError; //

int ArgNum;
for (ArgNum = 0; (ss == SwitchStatus.NoError) && (ArgNum < fisswitch =" false;" n =" 0;" fisswitch =" (0" flegalswitchsymbol =" false;" n =" 0;" flegalswitchsymbol =" (0" flegalswitchsymbol =" (0"> salir
if (!fLegalSwitchSymbol) {

ss = SwitchStatus.Error;
break;
} else {
// Switch valido -> notificar a la clase derivada el switch y su valor
ss = OnSwitch(
argSensiblesAMayOMin ? argumentos[n] : argumentos[n].ToLower(CultureInfo.InvariantCulture),
args[ArgNum].Substring(1 + argumentos[n].Length));
}
}
else // Esto no es un switch, retornar a la clase derivada para que lo analice
{
ss = OnNonSwitch(args[ArgNum]);
}
}

// Fin del analisis de los argumentos
if (ss == SwitchStatus.NoError) {
// -
ss = OnDoneParse();
}

if (ss == SwitchStatus.ShowUsage) {
//
ModoDeUso(null);
}

if (ss == SwitchStatus.Error) {
//
ModoDeUso((ArgNum == args.Length) ? null : args[ArgNum]);
}

// -
return(ss == SwitchStatus.NoError);
}
}


///////////////////////////////// End of File /////////////////////////////////

clases abstractas, Interfaces y enumeraciones-PARTE I

OBJETIVO

Vamos a hacer un ejercicio clásico: Un programa que lee un archivo de texto y cuenta los caracteres, palabras y líneas que hay en el.-
Este programa clasico suele llamarse (como era de esperar) WordCount y su ductilidad es tal que permite utilizar muy distintas estructuras de datos para lograr el cometido.-
En nuestro caso pondremos el acento en generar una clase abstracta que se encargara de analizar la entrada que acompaña a la invocación del programa (los argumentos de línea de comando).-
Por otro lado utilizaremos unas interfaces útiles para el tipo de estructura de datos que nos interesa utilizar.-
Creemos que WordCount al ser un programa clásico de algoritmos permite visualizar aspectos poderosos de la programación orientada a objetos así como sacar provecho de algunas de las bibliotecas estándar de C#.-




El requerimiento funcional

WordCount es un programa que:
A) Debe tomar como entrada un archivo de texto.Entiendase por esto que puede elegir distintos CodePages (encoding) pero que el archivo en si debe ser un flujo de caracteres en el que exista el concepto de palabra como unidad de construcción
B) De esa entrada debe producir un conteo:
· Lineas (grupos de caracteres separados por “nueva linea”).-
· Palabras (grupos de caracteres entre separadores de texto).-
· Caracteres (son los que conforman las palabras y los separadores de texto).-
· Bytes (son los caracteres mas las “nueva linea”).-
C) La salida dr la información puede ser por consola o puede ser hacia un archivo (de texto) que debera ser especificado.-
D) La salida admite ciertos ordenamientos:
· Por palabra indicando la cantidad de ocurrencias de la misma
· Por ocurrencia y luego alfabeticamente por palabra
E) La aplicación sera una aplicación de consola por lo que desde la linea de comandos debe poder especificarse lo que se desea que realice la aplicación. No se desea que la aplicación necesite de un archivo de propiedades dado que su funcionalidad es reducida y se adecua a recibir parametros dedes la linea de comandos del sistema operativo.-

Mapeo de funcionalidad con mi toolbox:




Este problema nos pone frente a una variedad de situaciones conocidas que enunciaremos como lista de requerimientos y funcionalidades que la aplicación debe tener. A cada comentario de funcionalidad mapearemos una tecnica o modo de resolucion. Esta es una buena practica para cualquier programador que permite probar la riqueza del lenguaje que utiliza asi como las bibliotecas y el Framework sobre el que esta trabajando.-




Siendo esta aplicación de consola, manejando caracteres y esencialmente algoritmica, veremos que las colecciones e interfases del CLR de .NET haran gran parte del trabajo.-




1- Analisis de parametros recibidos desde la linea de comando. Es una aplicación de consola sin interfaz grafica con el usuario. De modo que parece razonable utilizar parametros desde la linea de comandos en vez de utilizar un archivo de propiedades. Al fin de cuentas no son tantas las propiedades de configuracion que la aplicación deba contemplar.-
Como estamos acostumbrados a que estos parametros sean letras precedidas por separadores (como por ejempo leerdatos.exe –ac:\datos.txt –fc:\analisis.prn) haremos que esta clase sea una base utilizable para tareas similares.-
Cada letra indica una posible accion de la aplicación y es posible que exista una combinacion de letras, por lo que la tarea basica del analizador (Parser) es trabajar con separadores y argumentos y retornar arrays de estas categorias para que el programa que usa estos servicios pueda realizar su tarea especifica de interpretacion de los parametros y las llaves.-
Sin embargo esta clase no podra resolver todos los casos que plantee el analisis de argumentos, aun cuando usemos el esquema simple de llaves(switchs) y argumentos.
Por otro lado si pensamos en una interfaz, solo enunciaremos los metodos y deberemos escribir en cada implementacion codigo que es comun.-
Es decir una clase no nos resuelve todos los casos y una interfaz no cumple una funcion util.-
Para eso vamos a hacer una clase abstracta que hara las tareas de analisis de parametros en una linea de comandos.-
Esta es la real utilidad de una clase abstracta: Presentar la posibilidad de herencia de los propios metodos de la clase, tener metodos que deben ser escritos por el implementador de la clase que hereda, Y NO PERMITIR LA INSTANCIACION DIRECTA.
Es decir no podemos hacer




MiClaseAbstracta ca = new MiClaseAbstracta();




Este no es un detalle menor como elección de ingeniería. Al no permitir la instanciación directa obliga a que otra clase:
Herede :usando lo que ya esta definido y es común.-
Redefina : (override)

2- Lectura desde un flujo plano de caracteres. Esto es leer un archivo de texto. Esta tarea la realizaremos con la clase FileStream del namespace FileStream y un lector de flujo (StreamReader):

FileStream fsIn = new FileStream(pathname, FileMode.Open, FileAccess.Read, FileShare.Read);

StreamReader sr = new StreamReader(fsIn, fileEncoding, true);

3- Los archivos que leeremos como entrada debemos colocarlos en un contenedor (Un array de strings que implementaremos como un ArrayList:
private ArrayList archivosALeer = new ArrayList();

4- Este arreglo deberá después enviarse de algún modo al programa que llama de modo que pueda barrer esa lista de archivos e ir procesándolos y llevando su estadística de líneas, palabras, etc.
Acá es valido pensar en un getter que devuelva dicho array al programa para que opere sobre el o bien utilizar una Interfaz de enumeración.
Esta es una aproximación muy interesante al problema que nos permite devolver el enumerador de esa colección y luego el programa que llama se encarga de iterar sobre dicho enumerado y tomar su valor asociado. Esto permite que si luego internamente en la clase que implementa lo indicado en el punto 3, deseamos cambiar la estructura, el programa que llama no se entera ya que el siempre barrera un iterador.-

// Retorna un enumerator incluyendo todos los archivos indicados
public IEnumerator GetEnumeradorDeArchivos() {
return archivosALeer.GetEnumerator(0, archivosALeer.Count);
}
IEnumerator que es la interfaz que aquí utilizamos es la Interfaz base de todas las enumeraciones genéricas.-
5- Analizador de cada línea leída, cortando esa línea en palabras. Este es un algoritmo clásico en el que el concepto de palabra es el conjunto consecutivo de caracteres entre separadores. Los separadores son los propios del sistema LEXICO que se esta utilizando (lo que en jerga técnica se llama ENCODING o CONJUNTO DE CARACTERES). Observemos que esto implica que para un lenguaje dado, existe un sistema LEXICO dado (un ENCODING) y por lo tanto un conjunto de separadores ya definidos que es el aceptado por los lenguajes que usan dicho ENCODING.-
Esta tarea es habitual y en las clases de C# (en realidad en las de .NET y el CLR) tenemos funcionalidad de cortar una línea de textos en palabras (String.Split()) que contiene todo lo que necesitamos para realizar esta tarea de separación.-
6- Necesitamos un contenedor especial: Un diccionario. Este es un contenedor de dos posiciones. Una es la clave y la otra el valor que nos interesa. Esta estructura de datos es de utilidad para llevar la cuenta de palabras encontradas que es uno de los objetivos primarios de la aplicación.
El Diccionario es un contenedor clásico y en las bibliotecas de .NET y el CLR tenemos una implementación de especial utilidad denominada SortedList.-

// La lista de palabras (ordenada alfabeticamente)
System.Collections.SortedList ctdorDePalabras = new SortedList();
Lo interesante de este contenedor es que es una implementación de una interfaz genérica denominada IDictionary. Esta interfaz es la base de todas aquellas estructuras que deban cumplir el patrón: par clave/valor.-
Otra característica interesante de este contenedor es que también es una implementación de la Interfaz de listas no genéricas (IList). Esto nos permite tener en el mismo constructor las facilidades de agregar un par clave/valor del diccionario, asi como las búsquedas de inclusión que tienen las listas.-
Advertencia: No esta demás decir que tanta funcionalidad es al precio de la performance. Sea cauto en el uso de SortedList para implementación de par/valor. En otros casos un HashTable puede ser más eficiente en términos de velocidad. Acá elegimos las SortedList ya que se adecuan a las necesidades de ordenamiento

7- El ordenamiento solicitado de las listas que contienen las palabras leídas y sus ocurrencias, se ha solicitado de modo doble: A) Alfabéticamente y B) Por Ocurrencia y Alfabéticamente.-
Esto nos propone una idea: Que exista una clase que exprese la ocurrencia de las palabras y que sirva para que nuestra lista resulte ordenada según las reglas del punto B) (por ocurrencia y alfabéticamente). Siendo que las listas se armaran como SortedLIst que es una lista ordenada, resulta necesario que esta clase que vamos a crear tenga alguna relación con SortedList.
Es aquí donde comienza a jugar un papel importante la idea de polimorfismo. SortedList implementa entre otras cosas la interfaz IComparable. Esta interfaz tiene un método virtual llamado CompareTo que implementa.

Para entender como enlazar todo esto veamos lo que dicen los ingenieros que implementaron SortedList:


Lo elementos de una SortedList estan ordenados por sus claves, ya sea de acuerdo a una implementacion de IComparer especificada cuando se crea la SortedList o de acuerdo a una implemenatcion de IComparable provista por las claves mismas. En ambos casos una SortedList no permite la duplicacion de llaves.


Esto significa que podemos resolver el tema de ordenamiento por ocurrencia y alfabéticamente haciendo una clase que IMPLEMENTE IComparable y que se creen objetos que sean de dicha clase:


public class OcurrenciaPalabra : IComparable
{
….
}




1- Para mostrar la información solicitada es necesario barrer las listas ordenadas. Las listas estarán ordenadas o bien Alfabéticamente o bien por cantidad de ocurrencias y luego alfabéticamente.-
Este caso es interesante si lo miramos más abstractamente. Lo que hace falta es un enumerador, que nos permita barrer la colección. Estos enumeradores tienen un método llamado MoveNext() y se conocen como genéricos, una de las funcionalidades de lenguajes como C#.
Tenemos para nuestra colección (que es un Diccionario: par/valor) un enumerador especifico denominado: IDictionaryEnumerator

Para el caso de la enumeración de las palabras ordenadas alfabéticamente:

// Retorna un enumerador para las palabras ordenadas alfabeticamente
public IDictionaryEnumerator GetPalabrasAlfabeticamenteEnumerator() {
return (IDictionaryEnumerator) ctdorDePalabras.GetEnumerator();
}
Observamos que la posibilidad de obtener un enumerador de una SortedList() esta a través del método GetEnumerator() (que es un IEnumerator).-
Y para el caso de la enumeración de las palabras ordenadas por ocurrencia y alfabéticamente:
public IDictionaryEnumerator GetPalabrasPorOccurrenciaEnumerator() {

}