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

  • J2: не наследуйте от констант

  • J3: Константы против перечислений

  • Имена n1: Используйте содержательные имена

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


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница38 из 49
    1   ...   34   35   36   37   38   39   40   41   ...   49
    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 g.roll(pins);
    }
    Смысл переменной i
    абсолютно очевиден . Какое-нибудь раздражающее имя вида rollCount только затемнило бы смысл этого тривиального кода . С другой стороны, смысл коротких имен переменных и функций рассеивается на длинных дистанциях . Таким образом, чем длиннее область видимости имени, тем более длинным и точным должно быть ее имя .
    n6: Избегайте кодирования
    Информация о типе или области видимости не должна кодироваться в именах .
    Префиксы вида m_
    или f
    бессмысленны в современных средах . Кроме того, ин- формация о проекте и/или подсистеме (например, префикс vis_
    для подсистемы визуализации) также отвлекает читателя и является избыточной . Современные среды разработки позволяют получить всю необходимую информацию без уродования имен . Поддерживайте чистоту в своих именах, не загрязняйте их венгерской записью .
    353

    1   ...   34   35   36   37   38   39   40   41   ...   49


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