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

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

В ходе нескольких проектов я разработал шаблон для создания неизменяемых (readonly) объектов и неизменяемых графов объектов. Неизменяемые объекты несут на себе 100-процентную резьбу и, следовательно, могут быть повторно использованы по нити. В своей работе я очень часто использую этот шаблон в веб-приложениях для настроек конфигурации и других объектов, которые я загружаю и кеширую в памяти. Кэшированные объекты всегда должны быть неизменными, так как вы хотите гарантировать, что они не будут неожиданно изменены.

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

public class SampleElement
{
  private Guid id;
  private string name;

  public SampleElement(Guid id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public Guid Id
  {
    get { return id; }
  }

  public string Name
  {
    get { return name; }
  }
}

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

Итак, как вы создаете неизменяемые объекты с сеттерами?

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

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

public interface IElement : ICloneable
{
  bool IsReadOnly { get; }
  void MakeReadOnly();
}

Класс Element является реализацией интерфейса IElement по умолчанию:

public abstract class Element : IElement
{
  private bool immutable;

  public bool IsReadOnly
  {
    get { return immutable; }
  }

  public virtual void MakeReadOnly()
  {
    immutable = true;
  }

  protected virtual void FailIfImmutable()
  {
    if (immutable) throw new ImmutableElementException(this);
  }

  ...
}

Позвольте реорганизовать класс SampleElement выше для реализации шаблона неизменяемого объекта:

public class SampleElement : Element
{
  private Guid id;
  private string name;

  public SampleElement() {}

  public Guid Id
  {
    get 
    { 
      return id; 
    }
    set
    {
      FailIfImmutable();
      id = value;
    }
  }

  public string Name
  {
    get 
    { 
      return name; 
    }
    set
    {
      FailIfImmutable();
      name = value;
    }
  }
}

Теперь вы можете изменить свойство Id и свойство Name, пока объект не был помечен как неизменный, вызвав метод MakeReadOnly(). Как только он станет неизменным, вызов setter приведет к исключению ImmutableElementException.

Заключительное примечание: Полный шаблон более сложный, чем приведенные здесь фрагменты кода. Он также содержит поддержку наборов неизменных объектов и полных графов объектов неизменяемых графов объектов. Полный шаблон позволяет превратить весь объектный граф неизменным, вызывая метод MakeReadOnly() на самом удаленном объекте. Когда вы начинаете создавать более крупные объектные модели, используя этот шаблон, увеличивается риск протекания объектов. Протечка объекта - это объект, который не может вызвать метод FailIfImmutable() перед внесением изменений в объект. Для проверки утечек я также разработал общий класс детекторов утечек для использования в модульных тестах. Он использует отражение для проверки, если все свойства и методы вызывают исключение ImmutableElementException в неизменяемом состоянии. Другими словами, TDD используется здесь.

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

4b9b3361

Ответ 1

Для информации второй подход называется "неизменяемость едока".

У Эрика Липперта есть серия записей в блогах о неизменности, начиная с здесь. Я все еще сталкиваюсь с CTP (С# 4.0), но выглядит интересно, какие необязательные/именованные параметры (в .ctor) могут делать здесь (при сопоставлении с полями только для чтения)... [обновление: я написал здесь здесь]

Для информации, я, вероятно, не буду делать эти методы virtual - мы, вероятно, не хотим, чтобы подклассы могли сделать это незамерзающим. Если вы хотите, чтобы они могли добавлять дополнительный код, я бы предложил что-то вроде:

[public|protected] void Freeze()
{
    if(!frozen)
    {
        frozen = true;
        OnFrozen();
    }
}
protected virtual void OnFrozen() {} // subclass can add code here.

Также - AOP (например, PostSharp) может быть жизнеспособным вариантом для добавления всех этих проверок ThrowIfFrozen().

(извинения, если я изменил имена терминов/методов - SO не сохраняет исходное сообщение видимым при составлении ответов)

Ответ 2

Другой вариант - создать класс Builder.

Например, в Java (и С# и многих других языках) String является неизменной. Если вы хотите сделать несколько операций для создания String, вы используете StringBuilder. Это можно изменить, а затем, как только вы закончите, вы вернете ему последний объект String. С этого момента он неизменен.

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

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

Ответ 3

После моего первоначального дискомфорта в связи с тем, что я должен был создать новый System.Drawing.Point для каждой модификации, я полностью обнял концепцию несколько лет назад. Фактически, я теперь создаю каждое поле как readonly по умолчанию и только изменяю его, чтобы быть изменчивым, если есть веская причина, - что удивительно редко.

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

Ответ 4

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

Более функциональным способом может быть возврат нового экземпляра объекта с каждым установщиком. Или создайте изменяемый объект и передайте его конструктору.

Ответ 5

(относительно) новая парадигма разработки программного обеспечения под названием Domain Driven design, делает различие между объектами сущности и объектами ценности.

Объект Объекты определяются как все, что должно сопоставляться с объектом, управляемым ключами, в постоянном хранилище данных, например, с сотрудником или клиентом, или с фактурой и т.д.... при изменении свойств объекта подразумевается, что вам необходимо сохранить изменения в хранилище данных где-то, а существование нескольких экземпляров класса с одним и тем же "ключом" подразумевает необходимость их синхронизации или координировать их персистентность с хранилищем данных, чтобы изменения одного экземпляра не перезаписать остальные. Изменение свойств объекта сущности подразумевает, что вы что-то меняете об объекте - не изменяя объект WHICH, на который вы ссылаетесь...

Объекты значения otoh - это объекты, которые можно считать неизменяемыми, чья утилита определяется строго их значениями свойств и для которых несколько экземпляров не нужно координировать каким-либо образом... например, адреса или номера телефонов, или колеса на автомобиле, или буквы в документе... эти вещи полностью определяются их свойствами... верхний регистр "A" в текстовом редакторе можно трансформировать прозрачно с любым другим прописным словом "A" в документе вам не нужен ключ, чтобы отличить его от всего другого ". В этом смысле он неизменен, потому что если вы измените его на" B "(точно так же, как изменение строки номера телефона в объекте номера телефона, вы не меняете данные, связанные с каким-либо изменяемым объектом, вы переключаетесь с одного значения на другое... так же, как при изменении значения строки...

Ответ 6

System.String - хороший пример неизменяемого класса с установщиками и мутировавшими методами, только каждый метод мутации возвращает новый экземпляр.

Ответ 7

Расширение в точке @Cory Foy и @Charles Bretana, где есть разница между сущностями и значениями. В то время как значения-объекты всегда должны быть неизменными, я действительно не думаю, что объект должен быть способен замерзнуть или позволить себе произвольно заморозить в кодовой базе. У этого есть очень плохой запах, и я волнуюсь, что это может быть трудно отследить, где именно объект был заморожен, и почему он был заморожен, и тот факт, что между вызовами объекта он мог изменить состояние от талой до замороженного,

Это не означает, что иногда вы хотите что-то передать (изменяемый), чтобы гарантировать, что он не будет изменен.

Итак, вместо замораживания самого объекта другая возможность заключается в копировании семантики ReadOnlyCollection <T>

List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();

Ваш объект может принимать роль как изменчивый, когда он ему нужен, а затем быть неизменным, когда вы этого хотите.

Обратите внимание, что ReadOnlyCollection <T> также реализует ICollection <T> , который имеет интерфейс Add( T item) в интерфейсе. Однако существует также bool IsReadOnly { get; }, определенный в интерфейсе, чтобы потребители могли проверить перед вызовом метода, который генерирует исключение.

Разница в том, что вы не можете просто установить IsReadOnly в значение false. Коллекция либо является, либо не является только для чтения, и никогда не изменяется для времени жизни коллекции.

Было бы хорошо, если бы у вас была const-correctness, которую С++ дает вам во время компиляции, но у этого начинает свой набор проблем, и я рад, что С# туда не идет.


ICloneable. Я думал, что просто вернусь к следующему:

Не реализуйте ICloneable

Не используйте ICloneable в общедоступных API

Брэд Абрамс - Руководство по дизайну, управляемый код и .NET Framework

Ответ 8

Это важная проблема, и я очень рад видеть, что для ее решения требуется более непосредственная поддержка фреймворка/языка. Решение, которое у вас есть, требует большого количества шаблонов. Может быть легко автоматизировать некоторые из шаблонов, используя генерацию кода.

Вы должны сгенерировать неполный класс, содержащий все зависящие от свойства свойства. Было бы довольно просто сделать для этого многоразовый шаблон T4.

Шаблон примет это для ввода:

  • Пространство имен
  • имя класса
  • список свойств/типов кортежей

И выведет файл С#, содержащий:

  • Объявление пространства имен
  • частичный класс
  • каждое из свойств, с соответствующими типами, поле поддержки, геттер и сеттер, который вызывает метод FailIfFrozen

Теги AOP на свойствах freezable также могут работать, но для этого потребуется больше зависимостей, тогда как T4 встроен в более новые версии Visual Studio.

Другим сценарием, который очень похож на этот, является интерфейс INotifyPropertyChanged. Решения для этой проблемы, вероятно, будут применимы к этой проблеме.

Ответ 9

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

Вот почему я бы расширил этот шаблон кодирования с помощью ограничения времени компиляции в виде общего класса, например:

public class Immutable<T> where T : IElement
{
    private T value;

    public Immutable(T mutable) 
    {
        this.value = (T) mutable.Clone();
        this.value.MakeReadOnly();
    }

    public T Value 
    {
        get 
        {
            return this.value;
        }
    }

    public static implicit operator Immutable<T>(T mutable) 
    {
        return new Immutable<T>(mutable);
    }

    public static implicit operator T(Immutable<T> immutable)
    {
        return immutable.value;
    }
}

Вот пример того, как вы будете использовать это:

// All elements of this list are guaranteed to be immutable
List<Immutable<SampleElement>> elements = 
    new List<Immutable<SampleElement>>();

for (int i = 1; i < 10; i++) 
{
    SampleElement newElement = new SampleElement();
    newElement.Id = Guid.NewGuid();
    newElement.Name = "Sample" + i.ToString();

    // The compiler will automatically convert to Immutable<SampleElement> for you
    // because of the implicit conversion operator
    elements.Add(newElement);
}

foreach (SampleElement element in elements)
    Console.Out.WriteLine(element.Name);

elements[3].Value.Id = Guid.NewGuid();      // This will throw an ImmutableElementException

Ответ 10

Мне не нравится идея изменить объект с изменчивого на неизменное состояние, похоже, что оно меня поражает. Когда вам это нужно? Только объекты, которые представляют VALUES, должны быть неизменными

Ответ 11

Просто подскажите, как упростить свойства элемента. Используйте автоматические свойства с помощью private set и избегайте явного объявления поля данных. например.

public class SampleElement {
  public SampleElement(Guid id, string name) {
    Id = id;
    Name = name;
  }

  public Guid Id {
    get; private set;
  }

  public string Name {
    get; private set;
  }
}

Ответ 12

Вот новое видео на канале 9, где Андерс Хейлсберг с 36:30 в интервью начинает говорить о неизменности в С#. Он дает очень хороший прецедент для неизменности эскимосов и объясняет, как это то, что вам в настоящее время требуется для реализации. Это была музыка для моих ушей, когда он сказал, что стоит подумать о лучшей поддержке создания неизменяемых графов объектов в будущих версиях С#

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

Ответ 13

Два других варианта вашей конкретной проблемы, которые не обсуждались:

  • Создайте собственный десериализатор, который может вызвать средство настройки частной собственности. Хотя усилия по созданию десериализатора в начале будут намного больше, это делает вещи более чистыми. Компилятор не даст вам даже попытаться вызвать сеттеры, и код в ваших классах будет легче читать.

  • Поместите конструктор в каждый класс, который принимает XElement (или некоторый другой вкус объектной модели XML) и заполняет его. Очевидно, что по мере увеличения числа классов это быстро становится менее желательным в качестве решения.

Ответ 14

Как насчет наличия абстрактного класса ThingBase, с подклассами MutableThing и ImmutableThing? ThingBase будет содержать все данные в защищенной структуре, предоставляя общедоступные свойства только для чтения для полей и защищенное свойство только для чтения для своей структуры. Это также обеспечило бы преодолимый метод AsImmutable, который вернул бы ImmutableThing.

MutableThing будет скрывать свойства со свойствами чтения/записи и предоставлять как конструктор по умолчанию, так и конструктор, который принимает ThingBase.

Неизменяемая вещь - это запечатанный класс, который переопределяет AsImmutable, чтобы просто вернуть себя. Он также предоставит конструктор, который принимает ThingBase.

Ответ 15

Вы можете использовать необязательные именованные аргументы вместе с nullables, чтобы сделать неизменяемый сеттер с очень маленьким шаблоном. Если вы действительно хотите установить свойство null, то у вас могут возникнуть проблемы.

class Foo{ 
    ...
    public Foo 
        Set
        ( double? majorBar=null
        , double? minorBar=null
        , int?        cats=null
        , double?     dogs=null)
    {
        return new Foo
            ( majorBar ?? MajorBar
            , minorBar ?? MinorBar
            , cats     ?? Cats
            , dogs     ?? Dogs);
    }

    public Foo
        ( double R
        , double r
        , int l
        , double e
        ) 
    {
        ....
    }
}

Вы бы использовали его так

var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);