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

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

Глава 16. Я не понимаю этот код. Как мне его менять

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

Глава 17. У приложения нет структуры. Что делать

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

При работе с легаси может помочь приём «описание системы». Задайте себе вопрос «что из себя представляет структура системы?» и отвечайте на него так, как будто вам заранее ничего о ней не известно. Когда вы начнёте себе объяснять, как система функционирует, вы будете упрощать логику её работы. Это поможет выделить по-настоящему важные элементы и построить идеальную схему взаимодействий внутри системы.

Глава 18. Тестовый код мне мешает

Помечайте тестовые классы или функции суффиксами .test, .spec, фиктивные объекты — .fake, .mock, .stub. Тесты также можно вынести в отдельную папку в проекте. Удобно когда структура файлов тестов повторяет структуру файлов проекта.

Глава 19. У меня не ООП. Как безопасно рефакторить код

Дилемма легаси: чтобы изменить код, надо его покрыть тестами, а чтобы покрыть тестами, его надо изменить. Эта дилемма справедлива не только для ООП. Решения те же: разрыв зависимостей, фиктивные объекты, TDD.

Глава 20. Класс уже огромный. Не хочу, чтобы он стал больше

Если в классе 50 методов, то приходится долго вникать в его работу перед тем, как внести какие-то изменения. Не увеличивать размер класса помогут почкование метода или класса, но это временное решение. Настоящее решение — рефакторинг.

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

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

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

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

Глава 21. Я изменяю одинаковый код снова и снова

Один и тот же повторяющийся код можно переструктурировать по-разному:

const func = () => {
  a(); a(); b(); a(); b(); b();
}

// может превратиться как в
const func = () => {
  aa(); b(); a(); bb();
}

// так и в
const func = () => {
  a(); ab(); ab(); b();
}

Начните с малого и двигайтесь с оглядкой на главную цель класса или метода. По мере продвижения вам будет становиться яснее, как поступить правильнее. Когда два метода или класса выглядят почти одинаково, вынесите разницу в другие методы или классы, а прежние объедините:

class AddEmployeeCmd extends Command {
	constructor() {
		this.name = '';
		this.address = '';

		this.header = ['some', 'sophisticated', 'data', 'structure'];
		this.commandCharIndex = 42;
		this.footer = ['some', 'sophisticated', 'data', 'structure'];
	}
}

class LoginCommand extends Command {
	constructor() {
		this.userName = '';
		this.password = '';

		this.header = ['some', 'sophisticated', 'data', 'structure'];
		this.commandCharIndex = 44;
		this.footer = ['some', 'sophisticated', 'data', 'structure'];
	}
}

// мы можем вынести header и footer прямо в Command,
// потому что они одинаковые

class Command {
	constructor() {
		this.header = ['some', 'sophisticated', 'data', 'structure'];
		this.footer = ['some', 'sophisticated', 'data', 'structure'];
		// ...
	}
}

class AddEmployeeCmd extends Command {
	constructor() {
		this.name = '';
		this.address = '';
	}
	// ...
}

class LoginCommand extends Command {
	constructor() {
		this.userName = '';
		this.password = '';
	}
	// ...
}

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

Глава 22. Мне надо изменить гигантский метод, и я не могу написать тесты к нему

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

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

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

// этот класс строит ДОМ-дерево,
// мы хотим отрефакторить условие добавления узла в корень дерева
// но не знаем, в какой момент это происходит

class DomBuilder {
  processNode(node) {
    // ...
    if (node.type() === 'TYPE1'
        || node.type() === 'TYPE2'
        || node.type() === 'TYPE3' && node.isVisible()) {
      this.root.appendChild(node)
    }
  }
}

// вводим переменную, которая поможет выявить этот момент

class DomBuilder {
  constructor() {
    this._nodeAdded = false
  }

  processNode(node) {
    // ...запутанная логика
    if (this.isBaseChild(node)) {
      this.root.appendChild(node)
      this._nodeAdded = true
    }
  }

  isBaseChild(node) {
    return node.type() === 'TYPE1'
        || node.type() === 'TYPE2'
        || node.type() === 'TYPE3' && node.isVisible()
  }
}

// тест на случай, когда узел должен был добавиться
// и на случай, когда не должен был

it('tests if node is base child', () => {
  const node = new Node('TYPE1')
  const builder = new DomBuilder()

  builder.processNode(node)

  expect(builder._nodeAdded).toEqual(true)
})

it('tests if node is not base child', () => {
  const node = new Node('TYPE5')
  const builder = new DomBuilder()

  builder.processNode(node)

  expect(builder._nodeAdded).toEqual(false)
})

После завершения рефакторинга и тестирования переменную распознавания можно удалить.

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

Глава 23. Как мне узнать, что я ничего не сломал

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

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

Что дальше

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