Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
120 Глава 5 . Форматирование листинг 5 .6 (продолжение) public double getMeanLineWidth() { return (double)totalChars/lineCount; } public int getMedianLineWidth() { Integer[] sortedWidths = getSortedWidths(); int cumulativeLineCount = 0; for (int width : sortedWidths) { cumulativeLineCount += lineCountForWidth(width); if (cumulativeLineCount > lineCount/2) return width; } throw new Error("Cannot get here"); } private int lineCountForWidth(int width) { return lineWidthHistogram.getLinesforWidth(width).size(); } private Integer[] getSortedWidths() { Set Integer[] sortedWidths = (widths.toArray(new Integer[0])); Arrays.sort(sortedWidths); return sortedWidths; } } 120 Объекты и структуры данных Существует веская причина для ограничения доступа к переменным в програм- мах: мы не хотим, чтобы другие программисты зависели от них . Мы хотим иметь возможность свободно менять тип или реализацию этих переменных так, как считаем нужным . Тогда почему же многие программисты автоматически вклю- чают в свои объекты методы чтения/записи, предоставляя доступ к приватным переменным так, словно они являются открытыми? Абстракция данных Давайте сравним между собой листинги 6 .1 и 6 .2 . В обоих случаях код представ- ляет точку на декартовой плоскости . Однако в одном случае реализация открыта, а в другом она полностью скрыта от внешнего пользователя . 6 121 122 Глава 6 . Объекты и структуры данных листинг 6 .1 . Конкретная реализация Point public class Point { public double x; public double y; } листинг 6 .2 . Абстрактная реализация Point public interface Point { double getX(); double getY(); void setCartesian(double x, double y); double getR(); double getTheta(); void setPolar(double r, double theta); } Элегантность решения из листинга 6 .2 заключается в том, что внешний пользо- ватель не знает, какие координаты использованы в реализации — прямоугольные или полярные . А может, еще какие-нибудь! Тем не менее интерфейс безусловно напоминает структуру данных . Однако он представляет нечто большее, чем обычную структуру данных . Его методы устанавливают политику доступа к данным . Пользователь может читать значения координат независимо друг от друга, но присваивание координат долж- но выполняться одновременно, в режиме атомарной операции . С другой стороны, листинг 6 .1 явно реализован в прямоугольных координатах, а пользователь вынужден работать с этими координатами независимо . Более того, такое решение раскрывает реализацию даже в том случае, если бы переменные были объявлены приватными, и мы использовали одиночные методы чтения/записи . Скрытие реализации не сводится к созданию прослойки функций между пере- менными . Скрытие реализации направлено на формирование абстракций! Класс не просто ограничивает доступ к переменным через методы чтения/записи . Вместо этого он предоставляет абстрактные интерфейсы, посредством которых пользователь оперирует с сущностью данных . Знать, как эти данные реализованы, ему при этом не обязательно . Возьмем листинги 6 .3 и 6 .4 . В первом случае для получения информации о за- пасе топлива используются конкретные физические показатели, а во втором — абстрактные проценты . В первом, конкретном случае можно быть уверенным в том, что методы представляют собой обычные методы доступа к переменным . Во втором, абстрактном случае пользователь не имеет ни малейшего представ- ления о фактическом формате данных . листинг 6 .3 . Конкретная реализация Vehicle public interface Vehicle { double getFuelTankCapacityInGallons(); double getGallonsOfGasoline(); } 122 Антисимметрия данных/объектов 123 листинг 6 .4 . Абстрактная реализация Vehicle Abstract Vehicle public interface Vehicle { double getPercentFuelRemaining(); } В обоих примерах вторая реализация является предпочтительной . Мы не хотим раскрывать подробности строения данных . Вместо этого желательно использо- вать представление данных на абстрактном уровне . Задача не решается простым использованием интерфейсов и/или методов чтения/записи . Чтобы найти луч- ший способ представления данных, содержащихся в объекте, необходимо серьез- но поразмыслить . Бездумное добавление методов чтения и записи — худший из всех возможных вариантов . Антисимметрия данных/объектов Два предыдущих примера показывают, чем объекты отличаются от структур дан- ных . Объекты скрывают свои данные за абстракциями и предоставляют функции, работающие с этими данными . Структуры данных раскрывают свои данные и не имеют осмысленных функций . А теперь еще раз перечитайте эти определения . Обратите внимание на то, как они дополняют друг друга, фактически являясь противоположностями . Различия могут показаться тривиальными, но они при- водят к далеко идущим последствиям . Возьмем процедурный пример из листинга 6 .5 . Класс Geometry работает с тремя классами геометрических фигур . Классы фигур представляют собой простые структуры данных, лишенные какого-либо поведения . Все поведение сосредо- точено в классе Geometry листинг 6 .5 . Процедурные фигуры public class Square { public Point topLeft; public double side; } public class Rectangle { public Point topLeft; public double height; public double width; } public class Circle { public Point center; public double radius; } продолжение 123 124 Глава 6 . Объекты и структуры данных листинг 6 .5 (продолжение) public class Geometry { public final double PI = 3.141592653589793; public double area(Object shape) throws NoSuchShapeException { if (shape instanceof Square) { Square s = (Square)shape; return s.side * s.side; } else if (shape instanceof Rectangle) { Rectangle r = (Rectangle)shape; return r.height * r.width; } else if (shape instanceof Circle) { Circle c = (Circle)shape; return PI * c.radius * c.radius; } throw new NoSuchShapeException(); } } Объектно-ориентированный программист недовольно поморщится и пожалуется на процедурную природу реализации — и будет прав . Но возможно, его пре- зрительная усмешка не обоснована . Подумайте, что произойдет при включении в Geometry функции perimeter() . Классы фигур остаются неизменными! И все остальные классы, зависящие от них, тоже остаются неизменными! С другой стороны, при добавлении новой разновидности фигур мне придется изменять все функции Geometry , чтобы они могли работать с ней . Перечитайте еще раз . Обратите внимание на то, что эти два условия диаметрально противоположны . Теперь рассмотрим объектно-ориентированное решение из листинга 6 .6 . Метод area() является полиморфным, класс Geometry становится лишним . Добавление новой фигуры не затрагивает ни одну из существующих функций, но при добав- лении новой функции приходится изменять все фигуры! 1 листинг 6 .6 . Полиморфные фигуры Polymorphic Shapes public class Square implements Shape { private Point topLeft; private double side; public double area() { return side*side; } } 1 У проблемы существуют обходные решения, хорошо известные опытным объектно- ориентированным программистам: например, паттерн ПОСЕТИТЕЛЬ или двойная дис- петчеризация . Но у этих приемов имеются собственные издержки, к тому же они обычно возвращают структуру к состоянию процедурной программы . 124 Антисимметрия данных/объектов 125 public class Rectangle implements Shape { private Point topLeft; private double height; private double width; public double area() { return height * width; } } public class Circle implements Shape { private Point center; private double radius; public final double PI = 3.141592653589793; public double area() { return PI * radius * radius; } } И снова мы наблюдаем взаимодополняющую природу этих двух определений . В этом проявляется основополагающая дихотомия между объектами и структу- рами данных . Процедурный код (код, использующий структуры данных) позволяет легко добав- лять новые функции без изменения существующих структур данных. Объектно- ориентированный код, напротив, упрощает добавление новых классов без изме- нения существующих функций. Обратные утверждения также истинны . Процедурный код усложняет добавление новых структур данных, потому что оно требует изменения всех функций. Объектно-ориентированный код усложняет добавление новых функций, потому что для этого должны измениться все классы. Таким образом, то, что сложно в ОО, просто в процедурном программировании, а то, что сложно в процедурном программировании, просто в ОО! В любой сложной системе возникают ситуации, когда вместо новых функций в систему требуется включить новые типы данных . Для таких ситуаций объекты и объектно-ориентированное программирование особенно уместны . Впрочем, бывает и обратное — вместо новых типов данных требуется добавить новые функции . Тогда лучше подходит процедурный код и структуры данных . Опытные программисты хорошо знают: представление о том, что все данные должны представляться в виде объектов — миф . Иногда предпочтительны про- стые структуры данных и процедуры, работающие с ними . 125 126 Глава 6 . Объекты и структуры данных Закон Деметры Хорошо известное эвристическое правило, называемое законом Деметры 1 , гласит, что модуль не должен знать внутреннее устройство тех объектов, с которыми он работает . Как мы видели в предыдущем разделе, объекты скрывают свои данные и предоставляют операции для работы с ними . Это означает, что объект не дол- жен раскрывать свою внутреннюю структуру через методы доступа, потому что внутреннюю структуру следует скрывать . В более точной формулировке закон Деметры гласит, что метод f класса C должен ограничиваться вызовом методов следующих объектов: C; объекты, созданные f; объекты, переданные f в качестве аргумента; объекты, хранящиеся в переменной экземпляра C . Метод не должен вызывать методы объектов, возвращаемых любыми из раз- решенных функций . Другими словами, разговаривать можно с друзьями, но не с чужаками . Следующий код нарушает закон Деметры (среди прочего), потому что он вызы- вает функцию getScratchDir() для возвращаемого значения getOptions() , а затем вызывает getAbsolutePath() для возвращаемого значения getScratchDir() final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); Крушение поезда Подобная структура кода часто называется «крушением поезда», потому что цепочки вызовов напоминают сцепленные вагоны поезда . Такие конструкции считаются проявлением небрежного стиля программирования и их следует из- бегать [G36] . Обычно цепочки лучше разделить в следующем виде: Options opts = ctxt.getOptions(); File scratchDir = opts.getScratchDir(); final String outputDir = scratchDir.getAbsolutePath(); Нарушают ли эти два фрагмента закон Деметры? Несомненно, вмещающий модуль знает, что объект контекста ctxt содержит значения параметров, в число которых входит и временный каталог, обладающий абсолютным путем . Это довольно большой объем информации 1 http://en .wikipedia .org/wiki/Law_of_Demeter 126 Закон Деметры 127 для одной функции . Вызывающая функция должна знать, как перемещаться между множеством разных объектов . Нарушает ли этот код закон Деметры или нет? Все зависит от того, чем являются ctxt , Options и ScratchDir — объектами или структурами данных . Если это объ- екты, то их внутренняя структура должна скрываться, поэтому необходимость ин формации об их строении является явным нарушением закона Деметры . С другой стороны, если ctxt , Options и ScratchDir представляют собой обычные структуры данных, не обладающие поведением, то они естественным образом раскрывают свою внутреннюю структуру, а закон Деметры на них не распро- страняется . Применение функций доступа затрудняет ситуацию . Если бы код был записан следующим образом, вероятно, у нас не возникало бы вопросов по поводу нару- шения закона Деметры: final String outputDir = ctxt.options.scratchDir.absolutePath; Ситуация существенно упростилась бы, если бы структуры данных просто со- держали открытые переменные без функций, а объекты — приватные переменные с открытыми функциями . Однако некоторые существующие инфраструктуры и стандарты (например, Beans) требуют, чтобы даже простые структуры данных имели методы чтения и записи . Гибриды Вся эта неразбериха иногда приводит к появлению гибридных структур — напо- ловину объектов, наполовину структур данных . Гибриды содержат как функции для выполнения важных операций, так и открытые переменные или открытые методы чтения/записи, которые во всех отношениях делают приватные перемен- ные открытыми . Другим внешним функциям предлагается использовать эти пе- ременные так, как в процедурных программах используются структуры данных 1 Подобные гибриды усложняют как добавление новых функций, так и новых структур данных . Они объединяют все худшее из обеих категорий . Не исполь- зуйте гибриды . Они являются признаком сумбурного проектирования, авторы которого не уверены (или еще хуже, не знают), что они собираются защищать: функции или типы . Скрытие структуры А если ctxt , options и scratchDir представляют собой объекты с реальным пове- дением? Поскольку объекты должны скрывать свою внутреннюю структуру, мы не сможем перемещаться между ними . Как же в этом случае узнать абсолютный путь временного каталога? 1 Иногда это называется «функциональной завистью» (Feature Envy) — из [Refactoring] . 127 128 Глава 6 . Объекты и структуры данных ctxt.getAbsolutePathOfScratchDirectoryOption(); или ctx.getScratchDirectoryOption().getAbsolutePath() Первый вариант приведет к разрастанию набора методов объекта ctxt . Второй вариант предполагает, что getScratchDirectoryOption() возвращает структуру данных, а не объект . Ни один из вариантов не вызывает энтузиазма . Если ctxt является объектом, то мы должны приказать ему выполнить некую опе- рацию, а не запрашивать у него информацию о его внутреннем устройстве . За- чем нам понадобился абсолютный путь к временному каталогу? Что мы собира- емся с ним делать? Рассмотрим следующий фрагмент того же модуля (располо- женный на много строк ниже): String outFile = outputDir + "/" + className.replace('.', '/') + ".class"; FileOutputStream fout = new FileOutputStream(outFile); BufferedOutputStream bos = new BufferedOutputStream(fout); Смешение разных уровней детализации [G34][G6] выглядит немного пугающе . Точки, косые черты, расширения файлов и объекты File не должны так беспечно перемешиваться между собой и с окружающим кодом . Но если не обращать на это внимания, мы видим, что абсолютный путь временного каталога определялся для создания временного файла с заданным именем . Так почему бы не приказать объекту ctxt выполнить эту операцию? BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName); Выглядит вполне разумно! Такое решение позволяет объекту ctxt скрыть свое внутреннее строение, а текущей функции не приходится нарушать закон Деме- тры, перемещаясь между объектами, о которых ей знать не положено . Объекты передачи данных Квинтэссенцией структуры данных является класс с открытыми переменными и без функций . Иногда такие структуры называются объектами передачи данных, или DTO (Data Transfer Object) . Структуры DTO чрезвычайно полезны, особен- но при работе с базами данных, разборе сообщений из сокетов и т . д . С них часто начинается серия фаз преобразования низкоуровневых данных, полученных из базы, в объекты кода приложения . Несколько большее распространение получила форма bean-компонентов, пред- ставленная в листинге 6 .7 . Bean-компоненты состоят из приватных переменных, операции с которыми осуществляются при помощи методов чтения/записи . Подобная форма псевдоинкапсуляции поднимает настроение некоторым блю- стителям чистоты ОО, но обычно не имеет других преимуществ . листинг 6 .7 . address.java public class Address { private String street; private String streetExtra; 128 Закон Деметры 129 private String city; private String state; private String zip; public Address(String street, String streetExtra, String city, String state, String zip) { this.street = street; this.streetExtra = streetExtra; this.city = city; this.state = state; this.zip = zip; } public String getStreet() { return street; } public String getStreetExtra() { return streetExtra; } public String getCity() { return city; } public String getState() { return state; } public String getZip() { return zip; } } Активные записи Активные записи (Active Records) составляют особую разновидность DTO . Они тоже представляют собой структуры данных с открытыми переменными (или пе- ременными с bean-доступом), но обычно в них присутствуют навигационные ме- тоды — такие, как save или find . Активные записи чаще всего являются результа- тами прямого преобразования таблиц баз данных или других источников данных . К сожалению, разработчики часто пытаются интерпретировать такие структуры данных, как объекты, и включают в них методы, реализующие бизнес-логику . Однако такой подход нежелателен, так как он создает гибрид между структурой данных и объектом . Конечно, проблема решается иначе: активные записи интерпретируются как структуры данных, а в программе создаются отдельные объекты, которые со- держат бизнес-логику и скрывают свои внутренние данные (которые, возможно, представляют собой обычные экземпляры класса активной записи) . 129 |