Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
if(size > 0) html.append(" size=\"").append(size + 1).append("\""); html.append(">"); return html.toString(); } Разобраться в происходящем несложно . Функция конструирует тег HTML, который рисует на странице горизонтальную линию . Толщина линии задается переменной size А теперь взгляните еще раз . В этом методе смешиваются минимум два уровня аб- стракции . Первый уровень — наличие толщины у горизонтальной линии . Второй уровень — синтаксис тега HR . Код позаимствован из модуля HruleWidget проекта FitNesse . Модуль распознает строку из четырех и более дефисов и преобразует ее в соответствующий тег HR . Чем больше дефисов, тем больше толщина . Я переработал этот фрагмент кода так, как показано ниже . Обратите внимание: имя поля size изменено в соответствии с его истинным назначением (в нем хра- нится количество дополнительных дефисов) . public String render() throws Exception { HtmlTag hr = new HtmlTag("hr"); if (extraDashes > 0) 344 Разное 345 hr.addAttribute("size", hrSize(extraDashes)); return hr.html(); } private String hrSize(int height) { int hrSize = height + 1; return String.format("%d", hrSize); } Изменение разделяет два уровня абстракции . Функция render просто конструи- рует тег HR, ничего не зная о синтаксисе HTML этого тега . Модуль HtmlTag берет на себя все хлопоты с синтаксисом . Более того, при внесении этого изменения я обнаружил неприметную ошибку . Исходный код не закрывал тег HR косой чертой, как того требует стандарт XHTML (иначе говоря, он выдавал вместо ), хотя модуль HtmlTag был давно приведен в соответствие со стандартом XHTML . Разделение уровней абстракции — одна из самых важных и одновременно самых сложных в реализации функций рефакторинга . В качестве примера возьмем сле- дующий код — мою первую попытку разделения уровней абстракции в методе HruleWidget.render public String render() throws Exception { HtmlTag hr = new HtmlTag("hr"); if (size > 0) { hr.addAttribute("size", ""+(size+1)); } return hr.html(); } На этой стадии я стремился к тому, чтобы создать необходимое разделение, и обеспечить прохождение тестов . Мне удалось легко добиться этой цели, но в созданной функции по-прежнему смешивались разные уровни абстракции — на этот раз конструирование тега HR и интерпретация/форматирование перемен- ной size . Таким образом, при разбиении функции по уровням абстракции иногда обнаруживаются новые уровни, скрытые прежней структурой . g35: храните конфигурационные данные на высоких уровнях Если в программе имеется константа, определяющая значение по умолчанию или параметр конфигурации, и эта константа известна на высоких уровнях аб- стракции, — не прячьте ее в низкоуровневой функции . Передайте ее в аргументе низкоуровневой функции, вызываемой из функции высокого уровня . Рассмо- трим пример из FitNesse . 345 346 Глава 17 . Запахи и эвристические правила public static void main(String[] args) throws Exception { Arguments arguments = parseCommandLine(args); } public class Arguments { public static final String DEFAULT_PATH = "."; public static final String DEFAULT_ROOT = "FitNesseRoot"; public static final int DEFAULT_PORT = 80; public static final int DEFAULT_VERSION_DAYS = 14; } Аргументы командной строки разбираются в самой первой исполняемой строке FitNesse . Значения аргументов по умолчанию задаются в начале класса Argument Читателю не приходится спускаться на нижние уровни системы за командами следующего вида: if (arguments.port == 0) // 80 по умолчанию Конфигурационные константы находятся на очень высоком уровне . Если по- требуется, их можно легко изменить . Их значения передаются на более низкие уровни иерархии другим компонентам приложения . Значения этих констант не принадлежат нижним уровням приложения . g36: Избегайте транзитивных обращений В общем случае модуль не должен обладать слишком полной информацией о тех компонентах, с которыми он взаимодействует . Точнее, если A взаимодействует с B, а B взаимодействует с C, то модули, использующие A, не должны знать о C (то есть нежелательны конструкции вида a.getB().getC().doSomething(); ) . Иногда это называется «законом Деметры» . Прагматичные программисты используют термин «умеренный код» [PRAG, с . 138] . В любом случае все сводится к тому, что модули должны обладать информацией только о тех модулях, с которыми они непосредственно взаимодействуют, а не располагать навигационной картой всей системы . Если в нескольких модулях используется та или иная форма команды a.getB(). getC() , то в дальнейшем вам будет трудно изменить архитектуру системы, вставив между B и C промежуточный компонент Q . Придется найти каждое вхождение a.getB().getC() и преобразовать его в a.getB().getQ().getC() . Так образуются жесткие, закостеневшие архитектуры . Слишком многие модули располагают слишком подробной информацией о системе . Весь необходимый сервис должен предоставляться компонентами, с которыми напрямую взаимодействует модуль . Не заставляйте пользователя странствовать по графу объектов системы в поисках нужного метода . Проблема должна решать- ся простыми вызовами вида myCollaborator.doSomething() 346 Java 347 Java J1: Используйте обобщенные директивы импорта Если вы используете два и более класса из пакета, импортируйте весь пакет командой import package.*; Длинные списки импорта пугают читателя кода . Начало модуля не должно за- громождаться 80-строчным списком директив импорта . Список импорта должен быть точной и лаконичной конструкцией, показывающей, с какими пакетами мы собираемся работать . Конкретные директивы импорта определяют жесткие зависимости, обобщенные директивы импорта — нет . Если вы импортируете конкретный класс, то этот класс обязательно должен существовать . Но пакет, импортируемый обобщенной директивой, может не содержать ни одного класса . Директива импорта просто добавляет пакет в путь поиска имен . Таким образом, обобщенные директивы импорта не создают реальных зависимостей, а следовательно, способствуют смягчению логических привязок между модулями . В некоторых ситуациях длинные списки конкретных директив импорта бывают полезными . Например, если вы работаете с унаследованным кодом и хотите узнать, для каких классов необходимо создать заглушки и имитации, можно пройтись по конкретным спискам импорта, узнать полные имена классов и напи- сать для них соответствующие заглушки . Однако такое использование конкрет- ных директив импорта встречается крайне редко . Более того, многие современные IDE позволяют преобразовать обобщенный список импорта в список конкретных директив одной командой . Таким образом, даже в унаследованном коде лучше применять обобщенный импорт . Обобщенные директивы импорта иногда становятся причиной конфликтов имен и неоднозначностей . Два класса с одинаковыми именами, находящиеся в разных пакетах, должны импортироваться конкретными директивами (или по крайней мере их имена должны уточняться при использовании) . Это создает определен- ные неудобства, однако ситуация встречается достаточно редко, так что в общем случае обобщенные директивы импорта все равно лучше конкретных . J2: не наследуйте от констант Я уже неоднократно встречался с этим явлением, и каждый раз оно заставляло меня недовольно поморщиться . Программист размещает константы в интерфей- се, а затем наследует от этого интерфейса для получения доступа к константам . Взгляните на следующий код: public class HourlyEmployee extends Employee { private int tenthsWorked; private double hourlyRate; 347 348 Глава 17 . Запахи и эвристические правила public Money calculatePay() { int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK); int overTime = tenthsWorked - straightTime; return new Money( hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime) ); } } Где определяются константы TENTHS_PER_WEEK и OVERTIME_RATE ? Возможно, в классе Employee ; давайте посмотрим: public abstract class Employee implements PayrollConstants { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } Нет, не здесь . А где тогда? Присмотритесь повнимательнее к классу Employee . Он реализует интерфейс PayrollConstants public interface PayrollConstants { public static final int TENTHS_PER_WEEK = 400; public static final double OVERTIME_RATE = 1.5; } Совершенно отвратительная привычка! Константы скрыты на верхнем уровне иерархии наследования . Брр! Наследование не должно применяться для того, чтобы обойти языковые правила видимости . Используйте статическое импортирование . import static PayrollConstants.*; public class HourlyEmployee extends Employee { private int tenthsWorked; private double hourlyRate; public Money calculatePay() { int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK); int overTime = tenthsWorked - straightTime; return new Money( hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime) ); } } J3: Константы против перечислений В языке появились перечисления ( Java 5) — пользуйтесь ими! Не используй- те старый трюк с public static final int . Смысл int может потеряться; смысл перечислений потеряться не может, потому что они принадлежат указанному перечислению . 348 Имена 349 Тщательно изучите синтаксис перечислений . Не забудьте, что перечисления мо- гут содержать методы и поля . Это очень мощные синтаксические инструменты, значительно превосходящие int по гибкости и выразительности . Рассмотрим следующую разновидность кода начисления зарплаты: public class HourlyEmployee extends Employee { private int tenthsWorked; HourlyPayGrade grade; public Money calculatePay() { int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK); int overTime = tenthsWorked - straightTime; return new Money( grade.rate() * (tenthsWorked + OVERTIME_RATE * overTime) ); } } public enum HourlyPayGrade { APPRENTICE { public double rate() { return 1.0; } }, LEUTENANT_JOURNEYMAN { public double rate() { return 1.2; } }, JOURNEYMAN { public double rate() { return 1.5; } }, MASTER { public double rate() { return 2.0; } }; public abstract double rate(); } Имена n1: Используйте содержательные имена Не торопитесь с выбором имен . Позаботьтесь о том, чтобы имена были со- держательными . Помните, что смысл может изменяться в ходе развития про- 349 350 Глава 17 . Запахи и эвристические правила граммного продукта; почаще переосмысливайте уместность выбранных вами имен . Не рассматривайте это как дополнительный «фактор комфортности» . Имена в программных продуктах на 90% определяют удобочитаемость кода . Не жалейте времени на то, чтобы выбрать их осмысленно, и поддерживайте их актуальность . Имена слишком важны, чтобы относиться к ним легкомысленно . Возьмем следующий код . Что он делает? Когда я представлю вам тот же код с нормально выбранными именами, вы моментально поймете его смысл, но в этом виде он представляет собой мешанину из символов и «волшебных чисел» . public int x() { int q = 0; int z = 0; for (int kk = 0; kk < 10; kk++) { if (l[z] == 10) { q += 10 + (l[z + 1] + l[z + 2]); z += 1; } else if (l[z] + l[z + 1] == 10) { q += 10 + l[z + 2]; z += 2; } else { q += l[z] + l[z + 1]; z += 2; } } return q; } А вот как должен был выглядеть этот код . Вообще говоря, этот фрагмент чуть менее полон, чем приведенный выше . И все же вы сразу догадаетесь, что мы пытаемся сделать, и с большой вероятностью сможете написать отсутствующие функции, основываясь на своих предположениях . «Волшебные числа» перестали быть волшебными, а структура алгоритма радует своей очевидностью . public int score() { int score = 0; int frame = 0; for (int frameNumber = 0; frameNumber < 10; frameNumber++) { if (isStrike(frame)) { score += 10 + nextTwoBallsForStrike(frame); frame += 1; } else if (isSpare(frame)) { score += 10 + nextBallForSpare(frame); frame += 2; } else { score += twoBallsInFrame(frame); 350 Имена 351 frame += 2; } } return score; } Сила хорошо выбранных имен заключается в том, что они дополняют струк- туру кода описаниями . На основании этих описаний у читателя формируются определенные предположения по поводу того, что делают другие функции мо- дуля . Взглянув на приведенный код, вы сможете представить себе примерную реализацию isStrike() . А при чтении метода isStrike() становится очевидно, что он делает «примерно то, что предполагалось» 1 private boolean isStrike(int frame) { return rolls[frame] == 10; } n2: Выбирайте имена на подходящем уровне абстракции Не используйте имена, передающие информацию о реализации . Имена должны отражать уровень абстракции, на котором работает класс или функция . Сделать это непросто — и снова потому, что люди слишком хорошо справляются со сме- шением разных уровней абстракции . При каждом просмотре кода вам с большой вероятностью попадется переменная, имя которой выбрано на слишком низком уровне . Воспользуйтесь случаем и измените его . Чтобы ваш код хорошо читался, вы должны серьезно относиться к его непрерывному совершенствованию . Возь- мем следующий интерфейс Modem : public interface Modem { boolean dial(String phoneNumber); boolean disconnect(); boolean send(char c); char recv(); String getConnectedPhoneNumber(); } На первый взгляд все хорошо — имена функций выглядят разумно . В самом деле, во многих приложениях они точно соответствуют выполняемым операциям . А если для установления связи используется не коммутируемое подключение, а какой-то другой механизм? Например, модемы могут связываться на физи- ческом уровне (как кабельные модемы, обеспечивающие доступ к Интернету во многих домах) . А может быть, связь устанавливается посредством отправки номера порта коммутатору через интерфейс USB . Разумеется, концепция те- лефонных номеров в таких случаях относится к неверному уровню абстракции . 1 См . цитату Уорда Каннингема на с . 34 . 351 352 Глава 17 . Запахи и эвристические правила Более правильная стратегия выбора имен в таких сценариях может выглядеть так: public interface Modem { boolean connect(String connectionLocator); boolean disconnect(); boolean send(char c); char recv(); String getConnectedLocator(); } Теперь имена функций никак не ассоциируются с телефонными номерами . Они могут использоваться как для подключения по телефонной линии, так и для любой другой стратегии подключения . n3: По возможности используйте стандартную номенклатуру Имена проще понять, если они основаны на существующих конвенциях или стан- дартных обозначениях . Например, при использовании паттерна ДЕКОРАТОР можно включить в имена декорирующих классов слово Decorator . Например, имя AutoHangupModemDecorator может быть присвоено классу, который дополняет класс Modem возможностью автоматического разрыва связи в конце сеанса . Паттерны составляют лишь одну разновидность стандартов . Например, в языке Java функции, преобразующие объекты в строковые представления, часто на- зываются toString . Лучше следовать подобным стандартным конвенциям, чем изобретать их заново . Группы часто разрабатывают собственные стандартные системы имен для кон- кретного проекта . Эрик Эванс (Eric Evans) называет их всеобщим языком про- екта 1 . Широко используйте термины этого языка в своем коде . Чем больше вы используете имена, переопределенные специальным смыслом, относящимся к вашему конкретному проекту, тем проще читателю понять, о чем идет речь в вашем коде . n4: недвусмысленные имена Выбирайте имена, которые максимально недвусмысленно передают назначение функции или переменной . Рассмотрим пример из FitNesse: private String doRename() throws Exception { if(refactorReferences) renameReferences(); renamePage(); 1 [DDD] . 352 Имена 353 pathToRename.removeNameFromEnd(); pathToRename.addNameToEnd(newName); return PathParser.render(pathToRename); } Имя функции получилось слишком общим и расплывчатым; оно ничего не гово- рит о том, что делает функция . Ситуацию усугубляет тот факт, что в функции с именем doRename находится функция renamePage ! Что можно сказать о различиях между этими функциями по их именам? Ничего . Функцию было бы правильнее назвать renamePageAndOptionallyAllReferences . На первый взгляд имя кажется слишком длинным, но функция вызывается только из одной точки модуля, поэтому ее документирующая ценность перевешивает длину . n5: Используйте длинные имена для длинных областей видимости Длина имени должна соответствовать длине его области видимости . Пере- менным с крошечной областью видимости можно присваивать очень короткие имена, но у переменных с большей областью видимости имена должны быть длинными . Если область видимости переменной составляет всего пять строк, то переменной можно присвоить имя i или j . Возьмем следующий фрагмент из старой стандарт- ной игры «Bowling»: private void rollMany(int n, int pins) { for (int i=0; i } Смысл переменной i абсолютно очевиден . Какое-нибудь раздражающее имя вида rollCount только затемнило бы смысл этого тривиального кода . С другой стороны, смысл коротких имен переменных и функций рассеивается на длинных дистанциях . Таким образом, чем длиннее область видимости имени, тем более длинным и точным должно быть ее имя . n6: Избегайте кодирования Информация о типе или области видимости не должна кодироваться в именах . Префиксы вида m_ или f бессмысленны в современных средах . Кроме того, ин- формация о проекте и/или подсистеме (например, префикс vis_ для подсистемы визуализации) также отвлекает читателя и является избыточной . Современные среды разработки позволяют получить всю необходимую информацию без уродования имен . Поддерживайте чистоту в своих именах, не загрязняйте их венгерской записью . 353 |