Me siento muy feliz de poder ofrecerles esta nueva publicación y este nuevo
proyecto Open Source. Naturalmente seguiremos evolucionando el proyecto de tests
de c#, pero he querido hacer este paréntesis para hablarles del patrón
CQRS, o lo que es lo mismo, Command Query Responsibility Segregation.
Esta clase es la representación lógica del concepto teórico de una figura
geométrica. En este caso son cuadrados dentro de un plano, pero bien podríamos
incorporar el concepto de cualquier otro polígono.
Una primera versión plenamente operativa del proyecto GEOW está ya
disponible para todos Uds. en GitHub. Les recomiendo encarecidamente que la clonen y jueguen con ella. No solo
tiene un elevado potencial pedagógico, además sus propiedades visuales
son altamente hipnóticas, por lo que les pido mucha precaución a la hora de
ponerlo a funcionar :)
Por esta misma razón me he decidido a crear un canal en
YouTube para irles mostrando videitos con los resultados de los dos proyectos
que nos traemos entre manos. Espero lo disfruten.
Son muchos los detalles técnicos que se desprenden en esta publicación, además
de la propia implementación del patrón CQRS. Por ejemplo:
- Aprenderás a manejar proyectos de base de datos SQL Server con Visual Studio y a usar la herramienta de comparación de esquemas.
- Seguirán profundizando y practicando los fundamentos básicos de la programación orientada a objetos.
- Darás unos interesantes primeros pasos con la clase System.Drawing.
Solemos pensar que las bases de datos adquieren complejidad cuando crecen,
cuando se vuelven voluminosas. No deja de ser cierto. En mi trayectoria como
DBA me he encontrado con bases de datos que concentraban grandísimas
cantidades de información en una sola tabla. Bueno, con una función de
partición y una buena estrategia de historificación la cosa quedaba más o
menos controlada. Otro elemento de estrés para una base de datos es, ya no el
tamaño de la misma, sino el número de usuarios que de forma concurrente accede
a la información. Todos nos hemos enfrentado alguna vez a situaciones de
bloqueos prolongados en una base de datos con muchos usuarios. En este sentido
que las transacciones que se gestionan en el motor de base de datos sean de
poca duración es fundamental. Pero la cosa se complica
in extremis cuando además de muchos usuarios concurrentes, nos
encontramos con elevadas exigencias simultáneas de lectura y escritura. Este
es sin duda el escenario más hostil para una base de datos, y es en este
escenario donde operar con el patrón CQRS nos puede resultar provechoso.
Les voy a dejar como siempre hago alguna referencia teórica: ingeniería en tiendanube - Potenciando las lecturas con Command Query
Responsibility Segregation (CQRS)
En esencia el patrón CQRS consiste en crear dos modelos separados para
lecturas y escrituras, lo cual en realidad puede materializarse de formas bien
distintas. Se consigue poder optimizar cada uno de los modelos atendiendo a
sus necesidades específicas. Como todo, existen pros y contras a valorar. El
incremento de la complejidad a la hora de implementar este patrón en nuestros
proyectos es inevitable. Necesitaremos además implementar una pieza para
sincronizar los modelos de escritura y lectura, pieza que será clave para el
buen rendimiento del sistema.
Sin más vamos a introducirnos en las vísceras del proyecto. Veamos primero sus
componentes:
La arquitectura, como podrán apreciar no puede ser más sencilla e intuitiva.
GEOW es el proyecto principal que contiene un Front-End consistente en
formularios de Windows. Y por lo demás, capa de dominio, negocio, y datos.
Trabajando con proyectos de bases de datos SQL Server
Permítanme detenerme un momento en el proyecto GEOWDB, ya que les será
necesario manipularlo para poner a funcionar el proyecto en sus
máquinas.
Se trata de un proyecto de tipo SQL Server, y el mismo contiene el
esquema de una base de datos. Les muestro como crear un nuevo proyecto de este
tipo:
Una vez creado el proyecto pueden Uds. hacer click derecho sobre el proyecto,
y elegir la opción Importar -> Base de datos. En la imagen la opción no
está habilitada porque es una operación que puede hacerse una sola vez.
El resultado es que se vuelca el esquema de su base de datos en un proyecto.
Esta es la mejor solución para gestionar el control de código fuente sobre el
código contenido en las bases de datos, como por ejemplo el de los
procedimientos almacenados. También sirve para controlar la evolución del
diseño de todos los objetos como tablas, vistas, etc.
Como pueden apreciar el proceso de importación crea una carpeta por cada
esquema de base de datos, y en su interior un fichero para cada objeto
perteneciente a ese esquema, a saber, tablas, procedimientos almacenados,
etc.
Ud. puede añadir elementos al proyecto. Yo he añadido aquí dos carpetas,
comparer, y scripts. Luego les explicaré sobre el contenido de
estas dos carpetas.
¿Cómo pueden Uds., a partir de este proyecto, crear sus propias bases de datos
GEOWDB? Mediante la herramienta de comparación de esquemas.
Esta herramienta súper útil toma un origen y un destino, compara esquemas, y
muestra los cambios al usuario. Se usa bastante para realizar despliegues de
bases de datos en distintos entornos. Tenga en cuenta que el origen y destino
de la comparación puede ser tanto una base de datos, como un proyecto de base
de datos, así, si toman Uds. como origen de la comparación este proyecto, y lo
comparan contra una base de datos en blanco, el sistema le devolverá todos los
cambios necesarios para crear el esquema del proyecto.
La herramienta, tras la comparación le mostrará los cambios de forma
individualizada por objeto, para que Ud. pueda seleccionar y des-seleccionar
los cambios que desea ejecutar. Por último encontrará en la parte superior un
botón Update Target, que realizará en el destino todas las operaciones
necesarias para equipararlo al esquema de origen.
Los parámetros de la comparación pueden ser guardados en un fichero con
extensión .scmp, para que Ud. pueda repetir fácilmente esa comparación.
Cuando digo los parámetros de la comparación me refiero a que quedan guardados
el origen y el destino, y la configuración de la comparación, esto es, qué
objetos del esquema desean ser comparados.
En la carpeta comparer he dejado un ejemplo; se trata del comparador
que he usado, con origen en mi base de datos y destino en el proyecto
GEOWDB, para ir manteniendo el proyecto actualizado con los cambios que
voy practicando en la base de datos. Así puedo ir creando versiones del
esquema y código de la base de datos en el mismo repositorio Git o TFS.
Una vez creada la base de datos, la misma necesita un conjunto de datos
maestros para funcionar. La carga de los mismos está en un script, en
la carpeta scripts, llamado cargainicial.sql que no contiene más
que unas pocas instrucciones Insert que Ud. deberá ejecutar. Puede hacerlo
igual desde el propio IDE de Visual Studio, o bien desde
SQL Server Management Studio.
Finalmente revise la cadena de conexión en el archivo app.config, para
que apunte a su nueva base de datos, y todo estará listo para funcionar.
GEOW Proyect - Conociendo el proyecto: primeras ejecuciones
Una vez tenemos montada y funcionando la base de datos, podemos entrar de
lleno en el proyecto que implementa el patrón CQRS.
Si ejecutamos el proyecto nos encontramos con esta interfaz gráfica:
Observen que hay una lista de objetos, cada uno de los cuales tiene asociadas
unas coordenadas, que son sus puntos de inicio. Los objetos son rectángulos de
un alto y ancho pre-determinados, que se irán moviendo por la parte vacía de
la pantalla, que es en realidad un control PictureBox. Si marcan Uds.
el check Activo verán que los objetos se pintan en el control e inician
su movimiento.
Se puede eliminar la lista inicial de objetos con el botón "Eliminar objetos" y crear los suyos propios, informando de sus características con los
controles que hay debajo de la lista. Defina por ejemplo 100 objetos de tamaño
10x10 con una posición y dirección iniciales, seleccione un color y pulse el
botón Añadir Objetos.
Vamos a ir viendo los resultados:
Aquí he añadido 10 cuadrados negros de 10x10 y los he puesto a moverse. Sin
embargo, como todos los cuadrados tienen la misma posición inicial, aparenta
haber uno solo.
Los cuadrados se moverán sin variar su dirección hasta que lleguen al borde
del PictureBox. Al llegar al borde cambiarán de dirección. En cambio si
Ud. activa el check Cambios de dirección, aleatoriamente los objetos
modificarán su dirección, de modo que los 10 objetos se irán separando unos de
otros:
Experimente con más o menos objetos, de distintos tamaños y colores. Pruebe a
marcar y des-marcar el check de traza, añada nuevos objetos en tiempo
real... los resultados son visualmente hermosos, pero no es éste el propósito
final de nuestros bonitos rectángulos.
GEOW Proyect - Conociendo el proyecto: adentrándonos en el código
Los fundamentos de la programación orientada a objetos se ha tenido presente
durante el desarrollo del proyecto. Observen por ejemplo la belleza
intrínseca de la clase PointObj.
using System; using System.Drawing; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace GEOWObj { public class PointObj { public enum enumDireccion { Derecha = 1, Izquierda = 2, Arriba = 3, Abajo = 4 } public Pen PenObj; public int Alto; public int Ancho; public Color ColorFigura; public List<PointObj> Followers; //para el modo persecución public bool EsLider = false; public bool EsPerseguidor = false; //dependencias private contracts.INeg_BufferPositions _negObj; #region privadas - propiedades private int _x; private int _y; private int AnchoLimite; private int AltoLimite; private enumDireccion _direccion; private string guidObject; private string nombreobjeto; private Int64 _idJourney; #endregion //comportamiento estandar public PointObj(Color PenColor, int X_Ini, int Y_ini, int Alto, int Ancho, enumDireccion DireccionInicial, int AnchoLienzo, int AltoLienzo, string Nombre, contracts.INeg_BufferPositions p_negObj) { this.PenObj = new Pen(PenColor); _x = X_Ini; _y = Y_ini; _direccion = DireccionInicial; this.Alto = Alto; this.Ancho = Ancho; this.ColorFigura = PenColor; NombreObjeto = Nombre; AnchoLimite = AnchoLienzo - Ancho - 5; AltoLimite = AltoLienzo - Alto - 55; _negObj = p_negObj; } //comportamiento Persecucion public PointObj(Color PenColor, int X_Ini, int Y_ini, int Alto, int Ancho, enumDireccion DireccionInicial, int AnchoLienzo, int AltoLienzo, string Nombre, bool esLider, bool esPerseguidor, contracts.INeg_BufferPositions p_negObj) { this.PenObj = new Pen(PenColor); _x = X_Ini; _y = Y_ini; _direccion = DireccionInicial; this.Alto = Alto; this.Ancho = Ancho; this.ColorFigura = PenColor; NombreObjeto = Nombre; AnchoLimite = AnchoLienzo - Ancho - 5; AltoLimite = AltoLienzo - Alto - 55; this.EsLider = esLider; this.EsPerseguidor = esPerseguidor; _negObj = p_negObj; } public enumDireccion Direccion { get { return _direccion; } set { bool permitido = true; switch (value) { case enumDireccion.Arriba: permitido = !(_y <= 10); break; case enumDireccion.Abajo: permitido = !(_y >= AltoLimite); break; case enumDireccion.Izquierda: permitido = !(_x <= 10); break; case enumDireccion.Derecha: permitido = !(_x >= AnchoLimite); break; } if (permitido) { _direccion = value; if (this.EsLider) { //informa del cambio de dirección a los perseguidores if (this.Followers != null) { foreach (var follower in this.Followers) { Task resprocess = new Task(() => follower.GetCoordenadasLider(this.X, this.Y)); resprocess.Start(); } } } } } } public int X { get { return _x; } set { if (value >= AnchoLimite || value < 10) { if (this.Y > (AltoLimite / 2)) { this.Direccion = enumDireccion.Arriba; } else { this.Direccion = enumDireccion.Abajo; } } else { _x = value; //Graba la posición DTO.InsertPositionDTO _pos = new DTO.InsertPositionDTO() { GUIDObject = this.GUIDObject, PointDesc = this.nombreobjeto, X = _x, Y = this.Y, Height = this.Alto, Width = this.Ancho, Color = this.ColorFigura.Name, idJourney = _idJourney }; Task _recordposition = new Task(() => _negObj.InsertPosition(_pos)); _recordposition.Start(); } } } public int Y { get { return _y; } set { if (value >= AltoLimite || value < 10) { if (this.X > AnchoLimite / 2) { this.Direccion = enumDireccion.Izquierda; } else { this.Direccion = enumDireccion.Derecha; } } else { _y = value; //Graba la posición DTO.InsertPositionDTO _pos = new DTO.InsertPositionDTO() { GUIDObject = this.GUIDObject, PointDesc = this.nombreobjeto, X = this.X, Y = _y, Height = this.Alto, Width = this.Ancho, Color = this.ColorFigura.Name, idJourney = _idJourney }; Task _recordposition = new Task(() => _negObj.InsertPosition(_pos)); _recordposition.Start(); } } } public string GUIDObject { get { return getASCIICode(this.NombreObjeto) + this.Alto.ToString() + this.Ancho.ToString() + getASCIICode(this.ColorFigura.Name); } } public string NombreObjeto { get { return nombreobjeto; } set { nombreobjeto=value.ToLower().Trim(); } } public void SetIdJourney(Int64 val) { _idJourney = val; } //-->> #region funciones de movimiento public void Muevelo() { MovimientoEstandar(); //switch (this.Comportamiento) //{ // case enumComportamiento.Estandar: // MovimientoEstandar(); // break; // case enumComportamiento.Persecucion: // MovimientoEstandar(); // break; //} } private void MovimientoEstandar() { switch (this.Direccion) { case PointObj.enumDireccion.Derecha: this.X += 10; break; case PointObj.enumDireccion.Izquierda: this.X -= 10; break; case PointObj.enumDireccion.Arriba: this.Y -= 10; break; case PointObj.enumDireccion.Abajo: this.Y += 10; break; } } #endregion //-->> #region funciones de comunicación public void GetCoordenadasLider(int X_Lider, int Y_Lider) { int distanciaX = X_Lider - this.X; int distanciaY = Y_Lider - this.Y; int distanciaAbsolutaX = distanciaX < 0 ? distanciaX * -1 : distanciaX; int distanciaAbsolutaY = distanciaX < 0 ? distanciaX * -1 : distanciaX; if (distanciaAbsolutaX > distanciaAbsolutaY) { //la prioridad es recortar en el eje X, pero solo se puede hacer si la dirección actual es arriba o abajo if (this.Direccion == enumDireccion.Arriba || this.Direccion == enumDireccion.Abajo) { //Recortamos en el eje X if (X_Lider < this.X) { this.Direccion = enumDireccion.Izquierda; } else { this.Direccion = enumDireccion.Derecha; } } else { //recortamos en eje Y aunque no sea la prioridad //a menos que la distancia sea "pequeña" en el eje Y en cuyo caso mantenemos la dirección if (distanciaAbsolutaY > 100) { if (Y_Lider < this.Y) { this.Direccion = enumDireccion.Arriba; } else { this.Direccion = enumDireccion.Abajo; } } } } else { //la prioridad es recortar en el eje Y, pero solo se puede si la dirección actual es izquierda o derecha if (this.Direccion == enumDireccion.Izquierda || this.Direccion == enumDireccion.Derecha) { //recortamos en eje Y if (Y_Lider < this.Y) { this.Direccion = enumDireccion.Arriba; } else { this.Direccion = enumDireccion.Abajo; } } else { //recortamos en eje X aunque no sea la prioridad //a menos que la distancia sea "pequeña" en el eje X en cuyo caso mantenemos la dirección if (distanciaAbsolutaX > 100) { { if (X_Lider < this.X) { this.Direccion = enumDireccion.Izquierda; } else { this.Direccion = enumDireccion.Derecha; } } } } } } #endregion //-->> #region Métodos privados private string getASCIICode(string texto) { string res = ""; for (int cont = 0; cont < texto.Length; cont++) { res += Encoding.ASCII.GetBytes(texto)[cont].ToString(); } return res; } #endregion } }
Se han implementado dos comportamientos en lo que se refiere al movimiento.
Uno, denominado estándar, consistente en que los cuadrados cambian de
dirección en tres circunstancias. (i) Si el check de
cambio de dirección está desmarcado, cambia la dirección del cuadrado
solo cuando llega al extremo de su contenedor, digamos, el área de
dibujo. Este valor, el tamaño del lienzo (o lo que es lo mismo, del control
PictureBox), pasa como parámetro en el constructor del objeto. (ii) Si
el check de cambios de dirección está marcado los cuadrados además de
rebotar en los extremos del lienzo, por cada nuevo movimiento tienen un cierto
número de probabilidades aleatorio de llevar a cabo un cambio de dirección.
(iii) Por último, se puede tomar control de una figura en concreto, y
determinar su movimiento de forma "manual". Esto se hace seleccionando en la
lista de objetos un elemento. Al hacerlo aparecer 4 botones de dirección en la
parte inferior del formulario. Además se pueden usar las teclas de
dirección del teclado (flechas arriba, abajo izquierda y derecha) para tomar control manual de la dirección de una figura en concreto (la seleccionada en el ListView).
Se ha programado un segundo comportamiento, denominado modo persecución, para
lo que usamos el segundo constructor de la clase PointObj. Este
constructor le añade una propiedad al objeto, la propiedad EsLider. En
cada ciclo de movimiento, las figuras líderes informaran a las figuras no
líder (perseguidores) si cambiaron de dirección. Pudieron cambiar de dirección
por cualquiera de las 3 causas anteriormente explicadas. Si hay cambio de
dirección en una figura líder, los perseguidores reaccionaran con un cambio de
dirección orientado a "cazar" a las figuras líderes, pero no sin cierta "torpeza". Mi intención es progresar con mejoras en el algoritmo de caza al líder. De momento el actual está programado en la función GetCoordenadasLider:
public void GetCoordenadasLider(int X_Lider, int Y_Lider) { int distanciaX = X_Lider - this.X; int distanciaY = Y_Lider - this.Y; int distanciaAbsolutaX = distanciaX < 0 ? distanciaX * -1 : distanciaX; int distanciaAbsolutaY = distanciaX < 0 ? distanciaX * -1 : distanciaX; if (distanciaAbsolutaX > distanciaAbsolutaY) { //la prioridad es recortar en el eje X, pero solo se puede hacer si la dirección actual es arriba o abajo if (this.Direccion == enumDireccion.Arriba || this.Direccion == enumDireccion.Abajo) { //Recortamos en el eje X if (X_Lider < this.X) { this.Direccion = enumDireccion.Izquierda; } else { this.Direccion = enumDireccion.Derecha; } } else { //recortamos en eje Y aunque no sea la prioridad //a menos que la distancia sea "pequeña" en el eje Y en cuyo caso mantenemos la dirección if (distanciaAbsolutaY > 100) { if (Y_Lider < this.Y) { this.Direccion = enumDireccion.Arriba; } else { this.Direccion = enumDireccion.Abajo; } } } } else { //la prioridad es recortar en el eje Y, pero solo se puede si la dirección actual es izquierda o derecha if (this.Direccion == enumDireccion.Izquierda || this.Direccion == enumDireccion.Derecha) { //recortamos en eje Y if (Y_Lider < this.Y) { this.Direccion = enumDireccion.Arriba; } else { this.Direccion = enumDireccion.Abajo; } } else { //recortamos en eje X aunque no sea la prioridad //a menos que la distancia sea "pequeña" en el eje X en cuyo caso mantenemos la dirección if (distanciaAbsolutaX > 100) { { if (X_Lider < this.X) { this.Direccion = enumDireccion.Izquierda; } else { this.Direccion = enumDireccion.Derecha; } } } } } }
Centremos un momento nuestra atención en el SET de la propiedad X de un rectángulo (una de las dos coordenadas X e Y que definen su posición actual dentro del PictureBox). Aquí encontramos el código que provoca el "rebote" en los bordes del lienzo. Y encontramos también algo si cabe más interesante. Una llamada asíncrona a un método llamado InsertPosition.
public int X { get { return _x; } set { if (value >= AnchoLimite || value < 10) { if (this.Y > (AltoLimite / 2)) { this.Direccion = enumDireccion.Arriba; } else { this.Direccion = enumDireccion.Abajo; } } else { _x = value; //Graba la posición DTO.InsertPositionDTO _pos = new DTO.InsertPositionDTO() { GUIDObject = this.GUIDObject, PointDesc = this.nombreobjeto, X = _x, Y = this.Y, Height = this.Alto, Width = this.Ancho, Color = this.ColorFigura.Name, idJourney = _idJourney }; Task _recordposition = new Task(() => _negObj.InsertPosition(_pos)); _recordposition.Start(); } } }
Comentarios
Publicar un comentario