Como desenhar um objeto imutável com inicialização complexo
-
21-08-2019 - |
Pergunta
Eu estou aprendendo sobre DDD, e já se deparou com a afirmação de que "valor-objetos" devem ser imutáveis. Eu entendo que isso significa que o estado de objetos não devem mudar após ele ter sido criado. Esta é uma espécie de uma nova maneira de pensar para mim, mas faz sentido em muitos casos.
Ok, então eu começar a criar valor-objetos imutáveis.
- I certifique-se que tomar todo o estado como parâmetros para o construtor,
- Eu não adiciono setters de propriedade,
- e certifique-se há métodos estão autorizados a modificar o conteúdo (somente retornar novas instâncias).
Mas agora eu quero criar esse objeto de valor que irá conter 8 valores numéricos diferentes. Se eu criar um construtor que tem 8 parâmetros numéricos eu sinto que ele não vai ser muito fácil de usar, ou melhor - que vai ser fácil cometer um erro ao passar nos números. Isso não pode ser bom design.
Assim, as perguntas é: Existem outras maneiras de fazer o meu objeto imutável melhor .., qualquer mágica que pode ser feito em C # para superar uma lista de parâmetros de comprimento no construtor? Estou muito interessado em ouvir suas idéias ..
UPDATE: Antes que alguém o menciona, uma idéia tem sido discutida aqui: em C # - o que você acha
estaria interessado em ouvir outras sugestões ou comentários embora.
Solução
Use um construtor:
public class Entity
{
public class Builder
{
private int _field1;
private int _field2;
private int _field3;
public Builder WithField1(int value) { _field1 = value; return this; }
public Builder WithField2(int value) { _field2 = value; return this; }
public Builder WithField3(int value) { _field3 = value; return this; }
public Entity Build() { return new Entity(_field1, _field2, _field3); }
}
private int _field1;
private int _field2;
private int _field3;
private Entity(int field1, int field2, int field3)
{
// Set the fields.
}
public int Field1 { get { return _field1; } }
public int Field2 { get { return _field2; } }
public int Field3 { get { return _field3; } }
public static Builder Build() { return new Builder(); }
}
Em seguida, criá-la como:
Entity myEntity = Entity.Build()
.WithField1(123)
.WithField2(456)
.WithField3(789)
.Build()
Se alguns dos parâmetros são opcionais você não precisa chamar o método WithXXX e podem ter valores padrão.
Outras dicas
No momento, você teria que usar um construtor com muitos argumentos, ou um construtor. No C # 4.0 (VS2010), você pode usar argumentos nomeados / opcionais para alcançar algo semelhante ao C # 3.0 Object-initializers - veja aqui . O exemplo no blog é:
Person p = new Person ( forename: "Fred", surname: "Flintstone" );
Mas você pode facilmente ver como algo semelhante pode aplicar para qualquer construtor (ou outro método complexo). Comparar com o C # 3.0 objeto-inicializador de sintaxe (com um tipo mutável):
Person p = new Person { Forename = "Fred", Surname = "Flintstone" };
Não há muito a diferenciá-las, realmente.
Jon Skeet publicou algumas reflexões sobre este assunto também, aqui .
Em cima da minha cabeça, duas respostas diferentes vêm à mente ...
... o primeiro, e provavelmente mais simples, é usar uma fábrica de objeto (ou construtor) como um auxiliar que garante que você fazer as coisas direito.
inicialização objeto seria algo como isto:
var factory = new ObjectFactory();
factory.Fimble = 32;
factory.Flummix = "Nearly";
var mine = factory.CreateInstance();
... o segundo é para criar o seu objeto como um convencional, mutável, objeto com a função de bloqueio () ou Freeze (). Todos os seus mutators deve verificar para ver se o objeto foi bloqueado, e lançar uma exceção se ele tem.
inicialização objeto seria algo como isto:
var mine = new myImmutableObject();
mine.Fimble = 32;
mine.Flummix = "Nearly";
mine.Lock(); // Now it's immutable.
Qual o método para tomar depende muito de seu contexto - a fábrica tem a vantagem de ser conveniente se você tem uma série de objetos semelhantes para construir, mas introduz uma outra classe de escrever e manter. Um meio objeto bloqueável há apenas uma classe, mas outros usuários pode ter erros de execução inesperados, e teste é mais difícil.
Embora seja provavelmente parte do domínio do que você está fazendo, e, portanto, a minha sugestão pode ser inválido, o que sobre a tentativa de quebrar os parâmetros 8 em grupos lógicos?
Sempre que vejo montes de parâmetros, eu sinto como se o objeto / método / contructor deveria ser mais simples.
Eu tenho confundia com a mesma pergunta como construtores complexos também é design ruim para mim. Eu também não sou um grande fã do conceito construtor como parece que muito código extra para manter. O que precisamos é imutabilidade picolé, o que significa que um objeto começa como mutável, onde você tem permissão para usar os setters de propriedade. Quando todas as propriedades são definidas, deve haver uma maneira de congelar o objeto em um estado imutável. Esta estratégia infelizmente não é suportado nativamente na linguagem C #. Por isso, acabou projetando o meu próprio padrão para a criação de objetos imutáveis ??como descrito nesta pergunta:
padrão objeto imutável em C # - o que você acha?
Anders Hejlsberg está falando sobre o suporte para este tipo de imutabilidade de 36:30 na entrevista a seguir:
especialista para especialista: Anders Hejlsberg - The Future of C #
Você pode usar a reflexão, a fim de inicializar todos os campos do objeto e preguiça para fazer "setter" como métodos (usando estilo funcional monádico), a fim de cadeia os métodos set / funções juntos.
Por exemplo:
Você pode usar esta classe base:
public class ImmutableObject<T>
{
private readonly Func<IEnumerable<KeyValuePair<string, object>>> initContainer;
protected ImmutableObject() {}
protected ImmutableObject(IEnumerable<KeyValuePair<string,object>> properties)
{
var fields = GetType().GetFields().Where(f=> f.IsPublic);
var fieldsAndValues =
from fieldInfo in fields
join keyValuePair in properties on fieldInfo.Name.ToLower() equals keyValuePair.Key.ToLower()
select new {fieldInfo, keyValuePair.Value};
fieldsAndValues.ToList().ForEach(fv=> fv.fieldInfo.SetValue(this,fv.Value));
}
protected ImmutableObject(Func<IEnumerable<KeyValuePair<string,object>>> init)
{
initContainer = init;
}
protected T setProperty(string propertyName, object propertyValue, bool lazy = true)
{
Func<IEnumerable<KeyValuePair<string, object>>> mergeFunc = delegate
{
var propertyDict = initContainer == null ? ObjectToDictonary () : initContainer();
return propertyDict.Select(p => p.Key == propertyName? new KeyValuePair<string, object>(propertyName, propertyValue) : p).ToList();
};
var containerConstructor = typeof(T).GetConstructors()
.First( ce => ce.GetParameters().Count() == 1 && ce.GetParameters()[0].ParameterType.Name == "Func`1");
return (T) (lazy ? containerConstructor.Invoke(new[] {mergeFunc}) : DictonaryToObject<T>(mergeFunc()));
}
private IEnumerable<KeyValuePair<string,object>> ObjectToDictonary()
{
var fields = GetType().GetFields().Where(f=> f.IsPublic);
return fields.Select(f=> new KeyValuePair<string,object>(f.Name, f.GetValue(this))).ToList();
}
private static object DictonaryToObject<T>(IEnumerable<KeyValuePair<string,object>> objectProperties)
{
var mainConstructor = typeof (T).GetConstructors()
.First(c => c.GetParameters().Count()== 1 && c.GetParameters().Any(p => p.ParameterType.Name == "IEnumerable`1") );
return mainConstructor.Invoke(new[]{objectProperties});
}
public T ToObject()
{
var properties = initContainer == null ? ObjectToDictonary() : initContainer();
return (T) DictonaryToObject<T>(properties);
}
}
Pode ser implementado assim:
public class State:ImmutableObject<State>
{
public State(){}
public State(IEnumerable<KeyValuePair<string,object>> properties):base(properties) {}
public State(Func<IEnumerable<KeyValuePair<string, object>>> func):base(func) {}
public readonly int SomeInt;
public State someInt(int someInt)
{
return setProperty("SomeInt", someInt);
}
public readonly string SomeString;
public State someString(string someString)
{
return setProperty("SomeString", someString);
}
}
e pode ser usado como este:
//creating new empty object
var state = new State();
// Set fields, will return an empty object with the "chained methods".
var s2 = state.someInt(3).someString("a string");
// Resolves all the "chained methods" and initialize the object setting all the fields by reflection.
var s3 = s2.ToObject();