Уровни абстракции в Clean Code: понятное объяснение для разработчиков
Книга Роберта Мартина «Чистый код» - настольное руководство для программистов, но глава о функциях и уровнях абстракции часто вызывает вопросы. Особенно когда речь заходит о switch-выражениях и TO-абзацах. Давайте разберёмся простыми словами.
Что такое уровень абстракции функции?
Уровень абстракции - это степень детализации описания действия. Представьте, что вы объясняете коллеге, как приготовить кофе:
- Высокий уровень: «Приготовь эспрессо» (бизнес-логика, цель).
- Средний уровень: «Смели зерна, засыпь в рожок, включи кофемашину» (конкретные шаги).
- Низкий уровень: «Нажми кнопку включения, подожди 10 секунд, протри поддон» (детали реализации).
В коде функция должна работать на одном уровне абстракции. Если внутри неё смешаны высокоуровневые вызовы (например, calculatePay()) и низкоуровневые операции (доступ к БД, форматирование строк), читатель путается - он не понимает, что именно делает функция.
Почему switch в примере - это плохо?
Вернёмся к примеру из книги:
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}Мартин пишет, что эта функция выполняет более одной операции. Почему? Потому что она объединяет два уровня абстракции:
- Высокий уровень: «Рассчитать зарплату сотрудника» (сама функция
calculatePay). - Низкий уровень: «Выбрать тип сотрудника и вызвать соответствующий метод» (логика switch).
Switch - это деталь реализации, которая относится к низкому уровню. Если функция называется calculatePay, читатель ожидает увидеть внутри только высокоуровневые шаги (например, calculateBaseSalary(), applyBonus()), а не ветвление по типам.
Как TO-абзацы помогают выделить уровни?
TO-абзацы (от англ. «To-do paragraphs») - это приём, когда вы пишете функцию как список действий на естественном языке. Например:
«Чтобы рассчитать зарплату сотрудника, нужно:
1. Получить базовую ставку.
2. Применить надбавки.
3. Вычесть налоги.»
Каждый пункт - это один уровень абстракции. Если вы замечаете, что один из пунктов требует уточнения («выбрать тип сотрудника»), значит, он относится к более низкому уровню и его нужно вынести в отдельную функцию.
Как правильно реорганизовать switch?
Мартин предлагает спрятать switch за полиморфизмом. Вместо calculatePay с ветвлением, создайте абстрактный класс Employee и переопределите метод в наследниках:
abstract class Employee {
abstract Money calculatePay();
}
class CommissionedEmployee extends Employee {
Money calculatePay() { /* своя логика */ }
}
class HourlyEmployee extends Employee {
Money calculatePay() { /* своя логика */ }
}Тогда вызов employee.calculatePay() будет на одном высоком уровне абстракции, а switch исчезнет.
Практические советы по выделению уровней
- Читайте функцию как предложение: если можно вставить слово «затем» между строками, значит, уровни смешаны.
- Проверяйте длину: функция должна умещаться на экране (10-20 строк). Если больше - скорее всего, в ней несколько уровней.
- Используйте принцип единой ответственности (SRP): одна функция - одна задача на одном уровне.