Как спроектировать неизменный объект со сложной инициализацией - PullRequest
13 голосов
/ 10 декабря 2008

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

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

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

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

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

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

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

Ответы [ 6 ]

22 голосов
/ 10 декабря 2008

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

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, и они могут иметь значения по умолчанию.

9 голосов
/ 10 декабря 2008

В данный момент вам придется использовать конструктор с большим количеством аргументов или конструктор. В 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" };

Не так много, чтобы отличить их, правда.

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

3 голосов
/ 10 декабря 2008

От макушки головы приходят два разных ответа ...

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

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

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.

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

1 голос
/ 26 июня 2014

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

Например:

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

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();
1 голос
/ 06 мая 2009

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

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

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

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

1 голос
/ 10 декабря 2008

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...