Patrones de Diseño y Buenas Prácticas en C# .NET

Patrones de Diseño Creacionales

Singleton

Propósito: Evitar instancias duplicadas.

public class S
{
    private static S _i;
    private S() { }
    public static S Instance
    {
        get
        {
            if (_i == null) _i = new S();
            return _i;
        }
    }
}

Factory Method

Propósito: Uso de if/else para crear objetos.

public abstract class C
{
    public P Crear(string t)
    {
        var p = CrearP(t);
        p.Init();
        return p;
    }
    protected abstract P CrearP(string t);
}

public class CC : C
{
    protected override P CrearP(string t)
    {
        if (t == "A") return new PA();
        throw new ArgumentException();
    }
}

Patrones de Diseño Estructurales

Facade

Propósito: Varias clases llaman a los mismos servicios.

public class F
{
    private A _a = new A();
    private B _b = new B();
    public void Op(string id)
    {
        _a.P1(id);
        _b.P2(id);
    }
}

public class C
{
    private F _f;
    public C(F f) => _f = f;
    public void Op(string id) => _f.Op(id);
}

Patrones de Diseño de Comportamiento

Template Method

Propósito: Mismos pasos, varía el contenido.

public abstract class B
{
    public void Exec()
    {
        Pre();
        Step();
        Post();
    }
    protected virtual void Pre() { }
    protected abstract void Step();
    protected virtual void Post() { }
}

public class C : B
{
    protected override void Step() => Console.WriteLine("paso");
}

Strategy

Propósito: Uso de if/else por comportamiento.

public interface IS { void Exec(string d); }

public class C
{
    private IS _s;
    public C(IS s) => _s = s;
    public void Do(string d) => _s.Exec(d);
}

Observer

Propósito: Notificar a varios cuando cambia un estado.

public interface IO { void Update(string e); }

public class Sub
{
    private List<IO> _obs = new();
    public void Sub(IO o) => _obs.Add(o);
    public void Unsub(IO o) => _obs.Remove(o);
    private void Notify(string e)
    {
        foreach (var o in _obs) o.Update(e);
    }
    public void Change(string s) => Notify(s);
}

Configuración de Entity Framework Core

public class Ctx : DbContext
{
    public DbSet<E> Es { get; set; }
    public Ctx(DbContextOptions<Ctx> o) : base(o)
    {
        if (Database.IsSqlServer()) Database.Migrate();
    }
    protected override void OnModelCreating(ModelBuilder mb)
    {
        mb.Entity<E>().Property(e => e.N).IsRequired().HasMaxLength(100);
        mb.Entity<H>().HasOne(h => h.P).WithMany(p => p.Hs).HasForeignKey(h => h.PId);
    }
}

Configuración en appsettings.json:
"DefaultConnection": "Server=localhost,1433;Database=DB;User Id=sa;Password=Pass;TrustServerCertificate=true;"

Inyección de Dependencias:

builder.Services.AddDbContext<Ctx>(o => o.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IRepo, RepoSql>();

Carga de Datos en EF

  • Eager Loading: var list = _ctx.Libros.Include(l => l.Autor).ToList();
  • Eager Loading Anidado: var list = _ctx.Ps.Include(p => p.Ts).ThenInclude(t => t.E).ToList();
  • Explicit Loading (Objeto único): _ctx.Entry(l).Reference(l => l.Autor).Load();
  • Explicit Loading (Lista): _ctx.Entry(l).Collection(l => l.Rs).Load();

Implementación de DTOs y Servicios

public class Req { public string C { get; set; } public E ToEntity() => new() { C = this.C }; }
public class Res { public string C { get; set; } public static Res FromEntity(E e) => new() { C = e.C }; }

public class Svc
{
    public void Crear(Req r) { _repo.Save(r.ToEntity()); }
    public Res Get(int id) => Res.FromEntity(_repo.GetById(id));
}

Desarrollo Frontend con Blazor

@page "/ruta"
@inject IServicio Svc
@inject NavigationManager Nav

<button @onclick="Volver">Volver</button>

@code {
    private List<E> lista = new();
    protected override void OnInitialized() { lista = Svc.GetAll(); }
    private void Volver() => Nav.NavigateTo("/");
}

Ciclos de vida en DI:

  • AddSingleton: Persistencia en memoria.
  • AddScoped: Ciclo de vida por solicitud (ideal para EF Core).
  • AddTransient: Creación de nueva instancia cada vez (helpers).

Pruebas Unitarias y Testing

[TestClass]
public class T
{
    private Svc _sut;
    private ICol _fake;
    [TestInitialize]
    public void S()
    {
        _fake = new FakeCol();
        _sut = new Svc(_fake);
    }
    [TestMethod]
    public void M_C_R()
    {
        var r = _sut.M("d");
        Assert.AreEqual("e", r);
    }
}
  • Asserts comunes: AreEqual, IsNull, IsNotNull, IsTrue, IsFalse.
  • Excepciones esperadas: [ExpectedException(typeof(ArgEx))]
  • Base de datos en memoria: .UseInMemoryDatabase(Guid.NewGuid().ToString())
  • Stub: public class CS : IC { private DateTime _h; public CS(DateTime h) => _h = h; public DateTime Now => _h; }

TDD (Test Driven Development): ✅ Cada if/else/throw tiene su test. ❌ Un throw sin test no es TDD.

Principios FIRST:

  • F (Fast): Rápido.
  • I (Independent): Independiente.
  • R (Repeatable): Repetible.
  • S (Self-validating): Tiene Assert.
  • T (Timely): Test antes del código.

Principios SOLID y Señales de Diseño

Señales de violación de SOLID

  • SRP (Single Responsibility): La clase hace demasiado.
  • OCP (Open/Closed): Agregar funcionalidad obliga a modificar el código.
  • DIP (Dependency Inversion): Uso de “new” para instanciar clases concretas.
  • ISP (Interface Segregation): La clase usa métodos que no necesita.

Señales para aplicar Patrones

  • Singleton: Cuando hay instancias duplicadas.
  • Factory: Cuando hay if/else para crear objetos.
  • Facade: Cuando se llaman a los mismos servicios repetidamente.
  • Template: Cuando hay mismos pasos pero varía el contenido.
  • Strategy: Cuando hay if/else basados en comportamiento.
  • Observer: Cuando se requiere notificar a varios componentes.