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