Introducción
Este artículo explora patrones de diseño efectivos y técnicas de codificación limpia en el contexto de una aplicación de escritorio desarrollada con el framework WPF, utilizando el patrón MVVM y el lenguaje C# sobre la plataforma .NET.
La aplicación en cuestión proporciona capacidades de diseño visual que permiten a los usuarios crear interfaces de forma intuitiva, añadiendo elementos como texto e imágenes de manera visual. Estos elementos son posteriormente compilados y convertidos en datos binarios para su transmisión a dispositivos periféricos. A continuación, se detallan aspectos específicos de la arquitectura y la implementación.
Separación de Capas: Modelo de Datos Independiente
La capa del modelo de datos debe mantenerse completamente independiente de la lógica de presentación. Esta separación garantiza que los tipos de datos solo contengan información relevante al dominio, sin mezclar dependencias de la interfaz de usuario.
La estructura jerárquica del modelo sigue este patrón:
- ProyectoVisual: Contenedor principle que agrupa múltiples diagramas
- DiagramaVisual: Representa una vista individual, conteniendo colecciones de elementos
- ElementoBase y sus derivados: Representan los distintos tipos de elementos manipulables
La capa de vista puede envolver estos tipos de modelo en tipos de vista especializados que añadan propiedades específicas de la interfaz de usuario.
Abstracción Mediante Herencia para Compilación
El proceso de compilación requiere tratamientos diferenciados según el tipo de contenido. Inicialmente, cada tipo tenía su propia implementación independiente, lo que llevó a la creación de una jerarquía de clases basada en un compilador abstracto.
El método de compilación se implementa de forma polimórfica en cada subclase. La orquestación superior se simplifica mediante el uso de polimorfismo:
public bool ProcesarProyecto(ProyectoVisual proyecto, out string mensajeError)
{
var compiladores = new CompiladorBase[]
{
new CompiladorTexto(proyecto),
new CompiladorImagen(proyecto),
new CompiladorOtroTipo(proyecto)
};
foreach (var compilador in compiladores)
{
compilador.Ejecutar();
if (!compilador.Validar(out mensajeError))
{
return false;
}
}
return true;
}
Principio DRY: Eliminación de Código Duplicado
Cada tipo de compilación necesita iterar sobre los elementos correspondientes del proyecto. Para evitar duplciar esta lógica de recorrido, se extrajo un método genérico reutilizable en la clase base:
protected static void RecorrerElementos<T>(
ProyectoVisual proyecto,
TipoElemento tipoFiltro,
Action<T> accion) where T : ElementoBase
{
foreach (var diagrama in proyecto.Diagramas)
{
var elementosFiltrados = diagrama.Elementos
.Where(e => e.Tipo == tipoFiltro)
.OfType<T>();
foreach (var elemento in elementosFiltrados)
{
accion(elemento);
}
}
}
La lógica específica se inyecta mediante el parámetro de acción. En el compilador de texto, esto se aplica así:
RecorrerElementos<ElementoTexto>(
Proyecto,
TipoElemento.Texto,
elem =>
{
// Lógica específica de procesamiento de texto
});
Esta aproximación delega la responsabilidad del comportamiento concreto al invocador, facilitando la extensibilidad. El patrón Template Method es una alternativa viable para este tipo de problemas.
Utilidades Estáticas para Operaciones Comunes
Las clases estáticas proporcionan un mecanismo efectivo para organizar métodos utilitarios independientes. Es importante distinguir entre métodos estáticos útiles y variables estáticas que actúan como estado global, siendo estas últimas una práctica que deteriora la arquitectura del software.
Persistencia de Datos con Serialización
Para el almacenamiento local de datos se implementó una utilidad genérica:
public static class GestorSerializacion
{
public static void GuardarBinario<T>(string rutaArchivo, T datos)
{
using (var flujo = new FileStream(rutaArchivo, FileMode.Create, FileAccess.Write))
{
var formateador = new BinaryFormatter();
formateador.Serialize(flujo, datos);
}
}
public static T CargarBinario<T>(string rutaArchivo, SerializationBinder enlazador = null)
{
if (!File.Exists(rutaArchivo))
{
return default(T);
}
using (var flujo = new FileStream(rutaArchivo, FileMode.Open, FileAccess.Read))
{
var formateador = new BinaryFormatter();
if (enlazador != null)
{
formateador.Binder = enlazador;
}
return (T)formateador.Deserialize(flujo);
}
}
}
Conversión entre Enteros y Arreglos de Bytes
Para la comunicación con dispositivos hardware, se requiere una utilidad de conversión que soporte tanto ordenamiento little-endian como big-endian:
public static class ConvertidorBytes
{
public static byte[] EnteroABytes(int valor, int longitud, bool littleEndian = true)
{
if (valor < 0) throw new ArgumentException("El valor no puede ser negativo");
if (longitud > 4) throw new ArgumentException("La longitud no puede exceder 4 bytes");
var bytesOriginales = BitConverter.GetBytes(valor);
if (BitConverter.IsLittleEndian != littleEndian)
{
Array.Reverse(bytesOriginales);
}
var resultado = new byte[longitud];
Array.Copy(bytesOriginales, bytesOriginales.Length - longitud, resultado, 0, longitud);
return resultado;
}
public static int BytesAEntero(byte[] datos, int inicio, int longitud, bool littleEndian = true)
{
if (longitud == 1) return datos[inicio];
var fragmento = new byte[longitud];
Array.Copy(datos, inicio, fragmento, 0, longitud);
if (!littleEndian)
{
Array.Reverse(fragmento);
}
return longitud switch
{
2 => (int)BitConverter.ToUInt16(fragmento, 0),
4 => BitConverter.ToInt32(fragmento, 0),
_ => throw new ArgumentException($"Longitud no soportada: {longitud}")
};
}
}
Las utilidades estáticas ofrecen la ventaja de ser completamente independientes, sin dependencias externas ni necesidad de inicialización, lo que facilita su uso inmediato en cualquier parte del código.