Главная страница
Навигация по странице:

  • Стандартные унарные формы

  • Глаголы и ключевые слова

  • Создание, анализ ирефакторинг


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница9 из 49
    1   ...   5   6   7   8   9   10   11   12   ...   49
    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

    1   ...   5   6   7   8   9   10   11   12   ...   49


    написать администратору сайта