Почему методы получения и установки - зло

Я не собирался начинать серию статей «Это зло», но несколько читателей попросили меня объяснить, почему я упомянул, что вам следует избегать методов get / set в прошлой колонке «Почему extends Is Evil».

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

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

О природе дизайна

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

Я был ошеломлен некоторыми комментариями читателей, появившимися в прошлогодней колонке «Почему расширение - это зло» (см. Talkback на последней странице статьи). Некоторые люди считали, что я утверждал, что объектная ориентация плоха просто потому, что в ней extendsесть проблемы, как будто эти две концепции эквивалентны. Это определенно не то, о чем я думал , поэтому позвольте мне прояснить некоторые мета-вопросы.

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

Если вы не понимаете обе стороны проблемы, вы не сможете сделать разумный выбор; фактически, если вы не понимаете всех последствий своих действий, вы вообще не проектируете. Вы спотыкаетесь в темноте. Не случайно каждая глава в книге « Шаблоны проектирования банды четырех» включает раздел «Последствия», в котором описывается, когда и почему использование шаблона неуместно.

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

Точно так же маркетинговые или политические стимулы часто подталкивают идиомы дизайна. Иногда программисты принимают неверные решения, но компании хотят продвигать то, что может сделать технология, поэтому они принижают значение того, как вы это делаете, далеко не идеально. Они делают лучшее из плохой ситуации. Следовательно, вы действуете безответственно, когда принимаете какую-либо практику программирования просто потому, что «именно так вы должны делать что-то». Многие неудачные проекты Enterprise JavaBeans (EJB) подтверждают этот принцип. Технология на основе EJB - отличная технология при правильном использовании, но может буквально разрушить компанию, если используется ненадлежащим образом.

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

Абстракция данных

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

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

Если X был int, а теперь должен быть long, вы получите 1000 ошибок компиляции. Если вы неправильно решите проблему, приведя возвращаемое значение к int, код будет правильно скомпилирован, но работать не будет. (Возвращаемое значение может быть усечено.) Вы должны изменить код, окружающий каждый из этих 1000 вызовов, чтобы компенсировать это изменение. Я, конечно, не хочу так много работать.

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

Если вы создадите переменную экземпляра public, вы не сможете изменить поле по мере развития класса с течением времени, потому что вы нарушите внешний код, который использует это поле. Вы не хотите искать 1000 применений класса просто потому, что вы изменили этот класс.

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

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

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

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

Тщательно проектируя и сосредотачиваясь на том, что вы должны делать, а не на том, как вы это сделаете, вы устраняете подавляющее большинство методов получения / установки в своей программе. Не запрашивайте информацию, необходимую для работы; спросите объект, у которого есть информация, чтобы сделать работу за вас.Большинство средств доступа находят свой путь в код, потому что дизайнеры не думали о динамической модели: объектах среды выполнения и сообщениях, которые они отправляют друг другу для выполнения работы. Они начинают (неправильно) с разработки иерархии классов, а затем пытаются втиснуть эти классы в динамическую модель. Этот подход никогда не работает. Чтобы построить статическую модель, вам нужно обнаружить отношения между классами, и эти отношения точно соответствуют потоку сообщений. Связь существует между двумя классами только тогда, когда объекты одного класса отправляют сообщения объектам другого. Основная цель статической модели состоит в том, чтобы фиксировать эту информацию об ассоциации во время динамического моделирования.

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

Аксессуары также попадают в дизайн по привычке. Когда процедурные программисты принимают Java, они, как правило, начинают с написания знакомого кода. В процедурных языках нет классов, но у них есть C struct(подумайте: класс без методов). Тогда кажется естественным имитировать a struct, создавая определения классов практически без методов и только publicполей. Однако эти процедурные программисты где-то читали, что поля должны быть private, поэтому они создают поля privateи предоставляют publicметоды доступа. Но они только усложнили публичный доступ. Они определенно не сделали систему объектно-ориентированной.

Нарисуй себя

Одним из ответвлений полной инкапсуляции поля является создание пользовательского интерфейса (UI). Если вы не можете использовать аксессоры, у вас не может быть класса построителя пользовательского интерфейса для вызова getAttribute()метода. Вместо этого у классов есть такие элементы, как drawYourself(...)методы.

getIdentity()Метод может также работать, конечно, при условии , что возвращает объект , который реализует Identityинтерфейс. Этот интерфейс должен включать в себя метод drawYourself()(или JComponentметод give-me-a- that-represents-your-identity). Хотя он getIdentityначинается с «get», это не метод доступа, потому что он не просто возвращает поле. Он возвращает сложный объект с разумным поведением. Даже когда у меня есть Identityобъект, я все еще не знаю, как личность представлена ​​внутри.

Конечно, drawYourself()стратегия означает, что я (ах!) Вставляю UI-код в бизнес-логику. Подумайте, что происходит, когда требования пользовательского интерфейса меняются. Допустим, я хочу представить атрибут совершенно по-другому. Сегодня «идентичность» - это имя; завтра это имя и идентификационный номер; на следующий день это имя, идентификационный номер и фотография. Я ограничиваю объем этих изменений одним местом в коде. Если у меня есть JComponentкласс « дай мне- что-представляет-твою-идентичность», то я изолировал способ представления идентичности от остальной системы.

Имейте в виду, что я на самом деле не вставлял UI-код в бизнес-логику. Я написал уровень пользовательского интерфейса в терминах AWT (Abstract Window Toolkit) или Swing, которые являются уровнями абстракции. Фактический код пользовательского интерфейса находится в реализации AWT / Swing. В этом весь смысл уровня абстракции - изолировать вашу бизнес-логику от механизмов подсистемы. Я легко могу перенести в другую графическую среду без изменения кода, поэтому единственная проблема - небольшой беспорядок. Вы можете легко устранить этот беспорядок, переместив весь код пользовательского интерфейса во внутренний класс (или используя шаблон проектирования Façade).

JavaBeans

Вы могли бы возразить, сказав: «А как насчет JavaBeans?» Что насчет них? Конечно, вы можете создавать JavaBeans без геттеров и сеттеров. BeanCustomizer, BeanInfoИ BeanDescriptorклассы все существует именно для этой цели. Разработчики спецификации JavaBean применили идиому геттера / сеттера, потому что они думали, что это будет простой способ быстро создать компонент - то, что вы можете сделать, пока учитесь делать это правильно. К сожалению, этого никто не сделал.

Аксессоры были созданы исключительно для того, чтобы пометить определенные свойства, чтобы программа UI-builder или эквивалент могла их идентифицировать. Вы не должны сами вызывать эти методы. Они существуют для автоматизированного использования. Этот инструмент использует API интроспекции в Classклассе, чтобы найти методы и экстраполировать существование определенных свойств из имен методов. На практике эта идиома, основанная на самоанализе, не сработала. Это сделало код слишком сложным и процедурным. Программисты, которые не понимают абстракции данных, фактически вызывают методы доступа, и, как следствие, код менее удобен в обслуживании. По этой причине функция метаданных будет включена в Java 1.5 (ожидается в середине 2004 г.). Так что вместо:

частная внутренняя собственность; public int getProperty () {свойство возврата; } public void setProperty (int value} {property = value;}

Вы сможете использовать что-то вроде:

частное свойство @property int; 

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

Когда с аксессуаром все в порядке?

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