Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
62 Глава 3 . Функции Взгляните на листинг 3 .7 в конце этой главы . В нем приведен полный код функ- ции testableHtml , переработанной в соответствии с описанными здесь принципа- ми . Обратите внимание на то, как каждая функция «представляет» читателю сле- дующую функцию и как каждая функция остается на едином уровне абстракции . Команды switch Написать компактную команду switch довольно сложно 1 . Даже команда switch всего с двумя условиями занимает больше места, чем в моем представлении должен занимать один блок или функция . Также трудно создать команду switch , которая делает что-то одно — по своей природе команды switch всегда выполняют N операций . К сожалению, обойтись без команд switch удается не всегда, но по крайней мере мы можем позаботиться о том, чтобы эти команды были скрыты в низкоуровневом классе и не дублировались в коде . И конечно, в этом нам может помочь полиморфизм . В листинге 3 .4 представлена всего одна операция, зависящая от типа работника . листинг 3 .4 . Payroll.java public Money calculatePay(Employee e) throws InvalidEmployeeType { switch (e.type) { case COMMISSIONED: return calculateCommissionedPay(e); case HOURLY: return calculateHourlyPay(e); case SALARIED: return calculateSalariedPay(e); default: throw new InvalidEmployeeType(e.type); } } Эта функция имеет ряд недостатков . Во-первых, она велика, а при добавлении новых типов работников она будет разрастаться . Во-вторых, она совершенно очевидно выполняет более одной операции . В-третьих, она нарушает принцип единой ответственности 2 , так как у нее существует несколько возможных причин изменения . В-четвертых, она нарушает принцип открытости/закрытости 3 , пото- му что код функции должен изменяться при каждом добавлении новых типов . 1 Разумеется, сюда же относятся и длинные цепочки if/else . 2 http://en .wikipedia .org/wiki/Single_responsibility_principle; http://www .objectmentor .com/ resources/articles/srp .pdf . 3 http://en .wikipedia .org/wiki/Open/closed_principle; http://www .objectmentor .com/ resources/articles/ocp .pdf . 62 Команды switch 63 Но, пожалуй, самый серьезный недостаток заключается в том, что программа может содержать неограниченное количество других функций с аналогичной структурой, например: isPayday(Employee e, Date date) или deliverPay(Employee e, Money pay) и так далее . Все эти функции будут иметь все ту же ущербную структуру . Решение проблемы (листинг 3 .5) заключается в том, чтобы похоронить команду switch в фундаменте АБСТРАКТНОЙ ФАБРИКИ [GOF] и никому ее не пока- зывать . Фабрика использует команду switch для создания соответствующих эк- земпляров потомков Employee , а вызовы функций calculatePay , isPayDay , deliverPay и т . д . проходят полиморфную передачу через интерфейс Employee листинг 3 .5 . Employee и Factory public abstract class Employee { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } ----------------- public interface EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; } ----------------- public class EmployeeFactoryImpl implements EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType { switch (r.type) { case COMMISSIONED: return new CommissionedEmployee(r) ; case HOURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmploye(r); default: throw new InvalidEmployeeType(r.type); } } } Мое общее правило в отношении команд switch гласит, что эти команды допусти- мы, если они встречаются в программе однократно, используются для создания полиморфных объектов и скрываются за отношениями наследования, чтобы оставаться невидимыми для остальных частей системы [G23] . Конечно, правил без исключений не бывает и в некоторых ситуациях приходится нарушать одно или несколько условий этого правила . 63 64 Глава 3 . Функции Используйте содержательные имена В листинге 3 .7 я переименовал нашу функцию testableHtml в SetupTeardownIncluder. render . Новое имя гораздо лучше, потому что оно точнее описывает, что дела- ет функция . Кроме того, всем приватным методам были присвоены столь же содержательные имена isTestable , includeSetupAndTeardownPages и т . д . Трудно пере оценить пользу хороших имен . Вспомните принцип Уорда: «Вы работае- те с чистым кодом, если каждая функция в основном делает то, что вы от нее ожидали» . Половина усилий по реализации этого принципа сводится к выбору хороших имен для компактных функций, выполняющих одну операцию . Чем меньше и специализированнее функция, тем проще выбрать для нее содержа- тельное имя . Не бойтесь использовать длинные имена . Длинное содержательное имя лучше ко- роткого невразумительного . Выберите схему, которая позволяет легко прочитать слова в имени функции, а затем составьте из этих слов имя, которое описывает назначение функции . Не бойтесь расходовать время на выбор имени . Опробуйте несколько разных имен и посмотрите, как читается код с каждым из вариантов . В современных рабочих средах (таких, как Eclipse и IntelliJ) задача смены имени решается тривиально . Используйте одну из этих сред и поэкспериментируйте с разными именами, пока не найдете самое содержательное . Выбор содержательных имен прояснит архитектуру модуля и поможет вам усовершенствовать ее . Нередко поиски хороших имен приводят к полезной ре- структуризации кода . Будьте последовательны в выборе имен . Используйте в именах функций те же словосочетания, глаголы и существительные, которые используются в ва- ших модулях . Для примера можно взять имена includeSetupAndTeardownPages , includeSetupPages , includeSuiteSetupPage и includeSetupPage . Благодаря единой фразеологии эти имена рассказывают связную историю . В самом деле, если бы я показал вам только эту последовательность, вы бы спросили: «А где же includeTeardownPages , includeSuiteTeardownPage и includeTeardownPage ?» Вспомни- те — «…в основном делает то, что вы от нее ожидали» . Аргументы функций В идеальном случае количество аргументов функции равно нулю (нуль-арная функция) . Далее следуют функции с одним аргументом (унарные) и с двумя аргументами (бинарные) . Функций с тремя аргументами (тернарных) следует по возможности избегать . Необходимость функций с большим количеством аргу- ментов (полиарных) должна быть подкреплена очень вескими доводами — и все равно такие функции лучше не использовать . 64 Аргументы функций 65 Аргументы усложняют функции и лишают их значительной части концептуальной мощи . Имен- но по этой причине я почти полностью избавился от них в этом примере . Возьмем хотя бы пере- менную StringBuffer . Ее можно было бы пере- дать в аргументе (вместо того, чтобы делать ее переменной экземпляра), но тогда читателям кода пришлось бы интерпретировать ее каждый раз, когда она встречается в коде . Когда вы читаете историю, рассказываемую модулем, вызов in- cludeSetupPage() выглядит намного более понят- ным, чем вызов includeSetupPageInto(newPageCont ent) . Аргумент и имя функции находятся на раз- ных уровнях абстракции, а читателю приходится помнить о подробностях (то есть StringBuffer ), которые на данный момент не особенно важны . Аргументы создают еще больше проблем с точки зрения тестирования . Только представьте, как трудно составить все тестовые сценарии, проверяющие правиль- ность работы кода со всеми комбинациями аргументов . Если аргументов нет — задача тривиальна . При одном аргументе все обходится без особых сложностей . С двумя аргументами ситуация усложняется . Если же аргументов больше двух, за- дача тестирования всех возможных комбинаций выглядит все более устрашающе . Выходные аргументы запутывают ситуацию еще быстрее, чем входные . Читая код функции, мы обычно предполагаем, что функция получает информацию в аргументах, и выдает ее в возвращаемом значении . Как правило, никто не ожи- дает, что функция будет возвращать информацию в аргументах . Таким образом, выходные аргументы часто заставляют нас браться за чтение функции заново . Если уж обойтись без аргументов никак не удается, постарайтесь хотя бы огра- ничиться одним входным аргументом . Смысл вызова SetupTeardownIncluder. render(pageData) вполне прозрачен — понятно, что мы собираемся сгенерировать данные для объекта pageData Стандартные унарные формы Существует два очень распространенных случая вызова функции с одним ар- гументом . Первая — проверка некоторого условия, связанного с аргументом, как в вызове boolean fileExists("MyFile") . Вторая — обработка аргумента, его преобразование и возвращение . Например, вызов InputStream fileOpen("MyFile") преобразует имя файла в формате String в возвращаемое значение InputStream Выбирайте имена, которые четко отражают различия, и всегда используйте две формы в логически непротиворечивом контексте . (См . далее «Разделение команд и запросов») . 65 66 Глава 3 . Функции Несколько менее распространенным, но все равно очень полезным частным случаем функции с одним аргументом является событие . В этой форме имеется входной аргумент, а выходного аргумента нет . Предполагается, что программа интерпретирует вызов функции как событие и использует аргумент для измене- ния состояния системы, например, void passwordAttemptFailedNtimes(int attempts) Будьте внимательны при использовании данной формы . Читателю должно быть предельно ясно, что перед ним именно событие . Тщательно выбирайте имена и контексты . Старайтесь избегать унарных функций, не относящихся к этим формам, напри- мер void includeSetupPageInto(StringBuffer pageText) . Преобразования, в которых вместо возвращаемого значения используется выходной аргумент, сбивают чи- тателя с толку . Если функция преобразует свой входной аргумент, то результат должен передаваться в возвращаемом значении . В самом деле, вызов StringBuffer transform(StringBuffer in) лучше вызова void transform(StringBuffer out) , даже если реализация в первом случае просто возвращает входной аргумент . По край- ней мере она соответствует основной форме преобразования . Аргументы-флаги Аргументы-флаги уродливы . Передача логического значения функции — воис- тину ужасная привычка . Она немедленно усложняет сигнатуру метода, громко провозглашая, что функция выполняет более одной операции . При истинном значении флага выполняется одна операция, а при ложном — другая! В листинге 3 .7 у нас нет выбора, потому что вызывающая сторона уже передает этот флаг, а я хотел ограничить область переработки границами функции . Тем не менее вызов метода render(true) откровенно сбивает с толку бедного читателя . Если навести указатель мыши на вызов и увидеть render(boolean isSuite) , ситуа- ция слегка проясняется, но ненамного . Эту функцию следовало бы разбить на две: renderForSuite() и renderForSingleTest() Бинарные функции Функцию с двумя аргументами понять сложнее, чем унарную функцию . Напри- мер, вызов writeField(name) выглядит более доступно, чем writeField(outputStream, name) 1 . Хотя смысл обеих форм понятен, первая форма просто проскальзывает под нашим взглядом, моментально раскрывая свой смысл . Во второй форме приходится сделать непродолжительную паузу, пока вы не поймете, что первый 1 Я только что завершил переработку модуля, использовавшего бинарную форму . Мне уда- лось преобразовать outputStream в поле класса и привести все вызовы writeField к унарной форме . Результат получился гораздо более наглядным . 66 Аргументы функций 67 параметр должен игнорироваться . И конечно, это в конечном итоге создает проблемы, потому что никакие части кода игнорироваться не должны . Именно в проигнорированных частях чаще всего скрываются ошибки . Конечно, в некоторых ситуациях форма с двумя аргументами оказывается уместной . Например, вызов Point p = new Point(0,0); абсолютно разумен . Точка в декартовом пространстве естественным образом создается с двумя аргумен- тами . В самом деле, вызов new Point(0) выглядел бы довольно странно . Однако два аргумента в нашем случае являются упорядоченными компонентами одного значения! Напротив, outputStream и name не имеют ни естественной связи, ни естественного порядка . Даже с очевидными бинарными функциями вида assertEquals(expected, actual) возникают проблемы . Сколько раз вы помещали actual туда, где должен был на- ходиться аргумент expected ? Эти два аргумента не имеют естественного порядка . Последовательность expected, actual — не более чем условное правило, которое запоминается не сразу . Бинарные функции не являются абсолютным злом, и вам почти наверняка придется писать их . Тем не менее следует помнить, что за их использование приходится расплачиваться, а вам стоит воспользоваться всеми доступными средствами для их преобразования в унарные . Например, можно сделать метод writeField членом класса outStream , чтобы использовать запись outputStream. writeField(name) . Другой вариант — преобразование outputStream в поле текущего класса, чтобы переменную не приходилось передавать при вызове . Также можно создать новый класс FieldWriter , который получает outputStream в конструкторе и содержит метод write тернарные функции Разобраться в функции с тремя аргументами значительно сложнее, чем в бинар- ной функции . Проблемы соблюдения порядка аргументов, приостановки чтения и игнорирования увеличиваются более чем вдвое . Я рекомендую очень хорошо подумать, прежде чем создавать тернарную функцию . Для примера возьмем стандартную перегруженную версию assertEquals с тремя аргументами: assertEquals(message, expected, actual) . Сколько раз вы читали зна- чение message и думали, что перед вами expected ? Я сталкивался с этой конкретной тернарной функцией и задерживался на ней много раз . Более того, каждый раз, когда я ее вижу, мне приходится делать новый заход и вспоминать о необходи- мости игнорировать message С другой стороны, следующая тернарная функция не столь коварна: assert- Equals(1.0, amount, .001) . Хотя и она не воспринимается с первого раза, в данном случае эта трудность оправдана . Всегда полезно лишний раз вспомнить, что равенство вещественных значений — понятие относительное . 67 68 Глава 3 . Функции Объекты как аргументы Если функция должна получать более двух или трех аргументов, весьма вероят- но, что некоторые из этих аргументов стоит упаковать в отдельном классе . Рас- смотрим следующие два объявления: Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius); Сокращение количества аргументов посредством создания объектов может по- казаться жульничеством, но это не так . Если переменные передаются совместно как единое целое (как переменные x и y в этом примере), то, скорее всего, вместе они образуют концепцию, заслуживающую собственного имени . Списки аргументов Иногда функция должна получать переменное количество аргументов . Для при- мера возьмем метод String.format : String.format( " %s worked %.2f hours. " , name, hours); Если все переменные аргументы считаются равноправными, как в этом примере, то их совокупность эквивалентна одному аргументу типа List . По этой причине функция String.format фактически является бинарной . И действительно, следую- щее объявление String.format подтверждает это: public String format(String format, Object... args) Следовательно, в данном случае действуют уже знакомые правила . Функции с переменным списком аргументов могут быть унарными, бинарными и даже тернарными, но использовать большее количество аргументов было бы ошибкой . void monad(Integer... args); void dyad(String name, Integer... args); void triad(String name, int count, Integer... args); Глаголы и ключевые слова Выбор хорошего имени для функции способен в значительной мере объяснить смысл функции, а также порядок и смысл ее аргументов . В унарных функциях сама функция и ее аргумент должны образовывать естественную пару «глагол/ существительное» . Например, вызов вида write(name) смотрится весьма инфор- мативно . Читатель понимает, что чем бы ни было «имя» ( name ), оно куда-то «за- писывается» ( write ) . Еще лучше запись writeField(name) , которая сообщает, что «имя» записывается в «поле» какой-то структуры . Последняя запись является примером использования ключевых слов в имени функции . В этой форме имена аргументов кодируются в имени функции . На- пример, assertEquals можно записать в виде assertExpectedEqualsActual(expect ed, actual) . Это в значительной мере решает проблему запоминания порядка аргументов . 68 Избавьтесь от побочных эффектов 69 Избавьтесь от побочных эффектов Побочные эффекты суть ложь . Ваша функция обещает делать что-то одно, но де- лает что-то другое, скрытое от пользователя . Иногда она вносит неожиданные из- менения в переменные своего класса — скажем, присваивает им значения параме- тров, переданных функции, или глобальных переменных системы . В любом случае такая функция является коварной и вредоносной ложью, которая часто приводит к созданию противоестественных временных привязок и других зависимостей . Для примера возьмем безвредную на первый взгляд функцию из листинга 3 .6 . Функция использует стандартный алгоритм для проверки пары «имя пользова- теля/пароль» . Она возвращает true в случае совпадения или false при возник- новении проблем . Но у функции также имеется побочный эффект . Сможете ли вы обнаружить его? листинг 3 .6 . UserValidator.java public class UserValidator { private Cryptographer cryptographer; public boolean checkPassword(String userName, String password) { User user = UserGateway.findByName(userName); if (user != User.NULL) { String codedPhrase = user.getPhraseEncodedByPassword(); String phrase = cryptographer.decrypt(codedPhrase, password); if ( " Valid Password " .equals(phrase)) { Session.initialize(); return true; } } return false; } } Разумеется, побочным эффектом является вызов Session.initialize() . Имя checkPassword сообщает, что функция проверяет пароль . Оно ничего не говорит о том, что функция инициализирует сеанс . Таким образом, тот, кто поверит имени функции, рискует потерять текущие сеансовые данные, когда он решит проверить данные пользователя . Побочный эффект создает временную привязку . А именно, функция checkPassword может вызываться только в определенные моменты времени (когда инициали- зация сеанса может быть выполнена безопасно) . Несвоевременный вызов может привести к непреднамеренной потере сеансовых данных . Временные привязки создают массу проблем, особенно когда они прячутся в побочных эффектах . Если без временной привязки не обойтись, этот факт должен быть четко оговорен в имени функции . В нашем примере функцию можно было бы переименовать в checkPasswordAndInitializeSession , хотя это безусловно нарушает правило «одной операции» . 69 |