Использование неизменяемых объектов становится все более распространенным, даже если под рукой никогда не предполагается, что программа запускается параллельно. И все же мы по-прежнему используем геттеры, которые требуют 3 строки шаблона для каждого поля и 5 дополнительных символов при каждом доступе (на вашем любимом основном языке OO). Хотя это может показаться тривиальным, и многие редакторы в любом случае снимают большую часть бремени с программиста, это по-прежнему ненужные усилия.
Каковы причины постоянного использования средств доступа по сравнению с прямым доступом к полю неизменяемых объектов? В частности, есть ли преимущества в том, чтобы заставить пользователя использовать средства доступа (для клиента или автора библиотеки), и если да, то каковы они?
Обратите внимание, что я имею в виду неизменный объекты, в отличие от этого вопрос, что относится к объектам в целом. Для ясности, на неизменяемых объектах нет сеттеров.
Я бы сказал, что это на самом деле зависит от языка. Извините, я немного расскажу о C #, так как думаю, что это поможет ответить на этот вопрос.
Я не уверен, знакомы ли вы с C #, но его дизайн, инструменты и т. Д. Очень интуитивно понятны и удобны для программистов.
Одна особенность C # (которая также существует в Python, D и т. Д.), Которая помогает это имущество; Свойство — это, в основном, пара методов (получатель и / или установщик), которые снаружи выглядят просто как поле экземпляра: вы можете присвоить ему и вы можете читать из него как переменную экземпляра.
Внутренне, конечно, это метод, и он может делать все что угодно.
Но типы данных C # также иногда имеют методы GetXYZ () и SetXYZ (), а иногда они даже напрямую выставляют свои поля … и возникает вопрос: как выбрать, что делать, когда?
Свойства должны вести себя так, как будто они являются полями; если метод не может, он не должен быть изменен на свойство. Методы лучше, чем свойства в следующих ситуациях:
- Метод выполняет трудоемкую операцию. Метод заметно медленнее, чем время, необходимое для установки или получения значения поля.
- Метод выполняет преобразование. Доступ к полю не возвращает преобразованную версию данных, которые оно хранит.
Get
Метод имеет заметный побочный эффект. Получение значения поля не вызывает никаких побочных эффектов.- Порядок исполнения важен. Установка значения поля не зависит от возникновения других операций.
- Вызов метода два раза подряд создает разные результаты.
- Метод является статическим, но возвращает объект, который может быть изменен вызывающей стороной. Получение значения поля не позволяет вызывающей стороне изменять данные, которые хранятся в этом поле.
- Метод возвращает массив.
уведомление что вся цель из этих рекомендаций, чтобы все свойства выглядели как поля внешне.
Таким образом, единственными реальными причинами использования свойств вместо полей будет:
Теперь вернемся к Java / C ++ и неизменным типам данных.
Какие из этих пунктов применимы к нашему сценарию?
Теперь Java и C ++ не имеют свойств, но методы занимают их место — так что совет, приведенный выше, все еще применяется, и правило для языков без свойств становится:
Важно понять, что все это философское; все эти руководства основаны на что ожидает программист. Очевидно, что цель в конце дня состоит в том, чтобы (1) выполнить работу и (2) сделать код читаемым / поддерживаемым. Вышеуказанное руководство оказалось полезным для того, чтобы это произошло, и ваша цель должна заключаться в том, чтобы сделать все, что вам нравится, чтобы это произошло.
Инкапсуляция служит нескольким полезным целям, но наиболее важной является сокрытие информации. Скрывая поле как деталь реализации, вы защищаете клиентов объекта от зависимости от того, есть ли там поле. Например, будущая версия вашего объекта может захотеть вычислить или извлечь значение лениво, и это можно сделать, только если вы сможете перехватить запрос на чтение поля.
Тем не менее, нет причин для того, чтобы получатели были особенно многословны. В частности, в мире Java даже там, где префикс get очень укоренился, вы все равно найдете методы getter, названные в честь самого значения (то есть метода foo()
вместо getFoo()
), и это отличный способ сохранить несколько символов. Во многих других ОО-языках вы можете определить метод получения и по-прежнему использовать синтаксис, который выглядит как доступ к полю, так что никакой дополнительной детализации вообще нет.
Неизменяемые объекты должны использовать прямой доступ к полю для однородность и потому что это позволяет проектировать объекты, которые выполнять точно так, как клиент ожидает, что они должны.
Рассмотрим систему, в которой каждое изменяемое поле было скрыто за аксессорами, а каждое неизменяемое поле — нет. Теперь рассмотрим следующий фрагмент кода:
class Node {
private final List<Node> children;
Node(List<Node> children) {
this.children = new LinkedList<>(children);
}
public List<Node> getChildren() {
return /* Something here */;
}
}
Не зная точной реализации Node
, как вы должны сделать, когда вы разрабатываете по контракту, где бы вы ни увидели root.getChildren()
Вы можете только предположить, что происходит одна из трех вещей:
children
возвращается как есть, и вы не можете изменить список, потому что вы нарушите неизменность Node. Для того, чтобы изменить List
Вы должны скопировать это, операция O (n).return new LinkedList<>(children);
, Это операция O (n). Вы можете изменить этот список.return new UnmodifiableList<>(children);
, Это операция O (1). Опять же, чтобы сделать это изменить List
Вы должны скопировать это, операция O (n).Во всех случаях изменение возвращаемого списка требует операции O (n) для его копирования, в то время как доступ только для чтения занимает где-нибудь от O (1) или O (n). Здесь важно отметить, что, следуя дизайну по контракту, вы не может знать, какую реализацию выбрал автор библиотеки и, следовательно, должен принять наихудший случай O (n). Следовательно, O (n) доступ и O (n) для создания собственной изменяемой копии.
Теперь рассмотрим следующее:
class Node {
public final UnmodifiableList<Node> children;
Node(List<Node> children) {
this.children = new UnmodifiableList<>(children);
}
}
Теперь везде, где вы видите root.children
есть только одна возможность, а именно UnmodifiableList
и поэтому Вы можете принять O (1) доступ и O (n) для создания локально изменяемой копии.
Ясно, что можно сделать выводы о характеристиках производительности доступа к полю в последнем случае, тогда как в первом случае можно сделать вывод, что производительность в худшем случае и, таким образом, случай, когда мы должен Предположим, это намного хуже, чем прямой доступ к полю. Как напоминание, это означает, что программист должен учитывать функцию сложности O (n) на каждый доступ.
Таким образом, в системе такого типа, где бы вы ни увидели геттер, клиент автоматически узнает, что либо получатель соответствует изменяемому полю, либо получатель выполняет какую-то операцию, будь то длительное защитное копирование O (n). , ленивая инициализация, преобразование или другое. Всякий раз, когда клиент видит прямой доступ к полю, он сразу же узнает характеристики производительности доступа к этому полю.
Следуя этому стилю, программист может получить больше информации о контракт предоставленный объектом, с которым он / она взаимодействует. Этот стиль также способствует равномерной неизменности, потому что, как только вы измените вышеупомянутый фрагмент UnmodifiableList
к интерфейсу List
прямой доступ к полю позволяет мутировать объект, тем самым вынуждая тщательно спроектировать иерархию объекта, чтобы она была неизменной сверху вниз.
Хорошей новостью является то, что вы не только получаете все преимущества неизменяемости, вы также можете вывести характеристики производительности доступа к полю, где бы он ни находился, не глядя на реализацию и с уверенностью, что она никогда не изменится.
Джошуа Блох, в Эффективная Java (2-е издание) «Пункт 14: В открытых классах используйте методы доступа, а не открытые поля», что говорит об экспонировании неизменяемых полей:
Хотя для публичного класса никогда не бывает хорошей идеей выставлять поля напрямую, это
менее вредно, если поля неизменны. Вы не можете изменить представление
такой класс без изменения его API, и вы не можете выполнять вспомогательные действия, когда
поле читается, но вы можете применить инварианты.
и резюмирует главу:
Таким образом, публичные классы никогда не должны выставлять изменяемые поля. Меньше
вредно, хотя все еще сомнительно, для публичных классов выставлять неизменные поля.
Вы можете иметь открытые конечные поля (чтобы имитировать некоторую неизменность), но это не значит, что ссылочные объекты не могут изменить свое состояние. Нам все еще нужна защитная копия в некоторых случаях.
public class Temp {
public final List<Integer> list;
public Temp() {
this.list = new ArrayList<Integer>();
this.list.add(42);
}
public static void foo() {
Temp temp = new Temp();
temp.list = null; // not valid
temp.list.clear(); //perferctly fine, reference didn't change.
}
}
Каковы причины постоянного использования средств доступа по сравнению с прямым доступом к полю неизменяемых объектов? В частности, есть ли преимущества в том, чтобы заставить пользователя использовать средства доступа (для клиента или автора библиотеки), и если да, то каковы они?
Вы звучите как процедурный программист, спрашивающий, почему вы не можете получить доступ к полям напрямую, но должны создавать средства доступа. Главная проблема в том, что даже то, как вы задаете вопрос, неверно. Это не то, как работает ОО-дизайн — вы проектируете поведение объекта с помощью его методов и демонстрируете это. Затем вы создаете внутренние поля, если это необходимо, для реализации этого поведения. Таким образом, формулировка так: «Я создаю эти поля, а затем выставляю каждое из них получателем, это многословно» — явный признак неправильного дизайна ОО.
Практика ООП заключается в том, чтобы инкапсулировать поля, а затем выставлять их только методом getters. Если вы выставите поле напрямую, это означает, что вам придется сделать его публичным. Публикация полей не является хорошей идеей, поскольку она раскрывает внутреннее состояние объекта.
Поэтому публикация вашего поля / членов данных не является хорошей практикой и нарушает принцип инкапсуляции ООП. Также я бы сказал, что это не относится к неизменным объектам; это верно и для неизменных объектов.
редактировать
Как указано @Thilo; Другая причина: может быть, вам не нужно показывать, как хранится поле.
спасибо @Thilo.
Одна очень практическая причина для продолжающейся практики генерации (в настоящее время никто не пишет их вручную) геттеров в программах Java, даже для неизменяемых «значимых» объектов, где, на мой взгляд, нет необходимости в накладных расходах:
Многие библиотеки и инструменты опираются на старые соглашения JavaBeans (или, по крайней мере, часть его получателей и установщиков).
Эти инструменты, которые используют отражение или другие динамические методы для доступа к значениям полей через геттеры, не могут обрабатывать доступ к простым публичным полям. JSP — пример, который приходит мне в голову.
Также современные IDE упрощают создание геттеров для одного или нескольких полей за раз, а также для изменения имени геттера при изменении имени поля.
Поэтому мы просто продолжаем писать геттеры даже для неизменных объектов.