Passa al contenuto principale

Factory Method in C#

Questa pagina mostra le forme idiomatiche del Factory Method in ASP.NET Core, dalla più leggera alla più strutturata.


1. Factory function (metodo statico)​

Idea​

Quando la costruzione ha logica non banale ma non richiede dipendenze, un metodo statico è la forma più semplice.

public class Order
{
public Guid Id { get; }
public Guid CustomerId { get; }
public IReadOnlyList<OrderLine> Lines { get; }
public DateTimeOffset CreatedAt { get; }

private Order(Guid id, Guid customerId, IReadOnlyList<OrderLine> lines, DateTimeOffset createdAt)
{
Id = id;
CustomerId = customerId;
Lines = lines;
CreatedAt = createdAt;
}

public static Order CreateNew(Guid customerId, IEnumerable<OrderLine> lines, TimeProvider clock)
{
var materialized = lines.ToList();
if (materialized.Count == 0)
throw new ArgumentException("Un ordine richiede almeno una riga.", nameof(lines));

return new Order(Guid.NewGuid(), customerId, materialized, clock.GetUtcNow());
}
}

Il costruttore è privato: l'unico modo di ottenere un Order valido è passare dalla factory. Le invarianti (almeno una riga, ID generato, timestamp) sono garantite alla costruzione.


2. Factory class registrata in DI​

Idea​

Quando la factory ha dipendenze proprie (configurazione, accesso al database, altri servizi) diventa una classe registrata nel container.

public interface IInvoiceNumberFactory
{
Task<string> NextAsync(Guid tenantId, CancellationToken ct);
}

public class InvoiceNumberFactory : IInvoiceNumberFactory
{
private readonly AppDbContext _db;
private readonly TimeProvider _clock;

public InvoiceNumberFactory(AppDbContext db, TimeProvider clock)
{
_db = db;
_clock = clock;
}

public async Task<string> NextAsync(Guid tenantId, CancellationToken ct)
{
var year = _clock.GetUtcNow().Year;
var counter = await _db.InvoiceCounters
.Where(c => c.TenantId == tenantId && c.Year == year)
.FirstAsync(ct);

counter.LastNumber++;
return $"{year}/{counter.LastNumber:D6}";
}
}

Registrazione​

builder.Services.AddScoped<IInvoiceNumberFactory, InvoiceNumberFactory>();

Il consumatore inietta IInvoiceNumberFactory e ottiene un numero pronto, senza conoscere la logica di numerazione.


3. Factory delegate con Func<T>​

Idea​

Quando la scelta dell'implementazione concreta dipende da un parametro noto solo a runtime (il payload di un messaggio, un tipo letto da configurazione, l'esito di una validazione) si registra una factory delegate che il container risolve.

Scenario: parser scelto in base al MIME type del file​

public interface IDocumentParser
{
string MimeType { get; }
Task<ParsedDocument> ParseAsync(Stream content, CancellationToken ct);
}

public class PdfParser : IDocumentParser { public string MimeType => "application/pdf"; /* ... */ }
public class DocxParser : IDocumentParser { public string MimeType => "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; /* ... */ }
public class CsvParser : IDocumentParser { public string MimeType => "text/csv"; /* ... */ }

Factory dedicata​

public interface IDocumentParserFactory
{
IDocumentParser For(string mimeType);
}

public class DocumentParserFactory : IDocumentParserFactory
{
private readonly Dictionary<string, IDocumentParser> _parsers;

public DocumentParserFactory(IEnumerable<IDocumentParser> parsers)
{
_parsers = parsers.ToDictionary(p => p.MimeType, StringComparer.OrdinalIgnoreCase);
}

public IDocumentParser For(string mimeType) =>
_parsers.TryGetValue(mimeType, out var parser)
? parser
: throw new NotSupportedException($"MIME type non supportato: {mimeType}");
}

Registrazione​

builder.Services.AddScoped<IDocumentParser, PdfParser>();
builder.Services.AddScoped<IDocumentParser, DocxParser>();
builder.Services.AddScoped<IDocumentParser, CsvParser>();
builder.Services.AddScoped<IDocumentParserFactory, DocumentParserFactory>();

La factory costruisce il dizionario una sola volta per richiesta (scope), poi serve i parser in O(1). Questo è esattamente il selettore di Strategy, espresso come factory dedicata.


Factory vs DI: quando una è meglio dell'altra​

SituazioneForma consigliata
Una sola implementazione, parametri noti a compile-timeIniettare direttamente l'interfaccia
Più implementazioni, scelta dichiarativa (chiave fissa)Keyed services
Più implementazioni, scelta dinamica da payloadFactory delegate (Func<T> o classe dedicata)
Costruzione con dipendenze, ma chiamata sporadicaFactory class registrata in DI
Oggetto di dominio con invarianti da proteggereFactory statica sul tipo stesso, costruttore privato

In generale: il container DI è la prima opzione. Si introduce una factory esplicita solo quando il container non è sufficiente, tipicamente perché la scelta dipende da dati che il container non conosce.


Anti-pattern: factory inutili​

Una factory che restituisce sempre new ConcreteType() senza logica aggiuntiva è solo indirezione. Va eliminata: il container fa già lo stesso lavoro, meglio.

Allo stesso modo, una «factory» il cui unico metodo accetta tutti i parametri del costruttore e li inoltra non sta costruendo nulla, sta solo aggiungendo un livello. Costruire direttamente è più chiaro.