Как решить, должен ли тип объекта данных быть неизменным?

23

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

Однако мои сверстники в целом уклоняются от неизменности. Они не совсем неопытны (отнюдь не так), но все же пишут объекты данных классическим способом - частные члены с геттером и сеттером для каждого члена. Тогда обычно их конструкторы не принимают аргументов или, возможно, просто принимают некоторые аргументы для удобства. Так часто создание объекта выглядит так:

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

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

Как вы решаете, должен ли тип объекта быть неизменным? Есть хороший набор критериев, чтобы судить об этом?

В настоящее время я спорю о том, стоит ли переключать несколько типов данных в моем собственном проекте на неизменяемые, но мне пришлось бы обосновать это для своих коллег, и данные в типах могут (ОЧЕНЬ редко) меняться - в это время вы, конечно, можете изменить это неизменный способ (создайте новый, скопировав свойства из старого объекта, за исключением тех, которые вы хотите изменить). Но я не уверен, является ли это просто моей любовью к неизменным, или есть ли в них реальная потребность / польза от них.

Ricket
источник
1
Программа на Эрланге и вся проблема решена, все неизменно
Захарий К
1
@ZacharyK Я действительно хотел упомянуть кое-что о функциональном программировании, но воздержался из-за моего ограниченного опыта работы с функциональными языками программирования.
Ricket

Ответы:

27

Похоже, вы приближаетесь к нему задом наперед. По умолчанию следует неизменным. Делайте объект изменчивым только в том случае, если вы абсолютно не можете / не можете заставить его работать как неизменный объект.

Брайан Кноблаух
источник
11

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

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

Роберт Харви
источник
15
Преимущество неизменных объектов заключается в том, что их легче рассуждать. В многопоточной среде это очень просто; в простых однопоточных алгоритмах это все еще проще. Например, вам никогда не нужно заботиться о том, является ли состояние непротиворечивым, прошел ли объект все мутации, необходимые для использования в определенном контексте, и т. Д. Лучшие изменяемые объекты позволяют вам также мало заботиться об их точном состоянии: например, кеширует.
9000
-1, согласен с @ 9000. Безопасность потока является вторичной (рассмотрим объекты, которые появляются в неизменяемом, но имеют внутреннее изменяемое состояние, например, из-за запоминания). Кроме того, повышение изменчивой производительности является преждевременной оптимизацией, и можно защищать что угодно, если вам требуется, чтобы пользователь «знал, что он делает». Если бы я знал, что я делал все время, я бы никогда не написал программу с ошибкой.
Доваль
4
@Doval: Тут нечего не соглашаться. 9000 абсолютно правильно; неизменные объекты являются легче рассуждать о. Отчасти это делает их такими полезными для параллельного программирования. Другая часть заключается в том, что вам не нужно беспокоиться об изменении состояния. Преждевременная оптимизация здесь неактуальна; использование неизменяемых объектов связано с дизайном, а не с производительностью, а неправильный выбор структур данных заранее является преждевременной пессимизацией.
Роберт Харви
@RobertHarvey Я не уверен, что вы подразумеваете под «плохим выбором структуры данных заранее». Существуют неизменяемые версии большинства изменяемых структур данных, обеспечивающие аналогичную производительность. Если вам нужен список, и у вас есть выбор использования неизменяемого списка и изменяемого списка, вы будете использовать неизменяемый список, пока не будете уверены, что это узкое место в вашем приложении, и изменяемая версия будет работать лучше.
Довал
2
@Doval: я читал Окасаки. Эти структуры данных обычно не используются, если вы не используете язык, который полностью поддерживает функциональную парадигму, например, Haskell или Lisp. И я оспариваю идею, что неизменяемые структуры являются выбором по умолчанию; Подавляющее большинство бизнес-вычислительных систем по-прежнему строятся вокруг изменчивых структур (то есть реляционных баз данных). Хорошая идея - начать с неизменяемых структур данных, но это все еще очень хорошая башня из слоновой кости.
Роберт Харви
6

Есть два основных способа решить, является ли объект неизменным.

а) в зависимости от характера объекта

Легко уловить эти ситуации, потому что мы знаем, что эти объекты не изменятся после его создания. Например, если у вас есть RequestHistoryсущность, и по своей природе сущности истории не изменяются после ее создания. Эти объекты могут быть спроектированы как неизменяемые классы. Имейте в виду, что объект запроса является взаимозаменяемым, так как он может изменять свое состояние, кому он назначен и т. Д. С течением времени, но история запросов не изменяется. Например, был элемент истории, созданный на прошлой неделе, когда он перешел из отправленного в назначенное состояние, и это право истории никогда не может измениться. Так что это классический неизменный случай.

б) Исходя из выбора дизайна, внешние факторы

Это похоже на пример java.lang.String. Строки на самом деле могут меняться с течением времени, но по своей конструкции они сделали его неизменным из-за факторов кэширования / пула строк / факторов параллелизма. Аналогично, кэширование / параллелизм и т. Д. Могут сыграть хорошую роль в том, чтобы сделать объект неизменным, если в приложении жизненно важны кэширование / параллелизм и связанная с ним производительность. Но это решение должно быть принято очень осторожно после анализа всех воздействий.

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

java_mouse
источник
4

В настоящее время я спорю о том, стоит ли переключать несколько типов данных в моем собственном проекте на неизменяемые, но мне пришлось бы обосновать это для своих коллег, и данные в типах могут (ОЧЕНЬ редко) меняться - в это время вы, конечно, можете изменить это неизменный способ (создайте новый, скопировав свойства из старого объекта, за исключением тех, которые вы хотите изменить).

Минимизация состояния программы очень выгодна.

Спросите их, хотят ли они использовать изменяемый тип значения в одном из ваших классов для временного хранения из клиентского класса.

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

Брайан
источник
4

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

Простого ответа не существует, но наверняка вы хотите сделать объекты с малыми значениями неизменяемыми. Java правильно сделала Strings неизменяемыми, но неправильно сделала Date и Calendar изменяемыми.

Поэтому определенно сделайте объекты небольших значений неизменяемыми и реализуйте конструктор копирования. Забудьте все о Cloneable, он настолько плохо спроектирован, что бесполезен.

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

Кевин Клайн
источник
Похоже, как выбирать между стеком и кучей при написании C или C ++ :)
Ricket
@Ricket: не так уж много ИМО. Стек / куча зависит от времени жизни объекта. В C ++ очень часто встречаются изменяемые объекты в стеке.
Кевин Клайн
1

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

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

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

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

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

Карл Билефельдт
источник
Я нахожу любопытным, что в дискуссиях об изменчивости не распознается связь между неизменяемостью и способами, которыми объект может быть безопасно выставлен или использован совместно. Ссылка на глубоко неизменяемый объект класса может безопасно передаваться недоверенному коду. Ссылка на экземпляр изменяемого класса может быть предоставлена ​​в общий доступ, если каждому, кто владеет ссылкой, можно доверять никогда не изменять объект или не подвергать его коду, который может это сделать. Ссылка на экземпляр класса, который может быть видоизменен, как правило, вообще не должна использоваться совместно. Если где-то во вселенной существует только одна ссылка на объект ...
суперкат
... и ничто не запрашивало его "хэш-код идентичности", использовало его для блокировки или иным образом осуществляло доступ к его "скрытым Objectфункциям", тогда непосредственное изменение объекта не было бы семантически ничем не отличается от перезаписи ссылки ссылкой на новый объект, который был идентичны, но для указанного изменения. IMHO, одна из самых больших семантических слабостей в Java заключается в том, что у нее нет средств, с помощью которых код может указывать, что переменная должна быть единственной неэфемерной ссылкой на что-то; ссылки должны передаваться только методам, которые не могут сохранить копию после их возврата.
суперкат
0

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

Например, это может быть очень дорого:

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

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

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

В моем случае слишком дорого (по крайней мере, с точки зрения производительности) сделать все неизменным, поэтому я оставляю его для классов, которые слишком дороги для полного копирования. Эти классы, как правило, представляют собой здоровенные структуры данных, такие как сетки и изображения, и я обычно использую изменяемый интерфейс для выражения изменений в них через объект «строитель», чтобы получить новую неизменяемую копию. И я делаю это не столько для того, чтобы попытаться достичь неизменных гарантий на центральном уровне класса, сколько для того, чтобы помочь мне использовать класс в функциях, которые могут быть свободны от побочных эффектов. Мое желание сделать Meshнеизменяемым выше не состоит в том, чтобы сделать сетки неизменяемыми напрямую, а позволить легко писать функции, свободные от побочных эффектов, которые вводят меш и выводят новый, не платя при этом огромной памяти и вычислительных затрат.

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

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

... тогда у меня нет соблазна сделать векторы неизменяемыми, поскольку они достаточно дешевы, чтобы просто копировать полностью. Здесь нет никакого выигрыша в производительности, превращая Векторы в неизменные структуры, которые могут копировать неизмененные части. Такие затраты перевесят стоимость простого копирования всего вектора.


источник
-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Если Foo имеет только два свойства, тогда легко написать конструктор, который принимает два аргумента. Но предположим, что вы добавили еще одно свойство. Затем вы должны добавить еще один конструктор:

public Foo(String a, String b, SomethingElse c)

На данный момент, это все еще управляемо. Но что, если есть 10 свойств? Вам не нужен конструктор с 10 аргументами. Вы можете использовать шаблон построителя для создания экземпляров, но это добавляет сложности. К этому времени вы будете думать, «почему я просто не добавил сеттеры для всех свойств, как это делают обычные люди»?

Майк Баранчак
источник
4
Является ли конструктор с десятью аргументами хуже, чем исключение NullPointerException, которое возникает, когда свойство7 не задано? Является ли шаблон компоновщика более сложным, чем код для проверки неинициализированных свойств в каждом методе? Лучше один раз проверить, когда объект построен.
Кевин Клайн
2
Как было сказано десятилетия назад именно для этого случая: «Если у вас есть процедура с десятью параметрами, вы, вероятно, пропустили некоторые».
9000
1
Почему вы не хотите конструктор с 10 аргументами? В какой момент вы рисуете линию? Я думаю, что конструктор с 10 аргументами - это небольшая цена за преимущества неизменности. Плюс, либо у вас есть одна строка с 10 вещами, разделенными запятыми (опционально разбито на несколько строк или даже 10 строк, если это выглядит лучше), или у вас есть 10 строк в любом случае, когда вы настраиваете каждое свойство индивидуально ...
Ricket
1
@Ricket: Потому что это увеличивает риск размещения аргументов в неправильном порядке . Если вы используете сеттеры или шаблон компоновщика, это маловероятно.
Майк Баранчак
4
Если у вас есть конструктор с 10 аргументами, возможно, пришло время подумать о том, чтобы инкапсулировать эти аргументы в собственный класс (или классы) и / или критически взглянуть на дизайн вашего класса.
Адам Лир