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

StackOverflow https://stackoverflow.com/questions/355172

Вопрос

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

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

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

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

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

Обновить: Прежде чем кто-либо упомянет об этом, здесь была обсуждена одна идея:Шаблон неизменяемого объекта в C # - что вы думаете?

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

Это было полезно?

Решение

Используйте конструктор:

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

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

Другие советы

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

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

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

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

На самом деле, их особо не отличишь друг от друга.

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

Навскидку мне на ум приходят два разных ответа ...

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

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

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's immutable.

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

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

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

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

Шаблон неизменяемого объекта в C # - что вы думаете?

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

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

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

Например:

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

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();
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top