Эффективная работа с легаси-кодом. Майкл Физерс
Приёмы из этой книги мне помогают в работе, поэтому я решил составить по ней конспект. Примеры в ней написаны на Джаве и С++, поэтому не всё получилось перевести на JS. Я постарался вытянуть наиболее важное, но советую прочесть книгу и самим. Теперь к делу.
Физерс определяет легаси-код, как код без тестов. В таком коде невозможно предсказать, сделает ли какое-то изменение этот код лучше или хуже.
Глава 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. Юнит-тесты помогут выявить это изменение.
В следующий раз
Поговорим об изменении кода, когда не хватает времени, добавлении фич, TDD и зависимостях.