Можно ли разоблачить состояние неизменяемого объекта?

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

public class Foo {
    public final int x;
    public final int y;

    public Foo( int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Я бы чувствовал себя более комфортно, объявляя поля как private и предоставляя методы getter для каждого, но это кажется слишком сложным, когда состояние явно открыто.

Какова наилучшая практика для обеспечения доступа к состоянию неизменяемого объекта?

Ответ 1

Все зависит от того, как вы собираетесь использовать объект. Публичные поля не являются по своей сути злыми, это просто плохо, чтобы все было публично. Например, класс java.awt.Point делает свои поля x и y общедоступными, и они даже не являются окончательными. Ваш пример кажется прекрасным использованием публичных полей, но опять же вам может не потребоваться раскрывать все внутренние поля другого неизменяемого объекта. Нет правила catch-all.

Ответ 2

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

Это напомнило мне то, что я недавно прочитал в "Чистом коде" Роберта К. Мартина. В главе 6 он дает несколько другую перспективу. Например, на стр. 95 он утверждает

"Объекты скрывают свои данные за абстракциями и раскрывают функции, которые работают с этими данными. Структура данных раскрывает их данные и не имеет значимых функций".

И на стр. 100:

Квазиинкапсуляция beans, похоже, делает некоторых пуристов OO лучше, но обычно не дает никакой другой пользы.

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

Снова, по моему опыту, я обычно пошел вперед и использовал "bean" подход частных данных с геттерами и сеттерами. Но опять же, никто никогда не просил меня написать книгу о том, как писать лучший код, поэтому, возможно, у Мартина есть что сказать.

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

Основная причина непосредственного отображения переменных состояния - это возможность использовать примитивные операторы непосредственно в этих полях. Если это будет сделано хорошо, это может повысить читаемость и удобство: например, добавление комплексных чисел с + или доступ к коллекции с ключом с помощью []. Преимущества этого могут быть удивительными, если ваше использование синтаксиса следует традиционным соглашениям.

Уловка заключается в том, что операторы не являются универсальным интерфейсом. Только их можно использовать только для определенного набора встроенных типов, они могут использоваться только так, как этого требует язык, и вы не могут определить новые. Итак, как только вы определили свой публичный интерфейс с помощью примитивов, вы заперли себя в использовании этого примитива и только тот примитив (и другие вещи, которые можно легко отнести к нему). Чтобы использовать что-либо еще, вы должны танцевать вокруг этого примитива каждый раз, когда вы общаетесь с ним, и это убивает вас с суровой точки зрения: все может очень быстро стать очень хрупким.

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

Однако в этом решении были компромиссы. Бывают случаи, когда операторский интерфейс действительно работает лучше, чем на основе функций, но без перегрузки оператора этот параметр просто isn ' t доступно. Попытка заставить операторов shoehorn в любом случае заблокировать вас в некоторых дизайнерских решениях, которые вы, вероятно, действительно не хотите, чтобы они были установлены в камне. Дизайнеры Java подумали, что этот компромисс стоит того, и они могут быть даже прав. Но такие решения не приходят без каких-либо выпадений, и такая ситуация возникает там, где выпадает поражение.

Короче говоря, проблема заключается не в том, чтобы разоблачить вашу реализацию, как таковую. Проблема заключается в том, чтобы блокировать себя в этой реализации.

Ответ 6

Фактически, он прерывает инкапсуляцию, чтобы каким-либо образом подвергать любое свойство объекта - каждое свойство является деталью реализации. Просто потому, что каждый делает это, не делает это правильно. Использование аксессуаров и мутаторов (геттеры и сеттеры) не делает его лучше. Скорее, шаблоны CQRS следует использовать для поддержания инкапсуляции.

Ответ 7

Я знаю только одну опору, чтобы иметь геттеры для окончательных свойств. Это тот случай, когда вы хотите получить доступ к свойствам через интерфейс.

    public interface Point {
       int getX();
       int getY();
    }

    public class Foo implements Point {...}
    public class Foo2 implements Point {...}

В противном случае общедоступные конечные поля в порядке.

Ответ 8

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

Например, после просмотра кода, кто-то думает о добавлении другого экземпляра переменной-члена класса 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.age.

Лучшая практика состоит в том, чтобы пометить все поля как частные, иметь поля для полей. Если вы возвращаете объект или коллекцию, обязательно верните неизменяемую версию.

Иммунитетность необходима для параллельного программирования.

Ответ 9

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

Рассмотрим вопрос о том, что вы хотели бы совершить, если какой-либо метод Foo хочет предоставить вызывающему абоненту a Point3d, который инкапсулирует "X = 5, Y = 23, Z = 57", и, как это бывает, ссылка на a Point3d, где X = 5, Y = 23 и Z = 57. Если вещь Foo известна как простой непреложный держатель данных, то Foo должен просто дать вызывающей стороне ссылку на нее. Если, однако, это может быть что-то другое (например, он может содержать дополнительную информацию за пределами X, Y и Z), то Foo должен создать новый простой держатель данных, содержащий "X = 5, Y = 23, Z = 57" и дать вызывающая сторона ссылается на это.

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

Ответ 10

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

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

Также обратите внимание, что вы не можете использовать этот стиль для изменяемых элементов (например, печально известный java.util.Date). Только с геттерами вы можете сделать защитную копию или изменить представление (например, сохранить информацию Date как long)

Ответ 11

Я использую много конструкций, очень похожих на тот, который вы задали в вопросе, иногда есть вещи, которые могут быть лучше смоделированы с помощью (иногда inmutable) data-strcuture, чем с классом.

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

Например, у Роберта Мартина есть одна глава об этом в замечательной книге "Чистый код", которая должна быть прочитана, на мой взгляд.

Ответ 12

В тех случаях, когда единственная цель состоит в том, чтобы связать два значения друг с другом под значимым именем, вы можете даже подумать о том, чтобы пропустить определение любых конструкторов и сохранить переменные:

public class Sculpture {
    public int weight = 0;
    public int price = 0;
}

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

Ответ 13

Просто хочу отразить отражение:

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 все еще неизменен?:)