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

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

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

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

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

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


person StickyCube    schedule 20.02.2014    source источник


Ответы (13)


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

person Kevin Workman    schedule 20.02.2014
comment
Я думаю, что удобочитаемость субъективна, но одна вещь, в которой она действительно помогает, - это уменьшение небольших накладных расходов, которые вы получаете от вызова дополнительного метода только для получения значения поля. - person Kevin Workman; 20.02.2014
comment
@KevinWorkman В большинстве случаев все следы накладных расходов будут удалены путем встраивания метода. - person Marko Topolnik; 20.02.2014
comment
JIT JVM обычно устраняет накладные расходы на вызов метода, когда вы просто получаете значение поля. Вы не заметите разницы в производительности. - person mikera; 20.02.2014
comment
Я согласен, что вы, скорее всего, ничего не заметите, а преждевременная оптимизация — корень всех зол. Но ключевые слова здесь самые и вообще. Каждый контекст отличается. Просто кое-что, о чем нужно знать. - person Kevin Workman; 20.02.2014
comment
Встроенное устройство @mikera не имеет JIT - person AlexWien; 20.02.2014
comment
Тот факт, что java.awt.Point показывает свои x/y, может быть здесь вводящим в заблуждение примером. Класс Point существует со времен Java 1.0, где люди не могли полагаться на умное встраивание методов и JIT, но это был довольно критический класс, учитывая его использование в AWT. Сегодня я не могу представить реальную причину вообще делать поля публичными. Я скорее задаюсь вопросом, должны ли сегодня быть общедоступные классы в API, что включает в себя вопрос об общедоступных полях... - person Marco13; 20.02.2014
comment
@Marco13 - прикомандирован. Я бы не стал рассматривать основные классы Java как хорошие примеры кодирования, например. java.util.Дата - person Brian Agnew; 20.02.2014
comment
@ Marco13 Marco13 Советуете ли вы использовать фабрики для каждого объекта Java, каким бы простым он ни был? В более общем плане влияет ли размер кода на какое-либо проектное решение, которое вы принимаете? - person Marko Topolnik; 20.02.2014
comment
@Marco13 и Брайан: справедливо, но опять же, это все субъективно. ОП запрашивает универсальное правило, а его нет. - person Kevin Workman; 20.02.2014
comment
@Marko Topolnik: Как отметил Кевин, универсального правила не существует. Но я бы предпочел использовать интерфейсы + фабрики по умолчанию, а не общедоступные классы с общедоступными конструкторами. Иногда я думал о том, что здесь мне следовало использовать интерфейс вместо конкретного класса, но я никогда не думал о том, что здесь мне следовало использовать конкретный класс вместо интерфейса ;-) - person Marco13; 20.02.2014
comment
Как я уже сказал в своем ответе, использование public-everything плохо по умолчанию, но использование их когда это имеет смысл не приведет к автоматическому исключению вас из клуба Java. - person Kevin Workman; 20.02.2014
comment
@ Marco13 Marco13 С другой стороны, мой девиз: каждая строка кода должна доказывать свою ценность. Кажется, это приводит к совершенно другим значениям по умолчанию. - person Marko Topolnik; 20.02.2014
comment
@MarkoTopolnik, действительно, но я также думаю, что это приведет к выбору языка, отличного от Java. :) - person Charles Duffy; 21.02.2014
comment
@charlesDuffy ЕСЛИ предоставляется выбор, тогда да. Но я не одинок в этом отношении :-) - person Marko Topolnik; 21.02.2014
comment
Этот не-Java-программист никогда не понимал, почему кто-то не может напрямую предоставлять данные для всеобщего потребления. Вы всегда можете реорганизовать методы доступа, когда - и только - когда это действительно необходимо, не так ли? Если это не встроено в интерфейс библиотеки, в этом случае у вас есть большие проблемы, начиная с чувака, интерфейсы библиотек все еще должны быть определены в C. - person zwol; 21.02.2014
comment
@Zack: Вы не можете реорганизовать код, который использует ваш код. Когда вы публикуете свой код (даже внутри своей компании), почти невозможно изменить интерфейсы. - person Davor; 21.02.2014
comment
@davor В таком случае смотрите вторую половину моего комментария. - person zwol; 21.02.2014
comment
@Davor: Абстрагировать вещи имеет смысл, когда они пересекают модули. Внутри модуля должна превалировать эффективность. Такой класс, как Point, никогда не должен нуждаться в функциях доступа, потому что было бы безумием переопределять реализацию и оставлять интерфейс прежним. Если Point становится углом и расстоянием от нуля, функции доступа x и y ужасно, ужасно ошибочны. (cos(r)*d, sin(r)*d при каждом доступе???) - person Zan Lynx; 22.02.2014
comment
@Zack: Зачем такой библиотеке, как Swing, нужен интерфейс C? - person Davor; 22.02.2014
comment
@Zan Lynx: Нет, в этом случае вы вычисляете значение x, y один раз, когда они изменяются, и сохраняете их в закрытых полях. После этого просто верните их из геттеров, как и раньше. - person Davor; 22.02.2014
comment
@Davor Итак, для начала, он не заперт внутри обнесенного стеной сада Явы; потому что минимальный набор функций C заставляет вас заранее подумать о том, каким должен быть чистый API, что снижает вероятность будущих проблем; и, наконец, потому что только C может гарантировать стабильность бинарного интерфейса. - person zwol; 22.02.2014
comment
@Davor: Тогда объект больше не является неизменным. Вы должны добавить блокировки для защиты приватных полей. Вы также добавили штраф за абстракцию, потому что пользователи объекта не обязательно осознают, что они не используют нативное представление. Они могут даже оптимизировать свой код, делая предположение о представлении. Лучше разбить всех пользователей и убрать абстракцию. - person Zan Lynx; 25.02.2014
comment
@ZanLynx Я не понимаю, как это тормозит неизменность? Значения вычисляются один раз, во время построения, и сохраняются в приватных полях. После этого они возвращаются только в геттерах. Кроме того, если вы используете приватные поля для их кэширования, нет никаких накладных расходов, они, вероятно, даже будут встроенными. - person Davor; 25.02.2014

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

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

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

И на странице 100:

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

Судя по примеру кода, класс Foo может представлять собой структуру данных. Итак, основываясь на том, что я понял из обсуждения в «Чистом коде» (это больше, чем просто две цитаты, которые я дал), цель класса — предоставить данные, а не функциональность, и наличие геттеров и сеттеров, вероятно, не приносит много пользы.

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

person Paul J Abernathy    schedule 20.02.2014
comment
В Java не хватает концепций. Многие варианты использования класса Java не имеют ничего общего с инкапсуляцией и абстракцией. Другие языки предоставляют такие удобные функции, как векторы, кортежи и хэши, с кратким литеральным синтаксисом. Они полностью прозрачны и раскрывают все свои данные, что хорошо. - person Marko Topolnik; 20.02.2014
comment
Геттер - это очень слабая форма инкапсуляции, вы раскрываете все свойства своего класса и точно такие же типы. Сеттер, если намного хуже. Методы (сообщения, получаемые классом в теории ООП) — это приказы объекту выполнить некоторую работу, а не запрос информации. ИММО, очень мало оправданных вариантов использования геттеров, и я никогда не видел ни одного для сеттера. - person AlfredoCasado; 20.02.2014
comment
@AlfredoCasado Самое удивительное, что эту очевидную истину трудно объяснить лучшему практикующему специалисту по Java, что, к сожалению, очень распространено. Но обратите внимание, что обычно существуют варианты использования для геттеров/сеттеров, но не для чистых объектов данных. Например, объект конфигурации вполне может включать некоторую логику действия set property. - person Marko Topolnik; 20.02.2014
comment
@AlfredoCasado: +1, я схожу с ума, пытаясь объяснить это хардкорным парням из OO. Интенсивное использование геттеров — довольно веский намек на то, что вы просто пишете многословный процедурный код. - person Phoshi; 21.02.2014
comment
@AlfredoCasado: я постоянно слышу эту ерунду. Скажите, что getX() рассказывает о моем классе? Вы действительно знаете, что внутри есть поле X? Нельзя ли getX() реализовать как getX{ return y*z}? - person Davor; 22.02.2014
comment
@Davor, конечно, вы можете это сделать, но это не геттер, это метод с именем, которое начинается с get, не то же самое и, вероятно, очень плохое имя для метода. - person AlfredoCasado; 22.02.2014
comment
@AlfredoCasado Это прекрасное имя, и это именно то, чем является геттер, метод, который возвращает значение некоторого свойства объекта (НЕ поля!) Без побочных эффектов. Circle.getCircumference будет примерно таким же (возможно, кешируется из соображений производительности). - person Davor; 23.02.2014

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

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

person Marko Topolnik    schedule 20.02.2014
comment
, который может пригодиться, если вы разрабатываете объект, который будет широко использоваться и чья полезность будет распространяться в непредвиденном будущем. Вы можете привести один небольшой пример? - person Geek; 20.02.2014
comment
Простейшими примерами являются объекты, составляющие общедоступный API библиотеки. Внутри проекта у вас могут быть внутренние модули, которые ведут себя аналогично библиотекам. - person Marko Topolnik; 20.02.2014
comment
+1. Мое правило заключается в том, буду ли я в порядке с рефакторингом IDE «инкапсулировать поле» в будущем? Если это так, публичные поля пока не нужны в противном случае. Это охватывает создание API общедоступной библиотеки, где мой рефакторинг не может быть достигнут. - person orip; 26.02.2014
comment
@orip Очень хороший способ выразить это --- и, пожалуйста, поделитесь с нами, сколько раз вам на самом деле приходилось инкапсулировать поле final? Или вообще любое поле, если уж на то пошло? - person Marko Topolnik; 26.02.2014

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

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

person Brian Agnew    schedule 20.02.2014
comment
Если объект локального использования, это никогда не будет проблемой. В противном случае вам следует посоветовать разделить интерфейс и реализацию и предоставить абстрактную фабрику только для того, чтобы клиент не зависел от конкретного класса. - person Marko Topolnik; 20.02.2014
comment
Я бы предпочел написать это правильно, чтобы люди могли взять его и использовать повторно (напрямую или в качестве примера), а не изначально «оптимизировать» для моих конкретных целей. - person Brian Agnew; 20.02.2014
comment
Итак, вы посоветовали бы отдельный интерфейс и абстрактную фабрику? - person Marko Topolnik; 20.02.2014
comment
Не специально. Я советовал бы именно то, что я написал выше, то есть не открывать поля, даже если они неизменяемые - person Brian Agnew; 20.02.2014
comment
Значит, вы не согласны с общедоступными полями, но согласны с жесткой привязкой клиента к конкретной реализации? Я не понимаю, какая цепочка рассуждений заставляет вас предпочесть одну косвенность другой, когда практика ясно показывает, что обе они очень полезны. - person Marko Topolnik; 20.02.2014
comment
Обычно у меня есть лучшее представление о потенциальном использовании, производных и экземплярах классов, которые я определяю, в отличие от базовой реализации (которая, по моему опыту), изменяет намного больше. YMMV, конечно, но это мой общий взгляд на проблему. Я буду использовать интерфейсы и фабрики (не обязательно абстрактные фабрики) на регулярной основе. - person Brian Agnew; 20.02.2014
comment
Например, у меня есть класс package-private, который я использую для передачи кортежа данных из одного класса в другой. Этот вариант использования требует использования геттеров? - person Marko Topolnik; 20.02.2014
comment
Нет. Это не кажется неразумным, и я уверен, что делал то же самое в прошлом. Вы заметите, что мой ответ относится к тому факту, что реализация открыта и это может вызвать у вас проблемы. Он не требует единого подхода без возможности гибкости. - person Brian Agnew; 20.02.2014
comment
Я предполагаю, что ваш ответ сформулирован так, что он предполагает, что объект является частью общедоступного API и что его клиент отличается от своего провайдера. Я вижу много ответов, которые на самом деле принимают это как должное. К коду, определяющему общедоступный API, и к коду реализации применяются совершенно разные правила, и очевидно, что для каждой строки общедоступного API требуется как минимум 10 строк реализации. - person Marko Topolnik; 20.02.2014

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

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

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

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

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

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

person The Spooniest    schedule 20.02.2014

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

person Software Engineer    schedule 20.02.2014

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

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

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

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

person Shilpa    schedule 26.02.2014

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

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

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

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

person AnatolyG    schedule 20.02.2014

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

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

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

person supercat    schedule 21.02.2014

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

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

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

person Landei    schedule 21.02.2014

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

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

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

person AlfredoCasado    schedule 20.02.2014

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

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

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

person Wolf    schedule 21.02.2014
comment
@miniBill не может быть окончательным, я полагаю? Хорошо, но личное. Я изменю это. - person Wolf; 26.02.2014

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

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 по-прежнему остается неизменным? :)

person bobbel    schedule 20.02.2014
comment
Например, вы можете использовать отражение для вызова частных методов, и люди по-прежнему называют частные методы частными. Конечно, Foo неизменяем, несмотря на уловки, которые вы можете использовать, чтобы нарушить эту неизменность. Есть и другие, используемые очень известными библиотеками в мире java, например, улучшение байт-кода. - person AlfredoCasado; 20.02.2014
comment
Верно... Однако из-за этого я ненавижу рефлексию. С отражением вы можете делать все то, что вам обычно не разрешается делать. Но, наконец, вы можете! - person bobbel; 20.02.2014
comment
По крайней мере, в java у вас есть менеджеры безопасности, и вы можете защитить свой код от несанкционированного доступа, включая отражение, не самую часто используемую функцию в java, но она существует для некоторых экстремальных сценариев. Если вам не нравится отражение, вы, вероятно, любите ruby, python или javascript: P - person AlfredoCasado; 20.02.2014