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

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

Вопрос

В ходе нескольких проектов я разработал шаблон для создания неизменяемых объектов (только для чтения) и неизменяемых графов объектов.Неизменяемые объекты обладают преимуществом 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().Как только он станет неизменяемым, вызов установщика приведет к исключению ImmutableElementException.

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

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

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

Решение

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

У Эрика Липперта есть ряд записей в блоге об неизменяемости, начиная с здесь . Я до сих пор разбираюсь с CTP (C # 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 ().

(извиняюсь, если я изменил терминологию / имена методов - ТАК не сохраняет исходное сообщение видимым при составлении ответов)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Таким образом, вместо замораживания самого объекта, другой возможностью является копирование семантики ReadOnlyCollection < T & Gt;

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

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

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

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

Было бы неплохо иметь const-правильность, которую C ++ дает вам во время компиляции, но у нее возникают свои проблемы, и я рад, что C # не идет туда.

<Ч>

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

  

Не реализовывать ICloneable

     

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

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

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

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

Шаблон будет использовать это для ввода:

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

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

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

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

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

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

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

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

Просто подсказка для упрощения свойств элемента: используйте автоматические свойства с 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;
  }
}

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

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

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

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

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

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

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

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

    Мне не нравится идея о том, что я могу изменить объект с изменяемого состояния на неизменяемое, мне кажется, что этот вид поражения замышляет меня. Когда вам нужно это сделать? Только объекты, которые представляют ЗНАЧЕНИЯ, должны быть неизменяемыми

    Вы можете использовать необязательные именованные аргументы вместе со значениями 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);
    
    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top