При разработке классов для хранения вашей модели данных, которые я прочитал, может быть полезно создавать неизменяемые объекты, но в какой момент бремя списков параметров конструктора и глубоких копий становится слишком большим, и вы должны отказаться от неизменного ограничения?
Например, вот неизменяемый класс для представления именованной вещи (я использую синтаксис C #, но этот принцип применим ко всем языкам OO)
class NamedThing
{
private string _name;
public NamedThing(string name)
{
_name = name;
}
public NamedThing(NamedThing other)
{
this._name = other._name;
}
public string Name
{
get { return _name; }
}
}
Именованные вещи можно создавать, запрашивать и копировать в новые именованные вещи, но имя нельзя изменить.
Это все хорошо, но что происходит, когда я хочу добавить еще один атрибут? Я должен добавить параметр в конструктор и обновить конструктор копирования; Это не слишком большая работа, но проблемы начинаются, насколько я вижу, когда я хочу сделать сложный объект неизменным.
Если класс содержит атрибуты may и коллекции, содержащие другие сложные классы, мне кажется, что список параметров конструктора стал бы кошмаром.
Итак, в какой момент класс становится слишком сложным, чтобы быть неизменным?
Ответы:
Когда они становятся бременем? Очень быстро (особенно если выбранный вами язык не обеспечивает достаточной синтаксической поддержки неизменности.)
Неизменность продается как серебряная пуля для многоядерной дилеммы и все такое. Но неизменность в большинстве ОО-языков заставляет вас добавлять искусственные артефакты и практики в вашу модель и процесс. Для каждого сложного неизменяемого класса у вас должен быть одинаково сложный (по крайней мере, внутренний) конструктор. Независимо от того, как вы его спроектируете, он все равно создает сильную связь (поэтому у нас лучше есть веские основания для их внедрения).
Не обязательно возможно моделировать все в небольших не сложных классах. Поэтому для больших классов и структур мы искусственно разделяем их - не потому, что это имеет смысл в нашей предметной модели, а потому, что нам приходится иметь дело с их сложными экземплярами и создателями кода.
Еще хуже, когда люди слишком далеко зацикливаются на идее неизменности в языке общего назначения, таком как Java или C #, делая все неизменным. Затем, в результате, вы видите людей, форсирующих конструкции s-выражений в языках, которые не поддерживают такие вещи с легкостью.
Инжиниринг - это акт моделирования посредством компромиссов и компромиссов. Делать все неизменным с помощью эдикта, потому что кто-то прочитал, что все неизменно на функциональном языке X или Y (совершенно другая модель программирования), что неприемлемо. Это не хорошая техника.
Небольшие, возможно унитарные вещи можно сделать неизменными. Более сложные вещи можно сделать неизменными, когда это имеет смысл . Но неизменность - это не серебряная пуля. Возможность уменьшить количество ошибок, повысить масштабируемость и производительность - это не единственная функция неизменности. Это функция правильной инженерной практики . В конце концов, люди написали хорошее, масштабируемое программное обеспечение без неизменности.
Неизменность становится очень быстрым бременем (это добавляет случайной сложности), если это делается без причины, когда это делается вне того, что имеет смысл в контексте модели предметной области.
Я, например, стараюсь избегать этого (если я не работаю на языке программирования с хорошей синтаксической поддержкой).
источник
Я прошел этап настаивания на том, чтобы классы были неизменными, где это возможно. У меня были строители практически для всего, неизменяемых массивов и т. Д., И т. Д. Я нашел ответ на ваш вопрос простым: в какой момент неизменяемые классы становятся бременем? Очень быстро. Как только вы хотите сериализовать что-то, вы должны быть в состоянии десериализовать, что означает, что это должно быть изменяемым; как только вы захотите использовать ORM, большинство из них настаивают на том, чтобы свойства были изменяемыми. И так далее.
В конце концов я заменил эту политику неизменными интерфейсами к изменяемым объектам.
Теперь объект обладает гибкостью, но вы все равно можете сказать вызывающему коду, что он не должен редактировать эти свойства.
источник
IComparable<T>
гарантирует, что еслиX.CompareTo(Y)>0
иY.CompareTo(Z)>0
, тоX.CompareTo(Z)>0
. Интерфейсы имеют контракты . Если в контрактеIImmutableList<T>
указано, что значения всех предметов и свойств должны быть «установлены в камне», прежде чем какой-либо экземпляр подвергнется воздействию внешнего мира, то все законные реализации будут делать это. Ничто не мешаетIComparable
реализации нарушать транзитивность, но реализации, которые делают это, незаконны. ЕслиSortedDictionary
неисправности, когда дано незаконноIComparable
, ...IReadOnlyList<T>
будут неизменными, учитывая, что (1) такое требование не указано в документации интерфейса, и (2) самая распространенная реализацияList<T>
, даже не только для чтения ? Я не совсем понимаю, что двусмысленно в моих терминах: коллекция читаема, если данные внутри могут быть прочитаны. Он доступен только для чтения, если он может пообещать, что содержащиеся в нем данные не могут быть изменены, если какой-либо внешний источник не содержит код, который изменит его. Он неизменен, если может гарантировать, что его нельзя изменить, точка.Я не думаю, что есть общий ответ на это. Чем сложнее класс, тем сложнее рассуждать об изменениях его состояния и тем дороже создавать его новые копии. Так что выше некоторого (личного) уровня сложности станет слишком больно делать / сохранять класс неизменным.
Обратите внимание, что слишком сложный класс или длинный список параметров метода сами по себе являются запахами конструкции , независимо от неизменности.
Поэтому обычно предпочтительным решением было бы разбить такой класс на несколько отдельных классов, каждый из которых можно сделать изменяемым или неизменным самостоятельно. Если это невозможно, его можно отключить.
источник
Вы можете избежать проблемы копирования, если храните все свои неизменяемые поля во внутренней части
struct
. Это в основном вариация картины на память. Затем, когда вы хотите сделать копию, просто скопируйте сувенир:источник
У вас есть пара вещей на работе здесь. Неизменяемые наборы данных отлично подходят для многопоточной масштабируемости. По сути, вы можете немного оптимизировать свою память, чтобы один набор параметров был одним экземпляром класса - везде. Поскольку объекты никогда не меняются, вам не нужно беспокоиться о синхронизации доступа к его членам. Это хорошая вещь. Однако, как вы указали, чем сложнее объект, тем больше вам нужна изменчивость. Я бы начал с рассуждений по этим направлениям:
В языках, которые поддерживают только неизменяемые объекты (например, Erlang), если есть какая-либо операция, которая, по-видимому, изменяет состояние неизменяемого объекта, конечным результатом является новая копия объекта с обновленным значением. Например, когда вы добавляете элемент в вектор / список:
Это может быть нормальным способом работы с более сложными объектами. Например, когда вы добавляете узел дерева, в результате получается новое дерево с добавленным узлом. Метод в приведенном выше примере возвращает новый список. В примере в этом параграфе
tree.add(newNode)
будет возвращено новое дерево с добавленным узлом. Для пользователей становится легко работать. Для авторов библиотеки становится утомительно, когда язык не поддерживает неявное копирование. Этот порог зависит от вашего собственного терпения. Для пользователей вашей библиотеки самый вменяемый предел, который я нашел, составляет около трех-четырех параметров вершин.источник
Если у вас есть несколько конечных членов класса, и вы не хотите, чтобы они были доступны всем объектам, которые должны его создать, вы можете использовать шаблон builder:
Преимущество заключается в том, что вы можете легко создать новый объект, используя только другое значение с другим именем.
источник
newObject
.NamedThing
в данном случае)Builder
повторном использовании a существует реальный риск того, что произойдет, о чем я упоминал. Кто-то может создать множество объектов и решить, что, поскольку большинство свойств одинаковы, просто повторно использовать объектBuilder
, и фактически сделаем его глобальным синглтоном, в который вводится зависимость! Упс. Введены основные ошибки. Так что я думаю, что эта модель смешанного инстанцированного против статического - это плохо.На мой взгляд, не стоит беспокоиться о том, чтобы сделать небольшие классы неизменяемыми в языках, подобных тому, который вы показываете. Я использую маленький здесь и не сложный , потому что даже если вы добавите десять полей к этому классу, и он действительно над ними работает, я сомневаюсь, что это займет килобайты, не говоря уже о мегабайтах, не говоря уже о гигабайтах, так что любая функция, использующая экземпляры вашего Класс может просто сделать дешевую копию всего объекта, чтобы избежать изменения оригинала, если он хочет избежать внешних побочных эффектов.
Постоянные структуры данных
Где я нахожу личное использование для неизменности, так это для больших центральных структур данных, которые объединяют кучу маленьких данных, таких как экземпляры класса, который вы показываете, например, тот, который хранит миллион
NamedThings
. Принадлежность к постоянной структуре данных, которая является неизменной, и находящаяся за интерфейсом, который разрешает только доступ только для чтения, элементы, которые принадлежат контейнеру, становятся неизменяемыми, и элементу class (NamedThing
) не приходится иметь дело с ним.Дешевые копии
Постоянная структура данных позволяет преобразовывать ее области и делать их уникальными, избегая модификаций в оригинале без необходимости полностью копировать структуру данных. Это настоящая красота этого. Если вы хотите наивно писать функции, которые избегают побочных эффектов, которые вводят структуру данных, которая занимает гигабайты памяти и изменяет только объем памяти в мегабайтах, то вам нужно было бы скопировать всю чертову вещь, чтобы не касаться ввода, и вернуть новый выход. Это либо копирование гигабайт, чтобы избежать побочных эффектов, либо побочные эффекты в этом сценарии, поэтому вам придется выбирать между двумя неприятными вариантами.
Благодаря постоянной структуре данных это позволяет вам написать такую функцию и избежать копирования всей структуры данных, требуя только около мегабайта дополнительной памяти для вывода, если ваша функция только преобразовала объем памяти в мегабайтах.
обременять
Что касается бремени, то, по крайней мере, в моем случае, есть непосредственное. Мне нужны те конструкторы, о которых говорят люди, или «переходные процессы», как я их называю, чтобы они могли эффективно выражать преобразования в эту массивную структуру данных, не касаясь ее. Код как это:
... тогда должно быть написано так:
Но в обмен на эти две дополнительные строки кода функцию теперь можно безопасно вызывать через потоки с одним и тем же исходным списком, она не вызывает побочных эффектов и т. Д. Кроме того, это действительно позволяет легко сделать эту операцию отменяемым действием пользователя, поскольку отменить можно просто хранить дешевую мелкую копию старого списка.
Исключение-Безопасность или Восстановление после ошибок
Не все могут получить такую же выгоду, как я, от постоянных структур данных в подобных контекстах (я нашел их столь полезными в системах отмены и неразрушающем редактировании, которые являются центральными понятиями в моей области VFX), но одна вещь применима только к каждый должен учитывать исключительную безопасность или исправление ошибок .
Если вы хотите сделать исходную функцию мутации исключительной, то для нее требуется логика отката, для которой простейшая реализация требует копирования всего списка:
На этом этапе изменяемая на исключение изменяемая версия еще более затратна в вычислительном отношении и, возможно, даже сложнее написать правильно, чем неизменяемая версия, использующая «конструктор». И многие разработчики на C ++ просто пренебрегают безопасностью исключений и, возможно, это хорошо для их домена, но в моем случае я хотел бы убедиться, что мой код работает правильно даже в случае исключения (даже при написании тестов, которые намеренно генерируют исключения для проверки исключений). безопасность), и это делает меня таким, чтобы я был в состоянии откатить любые побочные эффекты, которые функция вызывает на полпути в функцию, если что-то выбрасывает.
Если вы хотите быть безопасными для исключений и корректно восстанавливаться после ошибок без сбоев и прожогов вашего приложения, тогда вам нужно отменить / отменить любые побочные эффекты, которые может вызвать функция в случае ошибки / исключения. И там конструктор может фактически сэкономить больше времени программиста, чем это стоит вместе с вычислительным временем, потому что: ...
Итак, вернемся к основному вопросу:
Они всегда обременительны для языков, которые вращаются вокруг изменчивости, а не неизменности, поэтому я думаю, что вы должны использовать их там, где выгоды значительно перевешивают затраты. Но на достаточно широком уровне для достаточно больших структур данных, я верю, что во многих случаях это достойный компромисс.
Кроме того, у меня есть только несколько неизменных типов данных, и все они представляют собой огромные структуры данных, предназначенные для хранения огромного количества элементов (пикселей изображения / текстуры, объектов и компонентов ECS, а также вершин / ребер / многоугольников). сетка).
источник