Итак, мы все осознаем преимущества неизменяемых типов, особенно в многопоточных сценариях.(Или, по крайней мере, мы все должны это понимать; см., Например, System.String.)
Однако то, что я не видел, является большим обсуждением для создания неизменяемых экземпляров, в частности, руководящих принципов проектирования.
Например, предположим, что мы хотим иметь следующий неизменяемый класс:
class ParagraphStyle {
public TextAlignment Alignment {get;}
public float FirstLineHeadIndent {get;}
// ...
}
Самый распространенный подход, который я видел, состоит в том, чтобы иметь изменяемые / неизменяемые "пары" типов, например изменяемый Список и неизменяемый ReadOnlyCollection типов или изменяемый StringBuilder и неизменяемый String типов.
Чтобы подражать этому существующему шаблону, потребовалось бы ввести некоторый тип «изменяемого» типа ParagraphStyle
, который «дублирует» членов (чтобы обеспечить установщики), а затем предоставить конструктор ParagraphStyle
, который принимает изменяемый тип в качестве аргумента
// Option 1:
class ParagraphStyleCreator {
public TextAlignment {get; set;}
public float FirstLineIndent {get; set;}
// ...
}
class ParagraphStyle {
// ... as before...
public ParagraphStyle (ParagraphStyleCreator value) {...}
}
// Usage:
var paragraphStyle = new ParagraphStyle (new ParagraphStyleCreator {
TextAlignment = ...,
FirstLineIndent = ...,
});
Итак, это работает, поддерживает автозавершение кода в IDE и делает достаточно очевидными вещи о том, как конструировать вещи ... но это кажется довольно дублирующим.
Есть ли лучший способ?
Например, анонимные типы C # являются неизменяемыми, И позволяют использовать «нормальные» установщики свойств для инициализации:
var anonymousTypeInstance = new {
Foo = "string",
Bar = 42;
};
anonymousTypeInstance.Foo = "another-value"; // compiler error
К сожалению, самый близкий способ дублировать этисемантика в C # заключается в использовании параметров конструктора:
// Option 2:
class ParagraphStyle {
public ParagraphStyle (TextAlignment alignment, float firstLineHeadIndent,
/* ... */ ) {...}
}
Но это не "хорошо масштабируется";если ваш тип имеет, например, 15 свойств, конструктор с 15 параметрами совсем не дружелюбен, и обеспечение «полезных» перегрузок для всех 15 свойств - это рецепт для кошмара.Я полностью отвергаю это.
Если мы попытаемся имитировать анонимные типы, кажется, что мы могли бы использовать свойства "set-Once" в типе "immutable" и таким образом отбросить вариант "mutable":
// Option 3:
class ParagraphStyle {
bool alignmentSet;
TextAlignment alignment;
public TextAlignment Alignment {
get {return alignment;}
set {
if (alignmentSet) throw new InvalidOperationException ();
alignment = value;
alignmentSet = true;
}
}
// ...
}
Проблема в том, что не очевидно, что свойства могут быть установлены только один раз (компилятор определенно не будет жаловаться), а инициализация не является поточно-ориентированной.Таким образом, становится заманчивым добавить метод Commit()
, чтобы объект мог знать, что разработчик завершил установку свойств (таким образом, вызывая все свойства, которые ранее не были настроены для выброса, если вызывается их установщик), но, похоже, этобыть способ сделать вещи хуже, а не лучше.
Есть ли лучший дизайн, чем разделение изменяемого / неизменного класса?Или я обречен бороться с дублированием членов?