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

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

Если в некоторой системе нас прежде всего интересует гибкость в добавлении новых типов данных, то в этой части системы предпочтение отдается объектной реализации. В других случаях нам нужна гибкость расширения поведения, и тогда в этой части используются типы данных и процедуры. Хороший программист относится к этой проблеме без предубеждения и выбирает то решение, которое лучше всего подходит для конкретной ситуации.

Литература

[Refactoring]: Refactoring: Improving the Design of Existing Code, Martin Fowler et al., Addison-Wesley, 1999.

Глава 7. Обработка ошибок

Майкл Физерс

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

Однако связь этих двух тем — обработки ошибок и чистого кода — очевидна. Во многих кодовых базах обработка ошибок выходит на первый план. Я вовсе не хочу сказать, что код не делает ничего полезного, кроме обработки ошибок; я имею в виду, что из-за разбросанной повсюду обработки ошибок практически невозможно понять, что же делает код. Обработка ошибок важна, но если они заслоняют собой логику программы — значит, она реализована неверно.

В этой главе представлены некоторые соображения и приемы, которые помогают писать чистый и надежный код, то есть код, в котором ошибки обрабатываются стильно и элегантно.

Используйте исключения вместо кодов ошибок

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


Листинг 7.1. DeviceController.java

public class DeviceController {

  ...

  public void sendShutDown() {

    DeviceHandle handle = getHandle(DEV1);

    // Проверить состояние устройства

    if (handle != DeviceHandle.INVALID) {

      // Сохранить состояние устройства в поле записи

      retrieveDeviceRecord(handle);

      // Если устройство не приостановлено, отключить его

      if (record.getStatus() != DEVICE_SUSPENDED) {

        pauseDevice(handle);

        clearDeviceWorkQueue(handle);

        closeDevice(handle);

      } else {

        logger.log("Device suspended.  Unable to shut down");

      }

    } else {

      logger.log("Invalid handle for: " + DEV1.toString());

    }

  }

  ...

}

У обоих решений имеется общий недостаток: они загромождают код на стороне вызова. Вызывающая сторона должна проверять ошибки немедленно после вызова. К сожалению, об этом легко забыть. По этой причине при обнаружении ошибки лучше инициировать исключение. Код вызова становится более понятным, а его логика не скрывается за кодом обработки ошибок.

В листинге 7.2 представлен тот же код с выдачей исключений в методах, способных обнаруживать ошибки.

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


Листинг 7.2. DeviceController.java (с исключениями)

public class DeviceController {

  ...


  public void sendShutDown() {

    try {

      tryToShutDown();

    } catch (DeviceShutDownError e) {

      logger.log(e);

    }

  }


  private void tryToShutDown() throws DeviceShutDownError {

    DeviceHandle handle = getHandle(DEV1);

    DeviceRecord record = retrieveDeviceRecord(handle);


    pauseDevice(handle);

    clearDeviceWorkQueue(handle);

    closeDevice(handle);

  }


  private DeviceHandle getHandle(DeviceID id) {

    ...

    throw new DeviceShutDownError("Invalid handle for: " + id.toString());

    ...

  }


  ...

}

Начните с написания команды  try-catch-finally

У исключений есть одна интересная особенность: они определяют область видимости в вашей программе. Размещая код в секции try команды try-catch-finally, вы утверждаете, что выполнение программы может прерваться в любой точке, а затем продолжиться в секции catch.

Блоки try в каком-то отношении напоминают транзакции. Секция catch должна оставить программу в целостном состоянии, что бы и произошло в секции try. По этой причине написание кода, который может инициировать исключения, рекомендуется начинать с конструкции try-catch-finally. Это поможет вам определить, чего должен ожидать пользователь кода, что бы ни произошло в коде try.

Допустим, требуется написать код, который открывает файл и читает из него сериализованные объекты.

Начнем с модульного теста, который проверяет, что при неудачном обращении к файлу будет выдано исключение:

@Test(expected = StorageException.class)

public void retrieveSectionShouldThrowOnInvalidFileName() {

  sectionStore.retrieveSection("invalid - file");

}

Для теста необходимо создать следующую программную заглушку:

public List retrieveSection(String sectionName) {

  // Пусто, пока не появится реальная реализация

  return new ArrayList();

}

Тест завершается неудачей, потому что код не инициирует исключения. Затем мы изменяем свою реализацию так, чтобы она попыталась обратиться к несуществующему файлу. При попытке выполнения происходит исключение:

public List retrieveSection(String sectionName) {

  try {

    FileInputStream stream = new FileInputStream(sectionName)

  } catch (Exception e) {

    throw new StorageException("retrieval error", e);

  }

  return new ArrayList();

}

Теперь тест проходит успешно, потому что мы перехватили исключение. На этой стадии можно переработать код. Тип перехватываемого исключения сужается до типа, реально инициируемого конструктором FileInputStream, то есть FileNotFoundException:

public List retrieveSection(String sectionName) {

  try {

    FileInputStream stream = new FileInputStream(sectionName);

    stream.close();

  } catch (FileNotFoundException e) {

    throw new StorageException("retrieval error", e);

  }

  return new ArrayList();

}

Определив область видимости при помощи структуры try-catch, мы можем использовать методологию TDD для построения остальной необходимой логики. Эта логи