«Эффективная работа с легаси‑кодом» Майкла Физерса

Приёмы из этой книги мне помогают в работе, поэтому я решил составить по ней конспект. Примеры в ней написаны на Джаве и С++, поэтому не всё получилось перевести на Джаваскрипт. Я постарался вытянуть наиболее важное, но советую прочесть книгу и самим. Теперь к делу.

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

Глава 1. Изменение софта

Менять код нужно, чтобы:

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

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

// до изменения
class Player {
  addPlaylist(name, tracks) {
    ...
  }
}

// после
class Player {
  addPlaylist(name, tracks) {
    ...
  }

  deletePlaylist(name) {
    ...
  }
}

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

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

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

Сохранить поведение неизменным трудно. Каждое изменение кода несёт риск изменения поведения. Чтобы смягчить риск, перед изменениями задавать себе три вопроса:

  • что надо поменять?
  • как узнать, что мы внесли изменения правильно?
  • как узнать, что мы не сломали остальное?

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

Глава 2. Работать на обратную связь

Вносить изменения в код можно двумя путями: «запушить и молиться» и «покрыть тестами и менять». Смысл второго подхода в том, чтобы получать обратную связь постоянно и как можно быстрее. Тесты дают обратную связь, говоря, как наши изменения ломают поведение программы.

Хороший юнит‑тест:

  • быстрый;
  • помогает найти проблему быстро;
  • не имеет внешних зависимостей.

Рефакторинг легаси‑кода следует начинать с покрытия его тестами. Проблема в том, что часто у легаси куча зависимостей, из‑за которых его трудно тестировать. Возникает дилемма: чтобы изменить код, надо иметь тесты к нему, а чтобы написать к нему тесты, его надо поменять.

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

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

Глава 3. Распознавание и разделение

Для тестирования нужно разрывать зависимости в коде по двум причинам:

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

Класс NetworkBridge получает список узлов, каждый из которых открывает сетевое соединение и общается с другими узлами:

class NetworkBridge {
  constructor(endpoints) {
    ...
  }

  formRouting(sourceId, destId) { 
    ...
  }
}

Как его тестировать? Если он связан с железом, можем ли мы себе позволить на каждый тест нагружать оборудование? Можем ли создать тестовый кластер? Есть ли на это ресурсы и время? Такие проблемы возникают, когда мы не понимаем, как выделить нужную часть и тестировать её изолированно. Здесь могут помочь фиктивные объекты.

Фиктивные объекты олицетворяют какой‑либо класс во время тестирования. Например, у нас есть класс Sale, который сканирует штрих‑коды, и выводит сообщения на экран устройства через класс Display:

class Sale {
  constructor(display) {
    this._display = display
  }

  scan(barcode) {
    // сканирует
    ...
    // выводит сообщение
    this._display.showMessage('hello world')
  }
}

class Display {
  showMessage(msg) {
    ...
  }
}

const display = new Display()
const sale = new Sale(display)

Чтобы не зависеть от конкретного оборудования, мы можем написать поддельный класс FakeDisplay:

class FakeDisplay {
  // вместо вывода на экран, будем запоминать сообщение
  // это метод, который имитирует настоящий метод класса Display
  showMessage(msg) {
    this.lastLine = msg
  }

  // и потом выводить его по требованию
  // это доп. метод, который нужен именно в тестах
  getLastLine() {
    return this.lastLine
  }
}

В тесте мы можем подменить класс, работающий с конкретным оборудованием на поддельный:

it('Выводит название товара на экран', () => {
  const fakeDisplay = new FakeDisplay()
  const saleTest = new Sale(fakeDisplay)

  saleTest.scan('1')

  expect(fakeDisplay.getLastLine).toEqual('Молоко')
})

Этот тест не упадёт, если не работает какая‑то часть в настоящем классе Display. Но мы тестируем класс Sale, а не Display, поэтому конкретно в этом тесте это не важно.

Глава 4. Швы

Шов — место в программе, где можно изменить её поведение, без редактирования кода в этом месте. Рабочий код должен быть одинаков и в продакшене и в тестах. Швы помогают разорвать зависимости и оттестировать код, без его изменения.

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

Удобнее всего в объектно‑ориентированных языках использовать объектные швы, когда действие какого‑то метода подменяется на другое. Например, при создании экземпляра класса в конструкторе.

Глава 5. Инструменты автоматизированного рефакторинга

Рефакторинг — упрощение или улучшение дизайна кода без изменения поведения. Есть среды разработки, которые предлагают автоматические инструменты для рефакторинга. Они помогают переименовывать переменные, удалять лишнее, выносить код в отдельные функции или классы.

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

// класс до рефакторинга
class Example {
  alpha = 0

  getValue() {
    this.alpha++
    return 42
  }

  doSomething() {
    let total = 0
    const val = this.getValue()
    for (let i = 0; i < 5; i++) {
      total += val
    }
  }
}

// после
class Example {
  alpha = 0

  getValue() {
    this.alpha++
    return 42
  }

  doSomething() {
    let total = 0
    for (let i = 0; i < 5; i++) {
      total += this.getValue()
    }
  }
}

Лишняя переменная исчезла, но вместе с этим alpha++ вызвалось 5 раз вместо 1. Юнит‑тесты помогут выявить это изменение.

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

Поговорим об изменении кода, когда не хватает времени, добавлении фич, ТДД и зависимостях.

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

← «Эффективная работа с легаси‑кодом», часть 2 «Тонкое искусство пофигизма» Марка Мэнсона →