Эффективная работа с легаси‑кодом. Часть 2

В первой части мы рассмотрели введение, причины изменения кода, швы и инструменты автоматизированного рефакторинга. Сегодня говорим об изменении кода, когда не хватает времени, добавлении фич, ТДД и зависимостях.

Глава 6. У меня нет времени, но мне нужно что‑то поменять

Рефакторинг и тесты — это дополнительная работа. Но в будущем она упростит внесение изменений в код и поможет быстрее отлавливать баги.

Почкование метода (погуглил, в переводе книги это называется именно так, прим. автора статьи). Если надо добавить фичу, которую можно выразить в новом коде, то лучше написать этот код в новом методе, и вызывать новый метод. Новый метод можно будет покрыть тестами.

class UsersList {
  // метод отправляет уведомления об акции 
  // указанным пользователям
  notifyOfAction(users) {
    const message = 'some message to send'
    users.forEach(user => {
      this.sendEmail(user, message)
    })
  }

  // ...
}

Если мы хотим, чтобы уведомление отправлялось только совершеннолетним, то можно профильтровать список пользователей прямо в методе:

class UsersList {
  notifyOfAction(users) {
    const message = 'some message to send'
    users = users.filter(user => user.age >= 18)
    users.forEach(user => {
      this.sendEmail(user, message)
    })
  }

  // ...
}

Но таким образом мы не поймём, где заканчивается старый код, где начинается новый, и как это тестировать. Лучше вынести фильтрацию в новый метод:

class UsersList {
  notifyOfAction(users) {
    const message = 'some message to send'
    users = this.filterInAdults(users)
    users.forEach(user => {
      this.sendEmail(user, message)
    })
  }

  filterInAdults(users) {
    return users.filter(user => user.age >= 18)
  }

  // ...
}

Теперь новый метод filterInAdults можно покрыть тестами.

Алгоритм метода:

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

Минусы:

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

Плюсы:

  • новый код полностью отделён от старого;
  • возможно протестировать;
  • можно наблюдать за всеми переменными, которые меняют поведение метода.

Почкование можно применить для целого класса, а не только метода. Например, если нужно поменять формат вывода результата со строки на ХТМЛ.

Оборачивание метода (в переводе охват, прим. автора статьи). Ещё один способ добавить поведение к уже существующему коду. Метод pay класса Employee вызывает paymentDispatcher, чтобы выплатить деньги сотруднику.

class Employee { 
  pay(amount) {
    const date = // ...
    paymentDispatcher(date, amount)
  }
}

Допустим, мы хотим логировать каждую оплату. Можно добавить логирование напрямую в метод pay, но можно создать новый метод с такой же сигнатурой, и вызывать его внутри:

class Employee { 
  // новый метод со старым кодом
  dispatchPayment(amount) {
    const date = // ...
    paymentDispatcher(date, amount)
  }

  // старый метод с вызовом выделенного и добавленным логированием
  pay(amount) {
    this.logPayment(amount)
    this.dispatchPayment(amount)
  }

  // добавочный метод
  logPayment(amount) {
    // ...
  }
}

Алгоритм:

  • определить метод, который надо изменить;
  • создать новый метод с такой же сигнатурой;
  • вызвать новый метод внутри старого;
  • создать метод для нового поведения через ТДД и вызывать его в старом методе.

Плюсы:

  • старые методы не меняются, только лишь переименовываются;
  • новые функции явно не зависимы от других.

Минусы:

  • могут появиться неудачные названия методов.

Можно так же обернуть не только метод, но и класс. Для примера выше можно создать класс LoggingEmployee, который будет наследоваться от Employee, и перегрузить метод pay, добавив нужное новое поведение. По‑другому это ещё называется декоратором.

Глава 7. Изменения до бесконечности

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

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

Глава 8. Как добавить фичу

Алгоритм работы по ТДД:

  • написать отказной тест;
  • написать функцию, чтобы тест перестал падать;
  • убрать дублирование кода;
  • повторить.

На примерах, первый шаг:

// пишем тест, который заведомо упадёт
it('Возвращает сумму двух однозначных чисел', () => {
  expect(sum(1, 1)).toEqual(1 + 1)
})

// функция пока пустая
const sum = (a, b) => {}

Убеждаемся, что тест падает с той причиной, которую мы ожидаем. На втором шаге:

// меняем реализацию функции или метода так, чтобы тест проходил
const sum = (a, b) => a + b

На третьем проверяем, чтобы не было дублирования и рефакторим код метода и тестов. Постепенно описываем примеры прохождения теста и примеры непрохождения. В легаси‑коде тестируемые методы и классы вначале готовят к тестированию, а потом переходят к алгоритму ТДД.

Глава 9. Я не могу протестировать этот класс

Некоторые классы трудно тестировать: дорого создавать объекты этого класса, при создании появляются сайд‑эффекты, куча параллельной работы выполняется. Самый простой способ узнать, сколько проблем доставит создание экземпляра такого класса — создать его. Попробуйте создать объект, отладчик скажет, чего не хватает для создания.

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

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

Если у класса есть скрытые зависимости в конструкторе, подумайте над тем, чтобы сделать их явными. Например, передавать эту зависимость в конструктор извне.

Глава 10. Я не могу выполнить этот метод в средствах тестирования

Иногда методы класса тоже трудно протестировать:

  • он приватный;
  • трудно создать аргументы для вызова этого метода;
  • метод имеет сайд‑эффекты.

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

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

class AccountDetails {
  // ...

  handlePerformedAction(event) {
    const {type} = event
    if (type === 'project activity') {
      const modal = this.createModal()
      const display = this.createElement('input')
      modal.setDescription('modal text')
      modal.show()

      let accountDescription = this.modal.getSymbol()
      accountDescription += ': '
      // ...
      display.value = accountDescription
    }
  }
}

Можно отделить код, который не зависит от интерфейса, от кода, который зависит. Выделим методы обработки команды, чтобы методы стали тестируемыми и независимыми друг от друга:

class AccountDetails {
  // ...

  handlePerformedAction(event) {
    const {type} = event
    this.performCommand(type)
  }

  performCommand(type) { 
    if (type === 'project activity') {
      this.setDescription('modal text')
      this.updateModalValue()
    }
  }

  setDescription(text) {
    const modal = this.createModal()
    modal.setDescription(text)
    modal.show()
  }

  updateModalValue() {
    const display = this.createElement('input')
    let accountDescription = this.modal.getSymbol()
    accountDescription += ': '
    // ...
    display.value = accountDescription
  }
}

Разделение команд и запросов. Метод может быть либо командным, либо запросным, но не тем и не другим одновременно. Командный — который видоизменяет состояние объекта, но не возвращает значение. Запросный возвращает значение, но не видоизменяющий объект.

Глава 11. Надо изменить код, но непонятно, какие методы тестировать

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

Воздействия распространяются в коде тремя основными путями.

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

Алгоритм распознавания воздействий:

  • определить метод, который надо поменять;
  • если метод возвращает значение, проверить места, где этот метод вызывается;
  • если метод меняет какие‑то параметры, проверить места, которые используют эти параметры;
  • проверить суперклассы и подклассы объекта с этим методом;
  • найти глобальные переменные, которые этот метод меняет в ходе работы.

Главы 12–13. Какие тесты писать, чтобы изменить код

Поведение легаси кода стоит принимать за аксиому: раз он так работает, значит, так надо. Поэтому поведение этого кода нужно сохранять. Характеристические тесты — тесты, которые сохраняют поведение.

Алгоритм создания характеристических тестов:

  • начать тестировать небольшой фрагмент кода;
  • написать заведомо отказной тест;
  • выявить поведение кода;
  • поправить тест так, чтобы в нём предполагалось это поведение.

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

Глава 14. Зависимости от библиотек меня убивают

Избегайте беспорядочного расположения прямых вызовов библиотек в своем коде.

В следующий раз

Поговорим о непонятном коде, коде без структуры, огромных классах и ситуациях, когда ничто не помогает.

Ссылки по теме

← Эффективная работа с легаси‑кодом. Часть 3 Эффективная работа с легаси‑кодом. Майкл Физерс →