Ir al contenido principal

Arquitectura — Catálogo de Patrones de Diseño

Este documento cataloga todos los patrones de diseño usados en Vali-Flow.Core, explicando por qué se eligió cada uno y dónde encontrarlo en el código.


1. Facade + Composition

Qué es: Un Facade es una clase que presenta una interfaz unificada hacia un conjunto de subsistemas. En lugar de heredar de múltiples clases (imposible en C#), la clase tiene instancias de esos subsistemas y delega las llamadas.

Dónde está: Builder/ValiFlow.cs

Por qué se usa:

  • C# no permite herencia de múltiples clases
  • Separar las 9 responsabilidades (strings, números, fechas, etc.) en clases independientes
  • Permite que ValiFlowQuery<T> comparta los mismos componentes pero exponga solo un subconjunto

El problema que resuelve: Sin este patrón, toda la lógica de validación (string, numérico, colecciones, fechas, etc.) viviría en una sola clase de miles de líneas.

Documentación detallada: facade-composition.md


2. CRTP (Curiously Recurring Template Pattern)

Qué es: Un patrón de C++ adaptado a C#. La clase base recibe el tipo concreto como parámetro genérico, lo que le permite declarar métodos que retornan el tipo concreto sin conocerlo.

Dónde está: Classes/Base/BaseExpression<TBuilder, T>

public class BaseExpression<TBuilder, T>
where TBuilder : BaseExpression<TBuilder, T>, new()
{
public TBuilder Or() // retorna el tipo concreto, no BaseExpression
{
_nextIsAnd = 0;
return (TBuilder)this;
}
}

// ValiFlow<T> hereda pasándose a sí mismo:
public class ValiFlow<T> : BaseExpression<ValiFlow<T>, T>

Por qué se usa: Permite que todos los métodos de la clase base (como Or(), And(), WithMessage()) retornen el tipo concreto del builder, manteniendo el encadenamiento tipado a lo largo de toda la cadena de métodos.

Sin CRTP: Los métodos de BaseExpression retornarían BaseExpression<TBuilder,T>, perdiendo el tipo. El usuario no podría llamar métodos de ValiFlow<T> después de un Or() porque el compilador solo conocería BaseExpression.


3. Builder (Fluent Builder)

Qué es: El patrón Builder separa la construcción de un objeto complejo de su representación. La variante "fluent" permite encadenar métodos de construcción.

Dónde está: Builder/ValiFlow.cs, Builder/ValiFlowQuery.cs, Builder/ValiSort.cs

var rule = new ValiFlow<User>()    // Builder
.NotNull(u => u.Email) // paso de construcción
.MinLength(u => u.Email, 5) // paso de construcción
.GreaterThan(u => u.Age, 18); // paso de construcción
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
// cada llamada retorna el builder para seguir encadenando

Por qué se usa:

  • La construcción de un predicado tiene un número variable de pasos (0 a N condiciones)
  • El objeto final (Expression<Func<T,bool>>) no existe hasta que se llama Build()
  • El API fluent es más legible que pasar todas las condiciones en un constructor

4. Immutable Builder (Freeze/Fork)

Qué es: Una variante del Builder donde el objeto de construcción tiene dos fases: mutable (durante la construcción) e inmutable (durante el uso). Las mutaciones sobre un objeto inmutable crean copias en lugar de modificar el original.

Dónde está: Classes/Base/BaseExpression.cs (campo _frozen, método Freeze, lógica de fork en cada método de mutación)

// Inspirado en IQueryable<T> de LINQ:
var query = dbContext.Users; // mutable conceptualmente
var filtered = query.Where(...); // no modifica query, crea uno nuevo
var sorted = filtered.OrderBy(...); // no modifica filtered, crea uno nuevo

// ValiFlow sigue el mismo patrón:
var base = new ValiFlow<User>().GreaterThan(u => u.Age, 18);
base.IsValid(user); // freeze implícito
var extended = base.IsTrue(u => u.IsActive); // fork, no modifica base

Por qué se usa:

  • Permite compartir una regla base y extenderla sin riesgo de modificarla
  • Hace que el builder sea thread-safe después de freeze (sin locks)
  • Patrón familiar para los desarrolladores de C# que conocen IQueryable

Documentación detallada: thread-safety.md


5. Value Object (ConditionEntry)

Qué es: Un Value Object es un objeto cuya identidad está definida por sus valores, no por su referencia. En C# se implementa típicamente como record.

Dónde está: Models/ConditionEntry.cs

internal sealed record ConditionEntry<T>
{
public Expression<Func<T, bool>> Condition { get; init; }
public bool IsAnd { get; init; }
public string? ErrorCode { get; init; }
// ...
}

Por qué es record y no class:

  • Los init setters garantizan que los campos no cambian después de la construcción
  • La sintaxis with permite crear copias modificadas de forma concisa:
var updated = existingEntry with { Message = "nuevo mensaje" };
// existingEntry no cambia; updated es un nuevo objeto con todo igual excepto Message

Esto se usa en MutateLastCondition para implementar WithMessage, WithError, WithSeverity sin mutar la lista.

Documentación detallada: condition-entry.md


6. Registry (ValiFlowGlobal)

Qué es: El patrón Registry es un objeto conocido globalmente (típicamente un singleton o clase estática) donde los componentes pueden registrarse y luego ser recuperados por otros componentes.

Dónde está: Builder/ValiFlowGlobal.cs

// Registro (durante startup de la aplicación)
ValiFlowGlobal.Register<User>(u => !u.IsDeleted);
ValiFlowGlobal.Register<User>(u => u.TenantId == currentTenantId);

// Uso (al construir queries)
var expr = builder.BuildWithGlobal();
// Produce: (condiciones del builder) AND (!u.IsDeleted) AND (u.TenantId == currentTenantId)

Por qué se usa:

  • Los filtros globales (soft-delete, multi-tenancy) son transversales a toda la aplicación
  • Sin este patrón, el desarrollador tendría que agregar .IsTrue(u => !u.IsDeleted) en cada query
  • Un solo punto de registro elimina la duplicación

Limitación importante: Es un diccionario estático. Persiste por toda la vida del proceso. En tests, llamar ValiFlowGlobal.ClearAll() en el teardown para evitar contaminación entre tests.


7. Strategy (ValiSort)

Qué es: El patrón Strategy define una familia de algoritmos, encapsula cada uno y los hace intercambiables. El cliente elige el algoritmo sin conocer los detalles de implementación.

Dónde está: Builder/ValiSort.cs (dos estrategias: IQueryable vs IEnumerable)

ValiSort<T> tiene dos estrategias de ordenamiento:

  • Estrategia IQueryable: usa Expression.Call(typeof(Queryable), "OrderBy", ...) para construir una llamada a método en el árbol de expresión, que EF Core puede traducir a ORDER BY en SQL.
  • Estrategia IEnumerable: compila delegates Func<IEnumerable<T>, IOrderedEnumerable<T>> en el momento del registro (no en Apply()).
var sort = new ValiSort<User>()
.By(u => u.LastName)
.ThenBy(u => u.FirstName, descending: true);

// Estrategia 1: IQueryable (EF Core)
IOrderedQueryable<User> sorted = sort.Apply(dbContext.Users);
// Genera: ORDER BY LastName ASC, FirstName DESC

// Estrategia 2: IEnumerable (en memoria)
IOrderedEnumerable<User> sorted = sort.Apply(inMemoryList);
// Ejecuta los delegates pre-compilados directamente

Por qué se usa:

  • El mismo builder de ordenamiento funciona con ambas fuentes de datos
  • Los delegates in-memory se compilan al registrar el criterio (no en Apply), evitando compilación en cada uso
  • La elección de estrategia es automática según el tipo del argumento de Apply

8. Visitor (ParameterReplacer, ForceCloneVisitor, ExpressionExplainer)

Qué es: El patrón Visitor permite definir operaciones sobre una estructura de objetos sin modificar las clases de esa estructura. Se "visita" cada nodo del árbol y se aplica la operación.

Dónde está: Utils/ExpressionHelpers.cs (ParameterReplacer, ForceCloneVisitor), Utils/ExpressionExplainer.cs

Los tres implementan ExpressionVisitor de .NET y sobreescriben los métodos de visita para distintos tipos de nodo:

// ParameterReplacer: reemplaza un parámetro por una expresión
protected override Expression VisitParameter(ParameterExpression node)
=> node == _old ? _new : base.VisitParameter(node);

// ForceCloneVisitor: crea copias de cada nodo
protected override Expression VisitMember(MemberExpression node)
{
var expr = Visit(node.Expression);
return Expression.MakeMemberAccess(expr, node.Member);
// Nuevo nodo, misma estructura
}

// ExpressionExplainer: convierte nodos a texto
protected override Expression VisitBinary(BinaryExpression node)
{
_sb.Append("(");
Visit(node.Left);
_sb.Append($" {NodeTypeToString(node.NodeType)} ");
Visit(node.Right);
_sb.Append(")");
return node;
}

Por qué se usa:

  • Los árboles de expresión son estructuras heterogéneas (distintos tipos de nodo)
  • El patrón Visitor permite traversal sin necesidad de casting manual o switch gigante
  • ExpressionVisitor de .NET provee la infraestructura base

Documentación detallada: expression-visitors.md


9. Source Generator (Roslyn)

Qué es: Técnicamente no es un patrón de diseño del GoF, sino una técnica de metaprogramación. Durante la compilación, el generador analiza el código fuente y produce nuevo código que se agrega al proyecto.

Dónde está: Proyecto separado Vali-Flow.Core.SourceGenerators (referenciado como Analyzer en el .csproj)

Por qué se usa:

  • Elimina ~800 líneas de boilerplate (métodos de delegación de una línea)
  • Garantiza que ValiFlow<T> siempre está sincronizado con las interfaces
  • La alternativa (reflection) tendría overhead en runtime y no generaría IntelliSense

Documentación detallada: source-generator.md


10. Analyzer (Roslyn)

Qué es: Un Analyzer de Roslyn analiza el código fuente y emite diagnósticos (warnings/errors). Similar a un linter pero con acceso completo al modelo semántico del compilador.

Dónde está: ValiFlowNonEfMethodAnalyzer (en el mismo proyecto de Source Generators)

Diagnóstico VF001: Detecta llamadas a métodos no-EF-safe en instancias de ValiFlowQuery<T>.

Por qué se usa:

  • Sin el Analyzer, el error de "método no traducible" solo aparece en runtime
  • Con el Analyzer, aparece en compilación (o en el IDE en tiempo real)
  • Es la solución correcta cuando la invariante que se quiere proteger es de tipo semántico (no de tipos)

Documentación detallada: ef-core-safety.md


Tabla resumen

PatrónComponenteProblema que resuelve
Facade + CompositionValiFlow<T>API unificada sin herencia múltiple
CRTPBaseExpression<TBuilder,T>Method chaining con tipo correcto
Fluent BuilderValiFlow<T>, ValiSort<T>Construcción incremental de predicados
Immutable BuilderBaseExpression<TBuilder,T>Thread safety en la fase de uso
Value ObjectConditionEntry<T>Condiciones inmutables con mutación controlada
RegistryValiFlowGlobalFiltros transversales sin repetición
StrategyValiSort<T>Mismo builder para IQueryable e IEnumerable
VisitorParameterReplacer, ForceCloneVisitor, ExpressionExplainerTraversal y transformación de expression trees
Source GeneratorForwardingGeneratorEliminación de boilerplate de delegación
Roslyn AnalyzerValiFlowNonEfMethodAnalyzerDetección de errores en tiempo de compilación