Ir al contenido principal

Arquitectura — Patrón Facade + Composition

El problema que motiva este diseño

ValiFlow<T> debe exponer métodos para validar strings, números, colecciones, fechas, booleans, y más. Hay más de 250 métodos públicos en total. Si todo ese código viviera en una sola clase, tendría:

  • Miles de líneas en un solo archivo
  • Responsabilidades mezcladas (lógica de strings junto con lógica de fechas)
  • Un archivo imposible de mantener y de revisar en un PR

La solución obvia sería usar herencia múltiple: que ValiFlow<T> herede de StringValidator, NumericValidator, etc. Pero C# no permite herencia múltiple de clases.

La solución elegida combina dos patrones: Facade y Composition.


Qué es el patrón Facade

Un Facade es una clase que presenta una interfaz simplificada hacia un conjunto de subsistemas más complejos. El Facade no implementa la lógica; la delega.

Analogía: un panel de control de un avión. El piloto interactúa con un conjunto de botones y palancas unificados. Detrás de ese panel hay docenas de subsistemas independientes (hidráulico, eléctrico, motores). El panel es el Facade.

En Vali-Flow:

ValiFlow<T>  ←  Facade
(el panel de control del usuario)

↓ delega a ↓

StringExpression ← subsistema de strings
NumericExpression ← subsistema de números
CollectionExpression ← subsistema de colecciones
DateTimeExpression ← subsistema de fechas
... etc.

Qué es el patrón Composition (frente a herencia)

En lugar de que ValiFlow<T> sea un StringExpression y sea un NumericExpression (herencia), ValiFlow<T> tiene un StringExpression y tiene un NumericExpression (composition).

// Herencia (NO se usa en Vali-Flow) — imposible en C# con múltiples clases
public class ValiFlow<T>
: StringExpression<T> // error: C# no permite herencia de múltiples clases
, NumericExpression<T>
, CollectionExpression<T>
// ...

// Composition (lo que SÍ se usa)
public class ValiFlow<T>
{
private readonly IStringExpression<ValiFlow<T>, T> _stringExpression;
private readonly INumericExpression<ValiFlow<T>, T> _numericExpression;
private readonly ICollectionExpression<ValiFlow<T>, T> _collectionExpression;
// ...
}

Cuando el usuario llama builder.MinLength(...), ValiFlow<T> simplemente delega:

public ValiFlow<T> MinLength(Expression<Func<T, string?>> selector, int min)
=> _stringExpression.MinLength(selector, min);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// toda la lógica real está en StringExpression

La estructura completa de ValiFlow<T>

public partial class ValiFlow<T> : BaseExpression<ValiFlow<T>, T>,
IBooleanExpression<ValiFlow<T>, T>,
IComparisonExpression<ValiFlow<T>, T>,
ICollectionExpression<ValiFlow<T>, T>,
IStringExpression<ValiFlow<T>, T>,
INumericExpression<ValiFlow<T>, T>,
IDateTimeExpression<ValiFlow<T>, T>,
IDateTimeOffsetExpression<ValiFlow<T>, T>,
IDateOnlyExpression<ValiFlow<T>, T>,
ITimeOnlyExpression<ValiFlow<T>, T>
{
// Los 9 campos de composition (marcados con [ForwardInterface])
[ForwardInterface]
private readonly IBooleanExpression<ValiFlow<T>, T> _booleanExpression;
[ForwardInterface]
private readonly ICollectionExpression<ValiFlow<T>, T> _collectionExpression;
[ForwardInterface]
private readonly IComparisonExpression<ValiFlow<T>, T> _comparisonExpression;
[ForwardInterface]
private readonly IStringExpression<ValiFlow<T>, T> _stringExpression;
[ForwardInterface]
private readonly INumericExpression<ValiFlow<T>, T> _numericExpression;
[ForwardInterface]
private readonly IDateTimeExpression<ValiFlow<T>, T> _dateTimeExpression;
[ForwardInterface]
private readonly IDateTimeOffsetExpression<ValiFlow<T>, T> _dateTimeOffsetExpression;
[ForwardInterface]
private readonly IDateOnlyExpression<ValiFlow<T>, T> _dateOnlyExpression;
[ForwardInterface]
private readonly ITimeOnlyExpression<ValiFlow<T>, T> _timeOnlyExpression;

public ValiFlow()
{
// Cada componente recibe `this` como referencia al builder
// para poder retornarlo al final de cada método (fluent chaining)
_booleanExpression = new BooleanExpression<ValiFlow<T>, T>(this);
_collectionExpression = new CollectionExpression<ValiFlow<T>, T>(this);
_comparisonExpression = new ComparisonExpression<ValiFlow<T>, T>(this);
_stringExpression = new StringExpression<ValiFlow<T>, T>(this);
_numericExpression = new NumericExpression<ValiFlow<T>, T>(this);
_dateTimeExpression = new DateTimeExpression<ValiFlow<T>, T>(this);
_dateTimeOffsetExpression = new DateTimeOffsetExpression<ValiFlow<T>, T>(this);
_dateOnlyExpression = new DateOnlyExpression<ValiFlow<T>, T>(this);
_timeOnlyExpression = new TimeOnlyExpression<ValiFlow<T>, T>(this);
}
}

Cómo los componentes llaman de vuelta al builder

Cada componente especializado recibe una referencia al builder padre (this) en su constructor. Cuando un método de componente agrega una condición, llama a _builder.Add(...) que vive en BaseExpression. Cuando termina, retorna _builder (no this) para que el encadenamiento continúe sobre el builder principal.

// Dentro de StringExpression<TBuilder, T>
public class StringExpression<TBuilder, T>
where TBuilder : BaseExpression<TBuilder, T>, new()
{
private readonly TBuilder _builder; // referencia al ValiFlow<T> padre

public StringExpression(TBuilder builder)
{
_builder = builder;
}

public TBuilder MinLength(Expression<Func<T, string?>> selector, int min)
{
// Construye el árbol de expresión
Expression<Func<T, bool>> expr = x =>
selector.Compile()(x) != null &&
selector.Compile()(x).Length >= min;

// Lo registra en el motor central (BaseExpression)
_builder.Add(expr);

// Retorna el builder padre (ValiFlow<T>), no this (StringExpression)
return _builder;
}
}

Sin este diseño, el encadenamiento rompería el tipo. Si MinLength retornara this (un StringExpression), el usuario no podría llamar .GreaterThan(...) a continuación porque StringExpression no tiene ese método.


El parámetro genérico TBuilder en los componentes

Todos los componentes son genéricos en TBuilder:

public class StringExpression<TBuilder, T>
where TBuilder : BaseExpression<TBuilder, T>, new()

Esto permite que los mismos componentes sean usados tanto por ValiFlow<T> como por ValiFlowQuery<T>:

// ValiFlow usa los componentes con TBuilder = ValiFlow<T>
private readonly IStringExpression<ValiFlow<T>, T> _stringExpression
= new StringExpression<ValiFlow<T>, T>(this);

// ValiFlowQuery usa los mismos componentes con TBuilder = ValiFlowQuery<T>
private readonly IStringExpression<ValiFlowQuery<T>, T> _stringExpression
= new StringExpression<ValiFlowQuery<T>, T>(this);

El código de los componentes no sabe ni le importa si está sirviendo a ValiFlow o a ValiFlowQuery.


La jerarquía de interfaces

loading...

IExpressionAnnotator<TBuilder> está separado de IExpressionBuilder<TBuilder,T> deliberadamente. Esto permite que un consumidor que solo necesita anotar condiciones (sin construir nuevas) dependa solo de la interfaz estrecha, en lugar del contrato completo.


Los métodos de delegación: el problema sin el source generator

Sin el source generator, ValiFlow<T> necesitaría escribir manualmente un método de delegación por cada método de cada interfaz. Para ilustrar el problema:

// Solo los métodos de string (~40 métodos):
public ValiFlow<T> MinLength(Expression<Func<T,string?>> s, int min)
=> _stringExpression.MinLength(s, min);
public ValiFlow<T> MaxLength(Expression<Func<T,string?>> s, int max)
=> _stringExpression.MaxLength(s, max);
public ValiFlow<T> ExactLength(Expression<Func<T,string?>> s, int len)
=> _stringExpression.ExactLength(s, len);
public ValiFlow<T> IsEmail(Expression<Func<T,string?>> s)
=> _stringExpression.IsEmail(s);
// ... x40 para strings
// ... x50 para numéricos
// ... x30 para colecciones
// ... x40 para DateTime
// ... etc.
// Total: ~250 métodos de una línea cada uno

800+ líneas de boilerplate que no contienen ninguna lógica. El source generator los produce automáticamente. Ver source-generator.md.


Qué ocurre si no existiera este patrón

Si todo el código viviera en ValiFlow<T> directamente:

  • El archivo tendría 3000+ líneas
  • Cualquier cambio en validación de strings requeriría abrir el mismo archivo que lógica de fechas
  • Las pruebas de strings y de números estarían mezcladas
  • ValiFlowQuery<T> (la variante EF Core) tendría que duplicar todo el código, no solo los métodos que difieren

Con Facade + Composition:

  • Cada dominio tiene su propio archivo de ~200-300 líneas
  • Las pruebas pueden enfocarse en el dominio específico
  • ValiFlowQuery<T> comparte exactamente los mismos componentes y solo omite los campos para dominios no-EF-safe