padrão de projeto Builder com herança: se existe uma maneira melhor?
Pergunta
Estou criando uma série de construtores para limpar a sintaxe que cria classes de domínio para o meu simulações como parte de melhorar os nossos testes de unidade global. Meus construtores essencialmente preencher uma classe de domínio (como um Schedule
) com alguns valores determinados invocando o WithXXX
apropriado e encadeando-os.
Eu encontrei algo em comum entre os meus construtores e quero abstrato que afastado em uma classe base para aumentar a reutilização de código. Infelizmente o que eu acabar com aparência:
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;
}
}
Tome nota especial do protected abstract BLDR This { get; }
.
A implementação de exemplo de um construtor de classe de domínio é:
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;
}
// ...
}
Porque BLDR não é do tipo BaseBuilder eu não posso usar return this
no método WithId(int)
de BaseBuilder
.
É expor o tipo de criança com a propriedade abstract BLDR This { get; }
minha única opção aqui, ou estou faltando algum truque sintaxe?
Update (desde que eu posso mostrar por que estou fazendo isto um pouco mais claro):
O resultado final é ter construtores que classes de domínio construir perfilado que seria de esperar para recuperar do banco de dados em um [programador] formato legível. Não há nada de errado com ...
mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
new Schedule
{
ScheduleId = 1
// ...
}
);
como isso é muito legível já. A sintaxe construtor alternativa é:
mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
new ScheduleBuilder()
.WithId(1)
// ...
.Build()
);
a vantagem Estou procurando fora de usar construtores (e implementação de todos esses métodos WithXXX
) é abstrair criação propriedade afastado complexo (expandir automaticamente os nossos valores de pesquisa de banco de dados com o Lookup.KnownValues
correta sem bater o banco de dados, obviamente) e tendo o construtor fornecer perfis de teste comumente reutilizáveis ??para classes de domínio ...
mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
new ScheduleBuilder()
.AsOneDay()
.Build()
);
Solução
Tudo o que posso dizer é que, se houver é uma maneira de fazê-lo, eu quero saber sobre isso também - eu uso exatamente este padrão no meu Protocol Buffers porta . Na verdade, eu estou contente de ver que alguém tenha recorrido a ele - isso significa que estamos pelo menos um pouco provável que seja certo
Outras dicas
Eu sei que isto é uma questão de idade, mas eu acho que você pode usar um molde simples para evitar o abstract BLDR This { get; }
O código resultante seria então:
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;
}
// ...
}
Claro que você pode encapsular o construtor com
protected BLDR This
{
get
{
return (BLDR)this;
}
}
Esta é uma boa estratégia de implementação para C #.
Algumas outras línguas (não pode pensar em nome da linguagem de pesquisa que eu vi isso em) têm sistemas do tipo que quer o apoio de um "eu" covariante / "isto" diretamente, ou ter outras formas inteligentes de expressar esse padrão, mas com sistema de tipo C # 's, este é um bom (apenas?) solução.