Подтвердить что ты не робот

Как создать неизменяемый объект со сложной инициализацией

Я изучаю DDD и сталкиваюсь с утверждением, что "ценности-объекты" должны быть неизменными. Я понимаю, что это означает, что состояние объектов не должно меняться после его создания. Для меня это новый вид мышления, но во многих случаях это имеет смысл.

Хорошо, поэтому я начинаю создавать объекты неизменяемого значения.

  • Я уверен, что они принимают все состояние в качестве параметров конструктору,
  • Я не добавляю настройки свойств,
  • и убедитесь, что никакие методы не могут изменять содержимое (возвращать только новые экземпляры).

Но теперь я хочу создать этот объект значения, который будет содержать 8 различных числовых значений. Если я создаю конструктор с 8 числовыми параметрами, я чувствую, что это будет не очень легко использовать, точнее - будет легко совершить ошибку при передаче чисел. Это не может быть хорошим дизайном.

Итак, есть вопросы: есть ли другие способы сделать мой неизменный объект лучше.., любую магию, которая может быть сделана на С#, чтобы преодолеть длинный список параметров в конструкторе? Мне очень интересно услышать ваши идеи.

ОБНОВЛЕНИЕ:. Прежде чем кто-нибудь это упоминает, здесь обсуждалась одна идея: Неизменяемый шаблон объекта в С# - что вы думаете?

Было бы интересно услышать другие предложения или комментарии, хотя.

4b9b3361

Ответ 1

Использовать построитель:

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(); }
}

Затем создайте его так:

Entity myEntity = Entity.Build()
                   .WithField1(123)
                   .WithField2(456)
                   .WithField3(789)
                  .Build()

Если некоторые из параметров являются необязательными, вам не нужно вызывать метод WiспасибоXX, и они могут иметь значения по умолчанию.

Ответ 2

В настоящий момент вам придется использовать конструктор с большим количеством аргументов или строителем. В С# 4.0 (VS2010) вы можете использовать аргументы named/optional для достижения чего-то похожего на инициализаторы объектов С# 3.0 - см. здесь. Пример в блоге:

  Person p = new Person ( forename: "Fred", surname: "Flintstone" );

Но вы можете легко увидеть, как что-то подобное может применяться для любого конструктора (или другого сложного метода). Сравните с синтаксисом объекта-инициализатора С# 3.0 (с изменяемым типом):

 Person p = new Person { Forename = "Fred", Surname = "Flintstone" };

Не так много, чтобы рассказать им об этом.

Джон Скит опубликовал некоторые мысли по этому вопросу, здесь.

Ответ 3

Сверху моей головы приходят в голову два разных ответа...

... первым и, вероятно, самым простым, является использование объекта factory (или строителя) в качестве помощника, который гарантирует, что вы все поняли.

Инициализация объекта будет выглядеть так:

var factory = new ObjectFactory();
factory.Fimble = 32;
factory.Flummix = "Nearly";
var mine = factory.CreateInstance();

... второй заключается в создании вашего объекта как обычного, изменяемого объекта с помощью функции Lock() или Freeze(). Все ваши мутаторы должны проверить, был ли объект заблокирован, и выдавать исключение, если оно есть.

Инициализация объекта будет выглядеть так:

var mine = new myImmutableObject();
mine.Fimble = 32;
mine.Flummix = "Nearly";
mine.Lock(); // Now it immutable.

Какой способ принимать зависит от вашего контекста - преимущество имеет factory, если у вас есть ряд подобных объектов для построения, но он вводит другой класс для записи и поддержки. Запираемый объект означает, что существует только один класс, но другие пользователи могут получить неожиданные ошибки во время выполнения, а тестирование сложнее.

Ответ 4

Хотя это, вероятно, часть домена того, что вы делаете, и, таким образом, мое предложение может быть недействительным, а как пытаться разбить 8 параметров на логические группы?

Всякий раз, когда я вижу кучу параметров, я чувствую, что объект/метод/конструктор должен быть проще.

Ответ 5

Я был ошеломлен тем же вопросом, что и сложные конструкторы, также плохой дизайн для меня. Я также не большой поклонник концепции строителя, поскольку это похоже на слишком много дополнительного кода для поддержки. Нам нужна неизменяемость, это означает, что объект запускается как изменчивый, когда вам разрешено использовать средства настройки свойств. Когда все свойства установлены, должен быть способ замораживания объекта в неизменяемом состоянии. К сожалению, эта стратегия не поддерживается на языке С#. Поэтому я закончил разработку своего собственного шаблона для создания неизменяемых объектов, как описано в этом вопросе:

Неизменяемый шаблон объекта в С# - что вы думаете?

Андерс Хейлсберг говорит о поддержке этого типа неизменности с 36:30 в следующем интервью:

Эксперт эксперту: Андерс Хейлсберг - Будущее С#

Ответ 6

Вы можете использовать отражение, чтобы инициализировать все поля объекта и лень, чтобы сделать "setter" подобно методам (используя монадический функциональный стиль), чтобы объединить множество методов/функций вместе.

Например:

Вы можете использовать этот базовый класс:

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);
    }
}

Может быть реализовано так:

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);
    }
}

и может использоваться следующим образом:

//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();