Чистый код. Создание, анализ и рефакторинг — страница 62 из 94

J1: Используйте обобщенные директивы импорта

Если вы используете два и более класса из пакета, импортируйте весь пакет командой

import package.*;

Длинные списки импорта пугают читателя кода. Начало модуля не должно загромождаться 80-строчным списком директив импорта. Список импорта должен быть точной и лаконичной конструкцией, показывающей, с какими пакетами мы собираемся работать.

Конкретные директивы импорта определяют жесткие зависимости, обобщенные директивы импорта — нет. Если вы импортируете конкретный класс, то этот класс обязательно должен существовать. Но пакет, импортируемый обобщенной директивой, может не содержать ни одного класса. Директива импорта просто добавляет пакет в путь поиска имен. Таким образом, обобщенные директивы импорта не создают реальных зависимостей, а следовательно, способствуют смягчению логических привязок между модулями.

В некоторых ситуациях длинные списки конкретных директив импорта бывают полезными. Например, если вы работаете с унаследованным кодом и хотите узнать, для каких классов необходимо создать заглушки и имитации, можно пройтись по конкретным спискам импорта, узнать полные имена классов и написать для них соответствующие заглушки. Однако такое использование конкретных директив импорта встречается крайне редко. Более того, многие современные IDE позволяют преобразовать обобщенный список импорта в список конкретных директив одной командой. Таким образом, даже в унаследованном коде лучше применять обобщенный импорт.

Обобщенные директивы импорта иногда становятся причиной конфликтов имен и неоднозначностей. Два класса с одинаковыми именами, находящиеся в разных пакетах, должны импортироваться конкретными директивами (или по крайней мере их имена должны уточняться при использовании). Это создает определенные неудобства, однако ситуация встречается достаточно редко, так что в общем случае обобщенные директивы импорта все равно лучше конкретных.

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

Я уже неоднократно встречался с этим явлением, и каждый раз оно заставляло меня недовольно поморщиться. Программист размещает константы в интерфейсе, а затем наследует от этого интерфейса для получения доступа к константам. Взгляните на следующий код:

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)

    );

  }

  ...

}

Где определяются константы 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 может потеряться; смысл перечислений потеряться не может, потому что они принадлежат указанному перечислению.

Тщательно изучите синтаксис перечислений. Не забудьте, что перечисления могут содержать методы и поля. Это очень мощные синтаксические инструменты, значительно превосходящие 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: Используйте содержательные имена

Не торопитесь с выбором имен. Позаботьтесь о том, чтобы имена были содержательными. Помните, что смысл может изменяться в ходе развития программного продукта; почаще переосмысливайте уместность выбранных вами имен.

Не рассматривайте это как дополнительный «фактор комфортности». Имена в программных продуктах на 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);

      frame += 2;

    }

  }

  return score;

}

Сила хорошо выбранных имен заключается в том, что они дополняют структуру кода описаниями. На основании этих описаний у читателя формируются определенные предположения по поводу того, что делают другие функции модуля. Взглянув на приведенный код, вы сможете представить себе примерную реализацию isStrike(). А при чтении метода isStrike() становится очевидно, что он делает «примерно то, что предполагалось»[76].

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. Разумеется, концепция телефонных номеров в таких случаях относится к неверному уровню абстракции. Более правильная стратегия выбора имен в таких сценариях может выглядеть так:

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) называет их всеобщим языком проекта[77]. Широко используйте термины этого языка в своем коде. Чем больше вы используете имена, переопределенные специальным смыслом, относящимся к вашему конкретному проекту, тем проще читателю понять, о чем идет речь в вашем коде.

N4: Недвусмысленные имена

Выбирайте имена, которые максимально недвусмысленно передают назначение функции или переменной. Рассмотрим пример из FitNesse:

private String doRename() throws Exception

{

  if(refactorReferences)

    renameReferences();

  renamePage();

  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_ для подсистемы визуализации) также отвлекает читателя и является избыточной. Современные среды разработки позволяют получить всю необходимую информацию без уродования имен. Поддерживайте чистоту в своих именах, не загрязняйте их венгерской записью.

N7: Имена должны описывать побочные эффекты

Имена должны описывать все, что делает функция, переменная или класс. Не скрывайте побочные эффекты за именами. Не используйте простые глаголы для описания функции, которая делает что-то помимо этой простой операции. Для примера возьмем следующий код из TestNG:

public ObjectOutputStream getOos() throws IOException {

  if (m_oos == null) {

    m_oos = new ObjectOutputStream(m_socket.getOutputStream());

  }

  return m_oos;

}

Функция не ограничивается простым получением m_oos; она создает объект m_oos, если он не был создан ранее. Таким образом, эту функцию было бы правильнее назвать createOrReturnOos.

Тесты