Modello di progettazione del costruttore con eredità: esiste un modo migliore?
Domanda
Sto creando una serie di costruttori per ripulire la sintassi che crea classi di dominio per i miei mock come parte del miglioramento dei nostri test unitari complessivi. I miei costruttori essenzialmente popolano una classe di dominio (come un Schedule
) con alcuni valori determinati invocando l'appropriato WithXXX
e concatenandoli insieme.
Ho riscontrato un po 'di comunanza tra i miei costruttori e voglio sottrarlo in una classe base per aumentare il riutilizzo del codice. Sfortunatamente quello che finisco sembra:
public abstract class BaseBuilder<T,BLDR> where BLDR : BaseBuilder<T,BLDR>
where T : new()
{
public abstract T Build();
protected int Id { get; private set; }
protected abstract BLDR This { get; }
public BLDR WithId(int id)
{
Id = id;
return This;
}
}
Prendi nota dell'estratto protetto BLDR Questo {get; }
.
Un'implementazione di esempio di un generatore di classi di domini è:
public class ScheduleIntervalBuilder :
BaseBuilder<ScheduleInterval,ScheduleIntervalBuilder>
{
private int _scheduleId;
// ...
// UG! here's the problem:
protected override ScheduleIntervalBuilder This
{
get { return this; }
}
public override ScheduleInterval Build()
{
return new ScheduleInterval
{
Id = base.Id,
ScheduleId = _scheduleId
// ...
};
}
public ScheduleIntervalBuilder WithScheduleId(int scheduleId)
{
_scheduleId = scheduleId;
return this;
}
// ...
}
Poiché BLDR non è di tipo BaseBuilder, non posso usare restituirlo
nel metodo WithId (int)
di BaseBuilder
.
Sta esponendo il tipo figlio con la proprietà abstract BLDR Questo {get; }
la mia unica opzione qui, o mi sto perdendo qualche trucco di sintassi?
Aggiornamento (poiché posso mostrare perché lo sto facendo un po 'più chiaramente):
Il risultato finale è avere costruttori che costruiscono classi di dominio profilate che ci si aspetterebbe di recuperare dal database in un formato leggibile da [programmatore]. Non c'è niente di sbagliato in ...
mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
new Schedule
{
ScheduleId = 1
// ...
}
);
dato che è già abbastanza leggibile. La sintassi del generatore alternativo è:
mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
new ScheduleBuilder()
.WithId(1)
// ...
.Build()
);
il vantaggio che sto cercando di utilizzare i costruttori (e implementare tutti questi metodi WithXXX
) è quello di sottrarre la creazione di proprietà complesse (espandere automaticamente i valori di ricerca del database con la ricerca corretta .KnownValues ??
senza colpire il database ovviamente) e avere il builder che fornisce profili di test comunemente riutilizzabili per le classi di dominio ...
mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
new ScheduleBuilder()
.AsOneDay()
.Build()
);
Soluzione
Tutto quello che posso dire è che se è un modo per farlo, voglio saperlo anche io - uso esattamente questo schema nel mio Porta buffer protocollo . In effetti, sono felice di vedere che qualcun altro ha fatto ricorso ad esso - significa che almeno in qualche modo avremo ragione!
Altri suggerimenti
So che questa è una vecchia domanda, ma penso che puoi usare un cast semplice per evitare il abstract BLDR This {get; }
Il codice risultante sarebbe quindi:
public abstract class BaseBuilder<T, BLDR> where BLDR : BaseBuilder<T, BLDR>
where T : new()
{
public abstract T Build();
protected int Id { get; private set; }
public BLDR WithId(int id)
{
_id = id;
return (BLDR)this;
}
}
public class ScheduleIntervalBuilder :
BaseBuilder<ScheduleInterval,ScheduleIntervalBuilder>
{
private int _scheduleId;
// ...
public override ScheduleInterval Build()
{
return new ScheduleInterval
{
Id = base.Id,
ScheduleId = _scheduleId
// ...
};
}
public ScheduleIntervalBuilder WithScheduleId(int scheduleId)
{
_scheduleId = scheduleId;
return this;
}
// ...
}
Ovviamente potresti incapsulare il builder con
protected BLDR This
{
get
{
return (BLDR)this;
}
}
Questa è una buona strategia di implementazione per C #.
Alcune altre lingue (non riesco a pensare al nome del linguaggio di ricerca in cui ho visto questo) hanno sistemi di tipi che supportano una covariante "auto" / ";" direttamente o hanno altri modi intelligenti per esprimere questo modello, ma con il sistema di tipi di C #, questa è una buona (unica?) soluzione.