
Поправка. В этой статье я ссылаюсь на главу 2 из книги Роберта Мартина «Чистый код». Но мне недавно сказали, что конкретно эту главу писал Тим Оттингер. Однако Мартин указан как единственный автор книги, а значит, он вполне поддерживает совет той главы.
Несмотря на то, что книга «Чистый код» привнесла в наш лексикон прекрасный термин, она также снискала и дурную славу. Это руководство от 2008 года представляет собой сборник принципов и исследований, которые «дядя Боб» (Uncle Bob, то есть Роберт Мартин) выработал за годы программирования.
В итоге его практики переняли многие разработчики, одни из которых почитают их как святыни, а другие воспринимают, скорее, в качестве ориентиров, нежели строгих правил. Но, как бы вы к этому ни относились, сам дядя Боб смотрит на них не как на руководства. Он следует этим практикам всецело и очень редко допускает исключения.
Так что можно подумать, что его примеры рефакторинга из книги как минимум окажутся лучше среднего кода, который вы встречаете в повседневной работе, или хотя бы будут согласовываться с другими распространёнными советами.
Можно подумать...
Здесь нужно иметь в виду, что Мартин взял эти примеры не с потолка. Это реальные изменения, которые он производил в своей работе, и которые он использует в качестве поучительных.
В статье я буду разбирать самый первый пример рефакторинга из главы 2 «Содержательные имена».
Вот код «до»:
private void printGuessStatistics(char candidate, int count) {
String number;
String verb;
String pluralModifier;
if (count == 0) {
number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1) {
number = "1";
verb = "is";
pluralModifier = "";
} else {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format(
"There %s %s %s%s", verb, number, candidate, pluralModifier
);
print(guessMessage);
}
А вот «после»:
public class GuessStatisticsMessage {
private String number;
private String verb;
private String pluralModifier;
public String make(char candidate, int count) {
createPluralDependentMessageParts(count);
return String.format(
"There %s %s %s%s",
verb, number, candidate, pluralModifier );
}
private void createPluralDependentMessageParts(int count) {
if (count == 0) {
thereAreNoLetters();
} else if (count == 1) {
thereIsOneLetter();
} else {
thereAreManyLetters(count);
}
}
private void thereAreManyLetters(int count) {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
private void thereIsOneLetter() {
number = "1";
verb = "is";
pluralModifier = "";
}
private void thereAreNoLetters() {
number = "no";
verb = "are";
pluralModifier = "s";
}
}
О, боги.
Прежде чем переходить к анализу недочётов, хочу кое-что сказать.
В книге есть целая отдельная глава о разделении функций, но она как-то просочилась сюда. Мартин часто так делает в своих примерах. Его строгая приверженность к использованию реальных примеров изменения кода из своей работы затрудняет понимание преподаваемых им в каждой главе уроков. Он ведь мог сразу грамотно выстроить эти функции и просто изменить их имена.
То же самое во многих других примерах, где он показывает код «до», который имеет много существенных недочётов, а не один, которому посвящена текущая глава. Но Мартин исправляет и их тоже, усложняя для читателя понимание того, насколько эффективен конкретно совет из текущей главы.
Всё это я говорю, потому что буду оценивать код целостно, точно так же, как дядя Боб производит рефакторинг в своих примерах. Думаю, это честно.
Первым бросается в глаза то, что Мартин взял одну, преимущественно ЧИСТУЮ функцию (привет всем поклонникам функционального программирования) и сделал из неё класс. Причём не статический вспомогательный класс или что-то подобное, а ЭКЗЕМПЛЯР, атрибуты которого изменяются для получения конкретного результата.
Зачем он это делает? Всё дело в непреклонном стремлении Мартина к разделению функций и отсутствию аргументов, которое ведёт к тому, что единственным способом присвоить в функции переменные для внешнего использования остаются атрибуты. В Java нет возможности передачи по ссылке, только через классы, но Мартин не хочет даже передавать класс в виде аргумента и изменять его. В результате образуются побочные эффекты (то есть функции вносят изменения вне своих областей видимости).
Похоже, он также выбирает, когда использовать переменные экземпляра, а когда передавать аргументы. Например, почему count, являясь аргументом, не передаётся также в переменную экземпляра? Свою логику Мартин здесь не объясняет. Могу лишь предположить, что count передаётся в класс извне, но почему? Реализация его в виде переменной экземпляра уменьшит число передаваемых аргументов, к чему Мартин вроде как и стремится.
Может, мне нужно рассматривать make как конструктор? Это же неправильно, ведь он возвращает String, а не тип GuessStatisticsMessage. Если бы Мартин создал кейс, где эти переменные экземпляра требовались для чего-то ещё, кроме возвращаемой здесь строки, тогда ладно. Но он этого не делает.
Перейдём ко второму сомнительному моменту. Несмотря на то, что глава посвящена правильному именованию, по иронии они в этом примере не так уж хороши.
Что значит make?
Что значат number, verb и pluralModifier в контексте реализуемой задачи?
Почему функции, которые изменяют состояние, начинаются с there?
Как читающий должен понять, что означает GuessStatisticsMessage, не имея контекста вызывающего кода? И почему это имя представляет странную смесь из глагола и существительного?
Список вопросов можно продолжить.
Если вы мыслите как я, то вам тоже пришлось прочесть весь код, чтобы понять действия make. Этот метод генерирует грамматически корректное предложение и описывает количество вхождений конкретного символа (например, «There are 4 Cs», «There are no Ds», «There is 1 B» и так далее). Ничего более. Для меня всё это проще было бы понять как раз по изначальному коду.
Ну и последний интересный момент. Обратите внимание, что Мартин по факту не сделал код понятнее — даже если использованные им имена и были содержательны, то теперь анализировать нужно больше кода.
Из-за пробелов между функциями вся логика перестала умещаться на одном экране. В итоге приходится напрягать зрительную память, чтобы уместить в голове всю функциональность.
Сами функции тоже не упрощают общую картину, так как вынуждают искать точки их вызова и отслеживать передаваемые им аргументы. Хуже того, отслеживать изменения переменных экземпляра.
Простое прочтение тела createPluralDependentMessageParts (вот это имя!) не прояснит, что конкретно происходит внутри. Всё дело в том, что Мартин относит блоки if к более низкому уровню абстракции, чем окружающая функция, и поэтому считает, что их нужно инкапсулировать (то есть прятать) в собственные функции.
А теперь представьте, что пытаетесь понять этот код впервые. Вы доходите до блоков if и видите, что они делегируют выполнение другим методам. Остановитесь ли вы на этом? Вряд ли.
Любой, кто дочитал код до этого места, захочет узнать, что находится внутри thereAreNoLetters(). В действительности, вы не сможете понять метод make без понимания того, как меняются verb, number и pluralModifier. Вы можете это угадать, глядя на возврат строки и порядок использования этих полей. Но здесь никак не поймёшь, что "no" заменяет 0, пока не копнёшь глубже. И это очень важная деталь для понимания make, которая запрятана на 2 уровня вглубь.
Такое не интуитивное разделение возникает из-за обострённого восприятия Мартином любых уровней абстракции, когда малейшее отклонение уже вызывает тревогу. Но суть в том, что это его личное видение. Большинство программистов, скорее, прибегнут к небольшому абстрагированию, нежели станут фрагментировать простую функцию на stateful-дерево методов, управляемое побочными эффектами.
Изначальный метод по факту был даже лучше. Намного. Но Мартин прав в том, что его можно было улучшить. Просто он пошёл не тем путём. Вот лично моя «идеальная» версия:
private String generateGuessSentence(char candidate, int count) {
String verbIs = count == 1 ? "is" : "are";
String countStr = count == 0 ? "no" : Integer.toString(count);
String sIfPlural = count == 1 ? "" : "s";
return String.format(
"There %s %s %s%s",
verbIs, countStr, candidate, sIfPlural
);
}
Вы могли заметить в исходном коде некоторую избыточность, в частности то, что значения String в 2 из 3 случаев одинаковы. Одно из правил Мартина гласит, что нужно стремиться сводить повторы к минимуму, и он сам нарушает его без оснований.
Вы также могли обратить внимание, что инструкции if не делают ничего, кроме присваивания значений переменным. В других примерах Мартин использует тернарные операторы, но почему не здесь? Возможно, он решил, что 3 кейса count будут понятнее, чем присваивание каждого значения с помощью отдельного условия, как сделал я.
Странно, не так ли? Мартин вынес логику в функции, прежде чем её оптимизировать. Я не могу прочесть его мысли, но в книге он почти не упоминает альтернативных подходов к рефакторингу. Он просто показывает свою доработанную версию и поверхностно объясняет, почему она является наилучшей. Я же считаю, что эта тема должна быть более раскрыта.
Ладно, если уж Мартин так хочет сохранить эти три кейса, именование и прочие функции, то я могу подыграть. Но и в этом случае его рефакторинг мог бы быть почище, даже по его собственным стандартам. Вот ещё одна моя версия:
public class GuessStatisticsMessage {
private char candidate;
private int count;
public GuessStatisticsMessage(char candidate, int count) {
this.candidate = candidate;
this.count = count;
}
public String make() {
if (count == 0) {
return thereAreNoLetters();
} else if (count == 1) {
return thereIsOneLetter();
} else {
return thereAreManyLetters();
}
}
private String thereAreManyLetters() {
return String.format(
"There are %s %ss", count, candidate
);
}
private String thereIsOneLetter() {
return String.format(
"There is 1 %s", candidate
);
}
private String thereAreNoLetters() {
return String.format(
"There are no %ss", candidate
);
}
}
Только посмотрите! Подобающий конструктор, никаких аргументов, и все операции со строками находятся в методах нижнего уровня. Вот теперь этот код чист! Просто не обращаем внимания, что причин для использования класса здесь нет.
Предлагаю подытожить. Если вы вдруг соберётесь прочесть эту книгу, игнорируйте предложенные в ней версии рефакторинга и придумывайте свои. Ну и изложенные автором принципы тоже стоит воспринимать с долей скептицизма.
P.S.
Даже не верится, что это моя первая статья.
Мне захотелось её написать, когда я заметил, как часто книгу «Чистый код» безусловно рекомендуют начинающим. К счастью, мне не доводилось работать с теми, кто пишет код таким образом. Но всё равно становится страшно за будущее отрасли, когда думаешь о том, что молодые разработчики будут на ней учиться.
Воспринимать конкретные мнения по части практик разработки ПО нужно критически. Сфера ПО ещё недостаточно зрелая и постоянно меняется. К тому моменту, когда вы станете экспертом, её ландшафт уже перестроится.
Мне также понравилось недавнее интервью Мартина на канале YouTube «ThePrimeTime». Они говорили с ведущим о разном, и в основном это была беспредметная беседа, хотя Primagen местами ставил под сомнение некоторые принципы «чистого кода».
Я всё же ожидал, что Мартин, спустя 16 лет после выхода его книги, ослабит некоторые из своих наиболее…спорных принципов. Но нет. Его напор только усилился, и это стало для меня последней каплей. Я не имею к нему личных претензий, просто считаю, что недостаточно людей озвучивают слабые места его рекомендаций.
Ну а вас я благодарю вас за чтение и желаю приятного дня!
Комментарии (2)

mapchelka
30.11.2025 09:10Хочу поделиться своим мнением: мне было очень трудно читать книгу, хотя я была согласна с принципами которые в ней описавают. Мне откликнулись ваши разборы примеров из книги, они действительно сложные, в них разбирают более чем одно исправление ошибки. Это противоречит подходу самого Чистого кода, и действительно усложняет понимание описываемой темы. В целом книга похожа больше на научный труд, а не на учебное пособие, которое можно предложить начинающему специалисту.
ohrenet
Чистая архитектура это прекрасная иллюстрация поговорки "Заставь дурака б-гу молиться - лоб расшибёт".
Nansch
Кстати, о стекле...