Можно ли выставить состояние неизменяемого объекта?
недавно столкнувшись с концепцией неизменяемых объектов, я хотел бы знать лучшие практики управления доступом к государству. Несмотря на то, что объектно-ориентированная часть моего мозга заставляет меня хотеть съежиться в страхе при виде общественных членов, Я не вижу никаких технических проблем с чем-то вроде этого:
public class Foo {
public final int x;
public final int y;
public Foo( int x, int y) {
this.x = x;
this.y = y;
}
}
Я бы чувствовал себя более комфортно, объявляя поля как private
и предоставляет методы чтения для каждого, но это кажется слишком сложным, когда государство явно только для чтения.
какова наилучшая практика предоставления доступа к состоянию неизменяемого объекта?
13 ответов:
Это полностью зависит от того, как вы собираетесь использовать объект. Публичные поля по своей сути не являются злом, просто плохо по умолчанию все быть публичным. Например, java.ОУ.Класс Point делает свои поля x и y общедоступными, и они даже не являются окончательными. Ваш пример кажется прекрасным использованием открытых полей, но опять же вы можете не захотеть раскрывать все внутренние поля другого неизменяемого объекта. Здесь нет правила "все включено".
Я думал то же самое в прошлом, но обычно заканчиваю тем, что делаю переменные частными и использую геттеры и сеттеры, чтобы позже у меня все еще была возможность вносить изменения в реализацию, сохраняя тот же интерфейс.
Это напомнило мне о том, что я недавно прочитал в "чистом коде" Роберта С. Мартина. В главе 6 он дает несколько иную точку зрения. Например, на странице 95 он утверждает
"объекты скрывают свои данные абстракции и открытые функции, которые работают с этими данными. Структура данных предоставляет свои данные и не имеет значимых функций."
и на странице 100:
квази-инкапсуляция бобов, похоже, заставляет некоторых пуристов OO чувствовать себя лучше, но обычно не дает никакой другой выгоды.
основываясь на примере кода, класс Foo, по-видимому, является структурой данных. Итак, основываясь на том, что я понял из обсуждения в чистом коде (что больше чем только две цитаты, которые я дал), цель класса-предоставить данные, а не функциональность, и наличие геттеров и сеттеров, вероятно, не приносит много пользы.
опять же, по моему опыту, я обычно шел вперед и использовал "бобовый" подход частных данных с геттерами и сеттерами. Но опять же, никто никогда не просил меня написать книгу о том, как написать лучший код, так что, возможно, у Мартина есть что сказать.
Если ваш объект имеет достаточно локальное использование, что вы не заботитесь о проблемах взлома изменений API для него в будущем, нет необходимости привязывать геттеры поверх переменных экземпляра. Но это общий предмет, не специфичный для неизменяемых объектов.
преимущество использования геттеров исходит от одного дополнительного уровня косвенности, который мая пригодится, если вы разрабатываете объект, который будет широко использоваться, и чья полезность будет распространяться на непростительное будущее.
независимо от неизменности, вы все еще выставляя реализация этого класса. На каком-то этапе вы захотите изменить реализацию (или, возможно, создать различные производные, например, используя пример точки, вам может понадобиться аналогичный класс точек с полярными координатами), и ваш клиентский код подвергается этому.
приведенный выше шаблон вполне может быть полезен, но я обычно ограничиваю его очень локализованными экземплярами (например, передача ОК of информация вокруг - я склонен находить, что объекты, казалось бы, несвязанной информации, однако, либо плохие инкапсуляции, либо что информация и связанных, и мой кортеж превращается в полноценный объект)
важно иметь в виду, что вызовы функций обеспечивают универсальный интерфейс. любой объект может взаимодействовать с другими объектами с помощью вызовов функций. Все, что вам нужно сделать, это определить правильные подписи, и вы идете. Единственная загвоздка заключается в том, что вы должны взаимодействовать исключительно через эти вызовы функций, которые часто работают хорошо, но в некоторых случаях могут быть неуклюжими.
основной причиной прямого предоставления переменных состояния было бы использование примитива операторы непосредственно на этих полях. когда сделано хорошо, это может повысить читабельность и удобство: например, добавление комплексных чисел с
+
, или доступ к коллекции Ключей с[]
. Преимущества этого могут быть удивительными, при условии, что ваше использование синтаксиса следует традиционным соглашениям.загвоздка в том, что операторы не универсальный интерфейс. только очень специфический набор встроенных типов может использовать их, эти можно только использовать в путях что язык ожидает, и вы не можете определить новые. Итак, как только вы определили свой публичный интерфейс с помощью примитивов, вы заблокировали себя в использовании этого примитива и только этого примитива (и других вещей, которые можно легко привести к нему). Чтобы использовать что-то еще, вы должны танцевать вокруг этого примитива каждый раз, когда вы взаимодействуете с ним, и это убивает вас с сухой точки зрения: вещи могут стать очень хрупкими очень быстро.
некоторые языки превращают операторов в универсальный интерфейс, но Java не. это не обвинение Java: его разработчики сознательно решили не включать перегрузку оператора, и у них были веские причины для этого. Даже когда вы работаете с объектами, которые, кажется, хорошо вписываются в традиционные значения операторов, заставляя их работать таким образом, что на самом деле имеет смысл, может быть удивительно тонким, и если вы не совсем прибиваете его, вы заплатите за это позже. Часто гораздо проще сделать функцию на основе интерфейс читаемый и удобный, чем пройти через этот процесс, и вы часто даже заканчиваете с лучшим результатом, чем если бы вы использовали операторы.
здесь были компромиссы, связанные с этим решением. здесь и раз, когда интерфейс на основе оператора действительно работает лучше, чем на основе функции, но без перегрузки оператора эта опция просто недоступна. Пытаясь втиснуть операторы в любом случае будут запереть тебя в дизайнерские решения, которые вы, вероятно, не очень хотите, чтобы быть установлены в камне. Разработчики Java считали, что этот компромисс стоит того, и они, возможно, даже были правы в этом. Но такие решения не приходят без каких-то последствий, и такая ситуация-это когда последствия поражают.
короче говоря, проблема не раскрывает вашу реализацию,per se. Проблема запереться в реализации.
на самом деле, он нарушает инкапсуляцию, чтобы каким-либо образом раскрыть любое свойство объекта-каждое свойство является деталью реализации. Просто потому, что все это делают, это не делает его правильным. Использование методов доступа и мутаторов (геттеров и сеттеров) не делает его лучше. Скорее, шаблоны CQRS должны использоваться для поддержания инкапсуляции.
Я знаю только одну опору, чтобы иметь геттеры для окончательного свойства. Это тот случай, когда вы хотите иметь доступ к свойствам через интерфейс.
public interface Point { int getX(); int getY(); } public class Foo implements Point {...} public class Foo2 implements Point {...}
в противном случае открытые конечные поля в порядке.
класс, который вы разработали, должен быть прекрасен в своем текущем воплощении. Вопросы обычно приходят в игру, когда кто-то пытается изменить этот класс или наследовать от него.
например, после просмотра кода выше, кто-то думает о добавлении другого экземпляра переменной-члена класса Bar.
public class Foo { public final int x; public final int y; public final Bar z; public Foo( int x, int y, Bar z) { this.x = x; this.y = y; } } public class Bar { public int age; //Oops this is not final, may be a mistake but still public Bar(int age) { this.age = age; } }
в приведенном выше коде экземпляр Bar не может быть изменен, но внешне любой может обновить значение Bar.возраст.
лучше всего отметить все поля, как частные, имеют геттеры для полей. Если вы возвращаете объект или коллекцию, обязательно верните неизменяемую версию.
иммунитет имеет важное значение для параллельного программирования.
объект с общедоступными конечными полями, которые загружаются из общедоступных параметров конструктора, эффективно изображает себя как простой держатель данных. Хотя такие держатели данных не являются особенно "ООП-иш", они полезны для того, чтобы позволить одному полю, переменной, параметру или возвращаемому значению инкапсулировать несколько значений. Если цель типа состоит в том, чтобы служить простым средством склеивания нескольких значений вместе, такой держатель данных часто является лучшим представлением в рамках без реального значимые типы.
рассмотрим вопрос о том, что бы вы хотели, чтобы произошло, если какой-то метод
Foo
хочет дать абоненту aPoint3d
который инкапсулирует "X=5, Y=23, Z=57", и у него есть ссылка на APoint3d
где X=5, Y=23 и Z=57. Если известно, что вещь Foo является простым неизменяемым держателем данных, то Foo должен просто дать вызывающему ссылку на него. Если, однако, это может быть что-то другое (например он может содержать дополнительную информацию помимо X, Y, и Z), затем Foo должен создать новый простой держатель данных, содержащий "X=5, Y=23, Z=57" и дать вызывающему ссылку на это.С
Point3d
быть запечатанным и выставлять его содержимое в качестве общедоступных конечных полей будет означать, что такие методы, как Foo, могут предполагать, что это простой неизменяемый держатель данных и может безопасно обмениваться ссылками на его экземпляры. Если существует код, который делает такие предположения, это может быть трудно или невозможно изменитьPoint3d
быть чем-то иным, кроме простого неизменяемого держатель данных без нарушения такого кода. С другой стороны, код, который предполагаетPoint3d
простой неизменяемый держатель данных может быть намного проще и эффективнее, чем код, который должен иметь дело с возможностью того, что это что-то другое.
вы видите этот стиль много в Scala, но есть решающее различие между этими языками: Scala следует Единый Принцип Доступа
- вам нужно извлечь интерфейс или суперкласс (например, ваш класс представляет собой комплексные числа, и вы хотите иметь класс-брат с полярным координатное представление тоже)
- вам нужно наследовать от вашего класса, и информация становится избыточной (например,
x
можно рассчитать по дополнительным данным подкласса)- вам нужно проверить ограничения (например
x
должно быть неотрицательным по какой-то причине)Также обратите внимание, что вы не можете использовать этот стиль для изменяемых (как печально известный
java.util.Date
). Только с геттерами у вас есть шанс сделать защитную копию или изменить представление (например, хранениеDate
информация какlong
)
Я использую много конструкций, очень похожих на тот, который вы ставите в вопрос, иногда есть вещи, которые могут быть лучше смоделированы с помощью (иногда inmutable) data-strcuture, чем с классом.
все зависит, если вы моделируете объект, объект его определяется его поведением, в этом случае никогда не выставляйте внутренние свойства. В других случаях вы моделируете структуру данных, а java не имеет специальной конструкции для структур данных, ее хорошо использовать класс и публиковать все свойства, и если вы хотите, чтобы неизменность окончательная и публичная, конечно.
например, у Роберта Мартина есть одна глава об этом в великой книге Clean Code, которую нужно прочитать, на мой взгляд.
в случаях, когда цель чтобы связать два значения друг с другом под значимым именем, вы можете даже рассмотреть возможность пропустить определение любых конструкторов и сохранить элементы изменяемыми:
public class Sculpture { public int weight = 0; public int price = 0; }
это имеет преимущество, чтобы свести к минимуму риск перепутать порядок параметров при создании экземпляра класса. Ограниченная изменчивость, если это необходимо, может быть достигнута путем взятия всего контейнера под
private
управление.
просто хочу задуматься отражение:
Foo foo = new Foo(0, 1); // x=0, y=1 Field fieldX = Foo.class.getField("x"); fieldX.setAccessible(true); fieldX.set(foo, 5); System.out.println(foo.x); // 5!
Итак,
Foo
по-прежнему незыблемы? :)