
Когда я в прошлом году услышал, что Дядя Боб планирует выпустить вторую редакцию «Чистого кода», то был восхищён, а это для меня редкость. Я считал, что и первый выпуск был хорош, хотя сам читаю редко.
Возможно, причиной восторга стала мысль о том, что я смогу снова разнести его примеры кода, как сделал в своей первой статье.
Или же меня обнадёжило данное Мартином обещание доработать руководства из предыдущей книги. Знаете, то удовольствие, когда читаешь заметки к долгожданным патчам для рабочего ПО.
А может, это была глубинная надежда, что кто-то, наконец, пересмотрел его идеи и осознал необходимость изменения подхода Мартина к написанию «чистого кода». Всё же это была самая жестокая критика первой редакции книги с момента её публикации более 17 лет назад.
Несмотря на весь свой цинизм и любовь постебаться, я искренне уважаю тех, кто может признать свои ошибки и взглянуть на вещи по-новому. Я испытываю глубокую радость, когда мой посыл доходит до умов людей и меняет их взгляд на вопросы, в которых они грубо заблуждаются (хотя порой мне кажется, что мой напористый подход может, наоборот, этому мешать).
Так что представьте, каково было моё разочарование, когда я потратил $60 на электронную версию этой книги, в которой Боб не просто не изменил своей позиции по большинству спорных практик, но и продолжил топить за них ещё круче!
Невероятно!
Но я забегаю вперёд…
Плюсы
Если смотреть в общем, то я согласен с большинством взглядов Дяди Боба.
Он говорит о том, что профессионалы должны предотвращать загнивание ПО, активно применяя принципы чистого кода, даже когда это замедляет вас в моменте.
Мне особенно понравилась его гипотетическая шутка на больную тему о том, как код стал настолько плохим, что пришлось создать отдельный рабочий процесс для его переписывания. Естественно, это ведёт к появлению багов и истощающим издержкам из-за необходимости согласования переписанной версии с легаси-кодом в плане новых фич и исправлений ошибок.
Мартин говорит о важности чистого кода не только из соображений продуктивности, но и по этическим причинам, имея в виду вред, который ошибки в софте могут нанести нашему программно-зависимому обществу.
Его приверженность к написанию чистого кода явно происходит из печального опыта. И первую главу стоит прочесть, хотя бы ради знакомства с его принципами.
Эти принципы архитектуры и проектирования известны как SOLID, ничего нового, и если вас интересуют тонкости создания хорошей архитектуры, то я рекомендую почитать эти разделы тоже.
Видно, что вторая редакция была обновлена под современные реалии. В ней затрагивается тема LLM и их роли в разработке ПО. Боб даже привлекает к работе Grok и Copilot, чтобы сравнить свои версии рефакторинга с теми, которые предлагает ИИ.
Особенно же мне понравилось, что он активно старается выражать свои идеи с использованием современных языков и конструкций. К примеру, он не придерживается исключительно Java, а также обращается к Golang, Python и JavaScript. И даже при работе с Java он задействует более современные конструкты вроде лямбд, потоков, классов Record и сопоставлений с шаблоном. Мартин определённо освоил многие принципы функционального программирования, и это приятно видеть.
Он разбивает каждый процесс рефакторинга на понятные этапы и подробно объясняет свой ход мысли между ними. Меня бесит, когда эксперты сразу переходят к итоговым решениям и обосновывают их постфактум. Хорошо, что Боб так не делает.
Обсуждая каждую свою идею, которая может показаться спорной, он старается развеять возможные контраргументы других людей. В процессе чтения я нередко замечал сомнительные моменты в его нити мысли, и буквально в следующем абзаце он приводил для них доводы. Это признак человека, который умеет вести технические дискуссии, и в этом плане книга определённо лучше первого издания.
Причём в последнем её разделе фактически приводится стенограмма его нашумевшего диалога с Джоном Остерхаутом, который ставил под сомнение принципы чистого кода. Неплохой бонус.
Всё, что вам могло нравиться в первой редакции, здесь тоже есть, причём этого больше.
Минусы
Но вернулось и всё то, что вы ругали — с лихвой.
Мартин повторно использовал некоторые примеры из первой книги, в частности, отвратительные классы GuessStatisticsMessage и PrimeGenerator. Он до сих пор считает, что они достаточно чисты для упоминания в подобном руководстве.
Но вместо того, чтобы ворошить старый код, я взгляну на один из новых примеров. Ниже показан первый крупный пример из книги, конкретно из второй главы «Clean That Code!». С помощью ИИ Мартин намеренно написал грязный код в целях демонстрации:
public class FromRoman {
public static int convert(String roman) {
if (roman.contains("VIV") ||
roman.contains("IVI") ||
roman.contains("IXI") ||
roman.contains("LXL") ||
roman.contains("XLX") ||
roman.contains("XCX") ||
roman.contains("DCD") ||
roman.contains("CDC") ||
roman.contains("MCM")) {
throw new InvalidRomanNumeralException(roman);
}
roman = roman.replace("IV", "4");
roman = roman.replace("IX", "9");
roman = roman.replace("XL", "F");
roman = roman.replace("XC", "N");
roman = roman.replace("CD", "G");
roman = roman.replace("CM", "O");
if (roman.contains("IIII") ||
roman.contains("VV") ||
roman.contains("XXXX") ||
roman.contains("LL") ||
roman.contains("CCCC") ||
roman.contains("DD") ||
roman.contains("MMMM")) {
throw new InvalidRomanNumeralException(roman);
}
int[] numbers = new int[roman.length()];
int i = 0;
for (char digit : roman.toCharArray()) {
switch (digit) {
case 'I' -> numbers[i] = 1;
case 'V' -> numbers[i] = 5;
case 'X' -> numbers[i] = 10;
case 'L' -> numbers[i] = 50;
case 'C' -> numbers[i] = 100;
case 'D' -> numbers[i] = 500;
case 'M' -> numbers[i] = 1000;
case '4' -> numbers[i] = 4;
case '9' -> numbers[i] = 9;
case 'F' -> numbers[i] = 40;
case 'N' -> numbers[i] = 90;
case 'G' -> numbers[i] = 400;
case 'O' -> numbers[i] = 900;
default -> throw new InvalidRomanNumeralException(roman);
}
i++;
}
int lastDigit = 1000;
for (int number : numbers) {
if (number > lastDigit) {
throw new InvalidRomanNumeralException(roman);
}
lastDigit = number;
}
return Arrays.stream(numbers).sum();
}
public static class InvalidRomanNumeralException extends RuntimeException {
public InvalidRomanNumeralException(String roman) {
}
}
Вот, что он делает:
Проверяет входную строку на наличие недопустимых последовательностей знаков в римских числах.
Заменяет двухзначные римские числа, включающие вычитание, на специальные однозначные символы.
Проверяет строку на присутствие излишних повторений определённых знаков («IIII» должно записываться как «IV»).
Перебирает каждый символ и преобразует его в десятичный эквивалент, в том числе полученные ранее кастомные символы, после чего помещает результат в массив.
Следит, чтобы числа в полученном массиве располагались не в порядке возрастания (выявляя недопустимые записи вроде «VX»).
Складывает все числа и возвращает результат.
В течение оставшейся части главы Боб пошагово описывает предлагаемый им рефакторинг. Я эти шаги показывать не стану. Если интересно, можете почитать книгу.
Примечание. Хочу отметить, что это довольно хитрый пример. Если вы заведомо не знаете, какой алгоритм окажется оптимальным, то будет сложно определить, какой рефакторинг здесь необходим — очистительный или алгоритмический. Первый ведёт к сокращению повторов, более лаконичному выражению синтаксиса, выносу функций или переменных и так далее. А второй означает пересмотр самой логики в поиске более простого решения или оптимизации.
Вот версия рефакторинга от Дяди Боба:
public class FromRoman2 {
private String roman;
private List<Integer> numbers = new ArrayList<>();
private int charIx;
private char nextChar;
private Integer nextValue;
private Integer value;
private int nchars;
Map<Character, Integer> values = Map.of(
'I', 1,
'V', 5,
'X', 10,
'L', 50,
'C', 100,
'D', 500,
'M', 1000);
public FromRoman2(String roman) {
this.roman = roman;
}
public static int convert(String roman) {
return new FromRoman2(roman).doConversion();
}
private int doConversion() {
checkInitialSyntax();
convertLettersToNumbers();
checkNumbersInDecreasingOrder();
return numbers.stream().reduce(0, Integer::sum);
}
private void checkInitialSyntax() {
checkForIllegalPrefixCombinations();
checkForImproperRepetitions();
}
private void checkForIllegalPrefixCombinations() {
checkForIllegalPatterns(
new String[]{"VIV", "IVI", "IXI", "IXV", "LXL", "XLX",
"XCX", "XCL", "DCD", "CDC", "CMC", "CMD"});
}
private void checkForImproperRepetitions() {
checkForIllegalPatterns(
new String[]{"IIII", "VV", "XXXX", "LL", "CCCC", "DD", "MMMM"});
}
private void checkForIllegalPatterns(String[] patterns) {
for (String badString : patterns)
if (roman.contains(badString)) throw new InvalidRomanNumeralException(roman);
}
private void convertLettersToNumbers() {
char[] chars = roman.toCharArray();
nchars = chars.length;
for (charIx = 0; charIx < nchars; charIx++) {
nextChar = isLastChar() ? 0 : chars[charIx + 1];
nextValue = values.get(nextChar);
char thisChar = chars[charIx];
value = values.get(thisChar);
switch (thisChar) {
case 'I' -> addValueConsideringPrefix('V', 'X');
case 'X' -> addValueConsideringPrefix('L', 'C');
case 'C' -> addValueConsideringPrefix('D', 'M');
case 'V', 'L', 'D', 'M' -> numbers.add(value);
default -> throw new InvalidRomanNumeralException(roman);
}
}
}
private boolean isLastChar() {
return charIx + 1 == nchars;
}
private void addValueConsideringPrefix(char p1, char p2) {
if (nextChar == p1 || nextChar == p2) {
numbers.add(nextValue - value);
charIx++;
} else
numbers.add(value);
}
private void checkNumbersInDecreasingOrder() {
for (int i = 0; i < numbers.size() - 1; i++)
if (numbers.get(i) < numbers.get(i + 1))
throw new InvalidRomanNumeralException(roman);
}
public static class InvalidRomanNumeralException extends RuntimeException {
public InvalidRomanNumeralException(String roman) {
super("Invalid Roman numeral: " + roman);
}
}
}
Похоже, он ничему не научился. Что делает этот код:
Проверяет наличие в строке недопустимых последовательностей числовых символов (неправильных префиксов и лишних повторений).
-
Перебирает символы римской записи и в каждой итерации:
Определяет текущую букву и следующую (если следующая есть).
Если текущая буква «I», «X» либо «C», проверяет, является ли она префиксом для следующей буквы. Если да, вычитает значение текущей буквы из значения следующей, добавляет его в список и при очередной итерации следующую букву пропускает.
Если следующей буквы нет, просто добавляет текущую в список.
В завершение проверяет, чтобы в списке никакое число не было больше предшествующего ему.
Первым делом он взял чистую функцию и вместо явной передачи аргументов превратил её в метод экземпляра с атрибутами. В тот раз он сделал то же самое, но теперь он приводит обоснования, которые я прокомментирую позже.
Далее всё, как и в прошлый раз — он зачем-то реализовал декомпозицию методов.
К примеру, метод doConversion вызывает три других метода, но они не ведут к уменьшению повторений и не делают понятнее сам метод. Естественно, выглядит всё как высокоуровневый список шагов, но если код будут читать люди без технического бэкграунда, то такой подход лишь затрудняет понимание того, КАК происходит преобразование.
Когда читающий дойдёт до метода doConversion, он уже будет догадываться, что внутри творится безобразие. Слово «conversion» это проясняет, поэтому произвольное вынесение этих деталей в отдельные функции только впустую тратит время. Для того, чтобы понять, как работает всё это преобразование, мне нужно углубиться на три метода, каждый из которых содержит слово «convert». Зачем?
Если придание коду эстетичности делает его «мутным», то лучше пожертвовать эстетикой ради ясности.
Признаю, в этом примере всё не так уж плохо. После прочтения каждого метода я счёл их имена вполне интуитивными. Но вы поняли, в чём суть?
После того, как я прочёл каждый метод.
Поскольку аргументов нет, как нет и гарантии чистоты (о чём говорит избыток переменных экземпляра), мне приходится слепо верить, что имя каждого метода правильно описывает его действие без каких-либо побочных эффектов.
Если вы, как и Дядя Боб, смотрите через призму хиндсайта, то это не проблема. Но я считаю, что код должен быть равноценно понятным как для тех, кто уже с ним знаком, так и для тех, кто видит его впервые. А одним из важнейших элементов читаемости является уверенность в том, что каждый метод делает именно то, о чём заявляет. Без уверенности читающие будут вынуждены прочитывать все эти методы, и польза от созданной Мартином абстракции будет сведена на нет, а издержки останутся.
Очевидно, что чистые функции не заслуживают доверия по умолчанию, но это не бинарный вопрос. Чистота больше способствует детерминизму и самодостаточности, что, в свою очередь, способствует большему доверию.
Вот что происходит, когда вы оптимизируете код с акцентом на поверхностную читаемость (хорошие имена методов, скрывающие сложность), позволяя расцветать непредсказуемому поведению, ведущему к «запутанности состояний».
Я мог бы построчно перебрать весь код и объяснить каждый момент, на котором у меня возникало недоумение типа «да ну?» или «что, серьёзно?». Но наверняка будет нагляднее, если я просто приведу собственную версию рефакторинга с сохранением общей логики Боба.
public class FromRoman3 {
private static final Map<Character, Integer> ROMAN_NUMERALS = Map.of(
'I', 1,
'V', 5,
'X', 10,
'L', 50,
'C', 100,
'D', 500,
'M', 1000);
private static final Map<Character, Character> NUMERAL_PREFIXES = Map.of(
'V', 'I',
'X', 'I',
'L', 'X',
'C', 'X',
'D', 'C',
'M', 'C'
);
private static final String[] ILLEGAL_PREFIX_COMBINATIONS = new String[]{
"VIV", "IVI", "IXI", "IXV", "LXL", "XLX",
"XCX", "XCL", "DCD", "CDC", "CMC", "CMD"
};
private static final String[] IMPROPER_REPETITIONS = new String[]{
"IIII", "VV", "XXXX", "LL", "CCCC", "DD", "MMMM"
};
public static int convert(String roman) {
if (containsIllegalPatterns(roman, ILLEGAL_PREFIX_COMBINATIONS) ||
containsIllegalPatterns(roman, IMPROPER_REPETITIONS)) {
throw new InvalidRomanNumeralException(roman);
}
List<Integer> numbers = new ArrayList<>();
int i = 0;
while (i < roman.length()) {
char currentLetter = roman.charAt(i);
char nextLetter = i == roman.length() - 1 ? 0 : roman.charAt(i + 1);
if (!ROMAN_NUMERALS.containsKey(currentLetter)) {
throw new InvalidRomanNumeralException(roman);
} else if (NUMERAL_PREFIXES.getOrDefault(nextLetter, (char) 0) == currentLetter) {
int num = ROMAN_NUMERALS.get(nextLetter) - ROMAN_NUMERALS.get(currentLetter);
numbers.add(num);
i += 2;
} else {
int num = ROMAN_NUMERALS.get(currentLetter);
numbers.add(num);
i += 1;
}
}
if (containsIncreasingNumbers(numbers)) {
throw new InvalidRomanNumeralException(roman);
}
return numbers.stream().mapToInt(Integer::intValue).sum();
}
private static boolean containsIllegalPatterns(String roman, String[] patterns) {
for (String badString : patterns)
if (roman.contains(badString)) return true;
return false;
}
private static boolean containsIncreasingNumbers(List<Integer> numbers) {
for (int i = 0; i < numbers.size() - 1; i++)
if (numbers.get(i) < numbers.get(i + 1)) return true;
return false;
}
public static class InvalidRomanNumeralException extends RuntimeException {
public InvalidRomanNumeralException(String roman) {
super("Invalid Roman numeral: " + roman);
}
}
}
Первым делом я заменил все переменные экземпляра локальными, передаваемыми через аргументы. Одно только это сделало код куда более читаемым.
Затем я извлёк каждое явное упоминание римских цифр в константы, отчасти из соображений производительности. Основная же цель была в том, чтобы в итоге работать с цифрами через имена констант, а не имена функций, как у Боба.
После этого я перекроил структуру функции.
Я превратил функцию преобразования в самую жирную во всём классе. Вы же залезли в эти дебри, чтобы узнать, как работает числовое преобразование — так вот оно, во всей своей красе.
Я изменил checkForIllegalPatterns на containsIllegalPatterns и перенёс механизм выброса исключений в основную функцию. Мне показалось, так будет прозрачнее. Слово «check» никак не проясняет, что происходит, если проверка проваливается, а «contains», вместе с остальной частью сигнатуры, чётко говорит о том, что делает функция.
Я также изменил checkNumbersInDecreasingOrder на containsIncreasingNumbers и вынес исключение наружу по аналогии с этапами перед проверкой. Но здесь же нет повторов, так зачем я сохранил этот метод? По двум причинам:
Его можно понять сам по себе.
Пост-проверочный этап — это не главная цель функции конвертации.
Самым проблемным оказался цикл преобразования. Боб написал его так, что если я просто встрою addValueConsideringPrefix, то возникнет много повторений. Мне же хотелось подчистить этот момент, не меняя его алгоритм.
Первой мыслью было использовать Map<Character, Character[]> для сопоставления каждой префиксной буквы с потенциальной следующей. Но в ходе реализации этой задумки я понял, что могу просто реверсировать сопоставление, сопоставляя каждую букву с её префиксом. И поскольку такое реверсивное сопоставление было уникальным, мне не потребовалось использовать Character[] в качестве типа значения.
После этого осталось лишь сохранить всю логику внутри цикла (который я изменил на while, чтобы сделать инкрементацию индекса более явной).
Естественно, в результате потерялась лаконичность сопоставления с шаблоном, но отсутствие абстрагирования того стоило.
К слову, Боб не поленился привести обширный набор тестов, который оказался весьма кстати при таком витиеватом алгоритме. Так что я не менее уверен в своём коде, чем он в своём.
import fromRoman.FromRoman.InvalidRomanNumeralException;
import org.junit.jupiter.api.Test;
import static fromRoman.FromRoman.convert;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class FromRomanTest {
@Test
public void valid() throws Exception {
assertThat(convert(""), is(0));
assertThat(convert("I"), is(1));
assertThat(convert("II"), is(2));
assertThat(convert("III"), is(3));
assertThat(convert("IV"), is(4));
assertThat(convert("V"), is(5));
assertThat(convert("VI"), is(6));
assertThat(convert("VII"), is(7));
assertThat(convert("VIII"), is(8));
assertThat(convert("IX"), is(9));
assertThat(convert("X"), is(10));
assertThat(convert("XI"), is(11));
assertThat(convert("XII"), is(12));
assertThat(convert("XIII"), is(13));
assertThat(convert("XIV"), is(14));
assertThat(convert("XV"), is(15));
assertThat(convert("XVI"), is(16));
assertThat(convert("XIX"), is(19));
assertThat(convert("XX"), is(20));
assertThat(convert("XXX"), is(30));
assertThat(convert("XL"), is(40));
assertThat(convert("L"), is(50));
assertThat(convert("LX"), is(60));
assertThat(convert("LXXIV"), is(74));
assertThat(convert("XC"), is(90));
assertThat(convert("C"), is(100));
assertThat(convert("CXIV"), is(114));
assertThat(convert("CXC"), is(190));
assertThat(convert("CD"), is(400));
assertThat(convert("D"), is(500));
assertThat(convert("CDXLIV"), is(444));
assertThat(convert("DCXCIV"), is(694));
assertThat(convert("CM"), is(900));
assertThat(convert("M"), is(1000));
assertThat(convert("MCM"), is(1900));
assertThat(convert("MCMXCIX"), is(1999));
assertThat(convert("MMXXIV"), is(2024));
}
@Test
public void invalid() throws Exception {
assertInvalid("ABE"); // I added this one
assertInvalid("IIII");
assertInvalid("VV");
assertInvalid("XXXX");
assertInvalid("LL");
assertInvalid("CCCC");
assertInvalid("DD");
assertInvalid("MMMM");
assertInvalid("XIIII");
assertInvalid("LXXXX");
assertInvalid("DCCCC");
assertInvalid("VIIII");
assertInvalid("MCCCC");
assertInvalid("VX");
assertInvalid("IIV");
assertInvalid("IVI");
assertInvalid("IXI");
assertInvalid("IXV");
assertInvalid("VIV");
assertInvalid("XVX");
assertInvalid("XVV");
assertInvalid("XIVI");
assertInvalid("XIXI");
assertInvalid("XVIV");
assertInvalid("LXL");
assertInvalid("XLX");
assertInvalid("XCX");
assertInvalid("XCL");
assertInvalid("CDC");
assertInvalid("DCD");
assertInvalid("CMC");
assertInvalid("CMD");
assertInvalid("MCMC");
assertInvalid("MCDM");
}
private void assertInvalid(String r) {
assertThrows(InvalidRomanNumeralException.class, () -> convert(r));
}
}
Боб утверждает, что его код чище оригинала. Но это не так. Мне даже нравится открытость изначальной функции. Она ничего не разбивает на части, но при этом почти не содержит ветвлений, что делает её более понятной. Это преобразование из двухзначных чисел в однозначные даже показалось мне…элегантным, что ли?
Серьёзно. Всё благодаря тому, что цикл преобразования чисел в десятичную запись почти не содержит логики. Его вполне можно представить таблицей. Боб отмечает, что изначальный код не проходит все тестовые кейсы, но я смог это исправить внесением лишь незначительных изменений.
Это не единственный пример, в котором Боб перегибает с декомпозицией. Но я бы писал статью до ночи, если бы разбирал каждый.
Жёсткий разнос
После приведённого выше рефакторинга он пишет:
Функциональные программисты могли ужаснуться от того, что функции не «чистые». Но в действительности функция преобразования чиста настолько, насколько это возможно. Остальные же — это лишь небольшие вспомогательные методы, которые работают внутри одного вызова этой общей чистой функции. Удобство используемых переменных экземпляра в том, что они позволяют отдельным методам общаться, не прибегая к передаче аргументов. Это показывает, что одним из удачных применений объекта является возможность реализовать взаимодействие внутри чистой функции через переменные его экземпляра.
И в главе 7 «Clean Functions» он утверждает, что следующие три варианта сигма-функции одинаково грязные:
public static double sigma(double… ns) {
var mu = mean(ns);
var deviations = Arrays.stream(ns)
.map(x->(x-mu)*(x-mu))
.boxed().mapToDouble(x->x);
double variance = deviations.sum() / ns.length;
return Math.sqrt(variance);
}
public static double sigma(double… ns) {
double mu = mean(ns);
double variance = 0;
for (double n : ns) {
var deviation = n - mu;
variance += deviation * deviation;
}
variance /= ns.length;
return Math.sqrt(variance);
}
public static double sigma(double… ns) {
return new SigmaCalculator(ns).invoke();
}
private static class SigmaCalculator {
private double[] ns;
private double mu;
private double variance = 0;
private double deviation;
public SigmaCalculator(double… ns) {
this.ns = ns;
}
public double invoke() {
mu = mean(ns);
for (double n : ns) {
deviation = n - mu;
variance += deviation * deviation;
}
variance /= ns.length;
return Math.sqrt(variance);
}
}
Вы можете задуматься, как он мог прийти к такому выводу?
Всё просто. Он неверно трактует само понятие «чистая функция». То есть он вроде бы и понимает, о чём оно, но потом говорит:
Как создаётся чистая функция? Ничего сложного. Просто не нужно менять значения никаких переменных. Или, перефразируя известную реплику из кинофильма «Дорогая мамочка!»: «Никаких присваиваний, никогда!». Ещё можно сказать просто: «Чистые функции иммутабельны».
На что он опирается?
На книгу «Чистая архитектура. Искусство разработки программного обеспечения», Роберта С. Мартина.
Итого Дядя Боб нарушил два принципа функционального программирования.
Первый — это принцип чистоты, который означает отсутствие побочных эффектов, то есть функции не должны вносить изменения вне своей области видимости.
Второй — это иммутабельность, то есть отсутствие повторных присваиваний переменных и изменений существующих значений.
Можно следовать первому, нарушая второй.
Вроде бы ничего сверхсерьёзного, но Мартин во многом строит на этом свои доводы. Поскольку инструкции присваивания в его понимании «грязные», он приравнивает в этом смысле два первых примера к третьему.
Теперь у нас есть переменные экземпляра и всевозможные манипуляции с ними. Тем не менее сигма-функция чиста. Ни одна из «грязных» операций не выпирает за её пределы. Суть в том, что чистота — это внешняя характеристика функции, а не внутренняя. Неважно, насколько запачкана функция внутри — она будет оставаться чистой, пока вся эта грязь скрыта от внешних наблюдателей (включая другие потоки).
Он коверкает определение чистой функции, по сути, применяя её только с позиции публичных методов.
Я в полном негодовании.
Принцип чистоты должен применяться ко всем функциям, а не только ко внешним. Его смысл — упростить понимание кода, включая детали реализации.
Боб утверждает, что передача переменных экземпляра несёт меньше издержек, чем передача аргументов функции. И не удивительно. Ведь он считает, что присвоил методам имена настолько точные и описательные, что аргументы просто излишни.
Представим, что кто-нибудь решает залезть в метод, чтобы понять деталь его реализации.
Он видит, что метод усыпан ссылками на переменные экземпляра, и задумывается, какие значения у этих переменных были раньше.
Возможно, этот метод зависит от того, чтобы конкретные переменные инициализировались определённым образом в определённых случаях.
Возможно, для правильного вызова этого метода сначала нужно вызвать какие-то другие методы.
Иными словами, каждый метод зависит от общего состояния.
Неужели Боб ожидает, что люди будут прочитывать каждый метод, прежде чем начнут разбирать тот, который хотят понять. Не лишает ли это абстракцию её изначального смысла?
Боб заменил издержки, связанные с аргументами метода, ещё большей проблемой общего состояния. Тот факт, что это состояние существует только внутри конкретного экземпляра класса, не делает такой подход приемлемым.
Чистые функции читаются как контракты (и аргументы являются частью такого контракта). Они получают конкретный ввод, в каком бы состоянии он ни находился, выполняют с ним определённые операции и каждый раз выводят один и тот же результат. Это упрощает их понимание в виде отдельных компонентов и уменьшает когнитивную нагрузку. Единственное «общее» состояние, если его вообще можно так назвать, это то, что функция более высокого уровня передаёт в виде аргументов в своих вызовах.
Stateful-методы без аргументов просят принимать их имена за чистую монету, как бы умышленно отвлекая вас от внутренней неразберихи. С каждым очередным шагом внутрь метода вам нужно добавлять в свой ментальный граф выполнения дополнительный узел (что вы в любом случае делаете), но при этом ещё и учитывать состояние каждой переменной экземпляра между вызовами функции.
Это может не создавать особых проблем, если общее состояние возникает только между двух функций, вызываемых последовательно. Но что случится, если оно уйдёт на четыре уровня вглубь в четырёх разных цепочках вызовов? У Боба, должно быть, чертовски крутая оперативная память, чтобы удерживать и обрабатывать в голове всю эту информацию.
Вы можете поспорить, что в плане когнитивной нагрузки между передачей локальных аргументов и ссылками на переменные экземпляра нет ощутимой разницы. Но она есть и связана с областью видимости. Делая область переменной максимально узкой, вы сокращаете участок, в течение которого читающему код нужно удерживать её в памяти для рассуждения.
Будь это не так, вы могли бы просто расширять область видимости каждой переменной до уровня класса, и никаких проблем. Но даже Боб не стал бы писать такой код.
Так что он либо не в курсе этих когнитивных издержек, либо же просто их СИЛЬНО недооценивает. Я склоняюсь ко второму, но это в любом случае должно быть стыдно.
Заключение
Моё мнение с прошлого раза не изменилось.
Следуйте рекомендациям Мартина в общих чертах, но примеры игнорируйте. Если вы ожидаете найти в них улучшения, то их там нет.
Признаю, в этой редакции Боб менее догматичен в своих примерах рефакторинга, и это радует. Но я также не думаю, что при виде такого плачевного «улучшенного» кода занять позицию «пусть каждый останется при своём мнении» нельзя. Понимаю, звучит грубо, но вежливо это никак не выразишь.
Ну а вас я снова благодарю за чтение и желаю приятного дня!
P.S.
Прошло пять дней с момента размещения книги на Amazon, и я ещё не видел по ней ни одной статьи или видео. Какое-то радиомолчание. Даже сам Дядя Боб не продвигает своё детище. Мне даже немного не по себе.
Она так одинока.
Комментарии (87)

cupraer
05.12.2025 13:24
Колизей, вход №44, надпись: «XLIIII» Источник: http://www.web40571.clarahost.co.uk/roman/howtheywork.htm
Как бы вот научить программистов сначала подвергать анализу ТЗ, а потом бросаться писать код? Ладно инфоцыган Мартин, откуда ему. Но вас что, в школе не учили, что до начала второго тысячелетия варианты «IIII» (4) и «XXXX» (40) были вполне себе используемы, и они по сию пору вполне легитимны?
Вот корректное решение на эликсире, вдруг надо.
defmodule RomanNumerals do require Integer @romans 'IVXLCDM' @spec numeral(pos_integer()) :: String.t() def numeral(number, romans \\ @romans) do fours = fn <<c, c, c, c>>, [last], _f -> <<c, last>> <<c, c, c, c>>, [c, next | _], _f -> <<c, next>> <<next, c, c, c, c>>, [c, next, result | _], _f -> <<c, result>> <<some, c, c, c, c>>, [c, next | _], _f -> <<some, c, next>> input, [_ | next], f -> f.(input, next, f) end romans |> Enum.with_index() |> Enum.reverse() |> Enum.reduce({number, []}, fn {c, i}, {number, acc} -> denominator = round( if Integer.is_even(i), do: :math.pow(10, i / 2), else: 5 * :math.pow(10, (i - 1) / 2) ) { rem(number, denominator), acc ++ List.duplicate(c, div(number, denominator)) } end) |> elem(1) |> to_string() |> String.replace(~r/.?(.)\1\1\1/, fn m -> fours.(m, romans, fours) end) end end
AdrianoVisoccini
05.12.2025 13:24Как бы вот научить программистов сначала подвергать анализу ТЗ, а потом бросаться писать код?
Во всех более-менее крупных командах для этого существует специальная бизнес-единица, именуемая аналитиком. А если заказчик хочет так как сделано у автора? Если ему вот эти варианты не нравятся? Дальше что?

cupraer
05.12.2025 13:24«Я так вижу» и «аналитика» — это разные вещи. Аналитика подразумевает в том числе обоснование для заказчика всех ошибок в ТЗ.
Заказчик может хотеть выплачивать с каждой сделки 121% НДС вместо 21% (реальный случай). Долг аналитика — заказчика от этого уберечь.
Теперь что касается ширины и толщины команд. Я что-то не вижу нескольких авторов в атрибуции данного текста, а значит — назвался груздем — полезай в кузов. Скулёж «это должен делать аналитик, стажёр, уборщица» — удел неудачников. Чья подпись под текстом — тот и отвечает.

Andrey_Solomatin
05.12.2025 13:24Про ТЗ вы верно подметили. Вы уточнили один из пунктов и решили другую задачу.

cupraer
05.12.2025 13:24Вы издеваетесь? Что значит «другую»? Задача звучит так: есть число записанное римскими цифрами, нужно его перевести в десятичную запись. Нет? Как-то иначе?
Не существует более насквозь римской нотации для чисел, чем надпись над одними из ворот Колизея. Значит, решения и инфоцыгана, и автора текста выше, — некорректно обрабатывают хрестоматийно корректный ввод. Dixi.

dogbert01
05.12.2025 13:24Если заказчики древние римляне, посещавшие этот колизей, то писать надо так, да. Но если заказчик живет в наше время и пользуется общепринятой на сегодняшний день системой римских цифр, то задание выполнено корректно.

cupraer
05.12.2025 13:24Вы тоже издеваетесь?
Заказчик живет в наше время и пользуется общепринятой на сегодняшний день системой римских цифр […]
Ладно, допустим, этот говорящий на латыни и завернутый в тогу патриций появляется в нашем офисе и делает заказ на сайт, который за бабло переводит одни закорючки в другие. Мы его выполняем как вы предлагаете, корректно (на нашей стороне всё работает, да) — и заказчик выходит на IPO.
Первый (ок, второй) его клиент сделает что, угадайте? Правильно, купив современный билет на бой гладиаторов в октагоне^W Колизее — пойдет проверять номер гейта.
О какой вообще современной общепринятой системе вы говорите? Общепринятой где именно? В Кащенко?

skovoroad
05.12.2025 13:24Очень познавательно. Хотя, разумеется, вполне себе существует более римская, чем надпись над воротами Колизея, нотация: это нотация, которой пользуется девяносто девять и девять процентов населения планеты и называет её "римской". Потому что значение слова, даже слова "римская", определяют не энциклопедии и не учебники, а словоупотребление. Но вопрос не в этом.
Скажите, а если не включать вот эту истерическую и отчасти оскорбительную клоунаду с "вы издеваетесь", "кащенко" и прочими элоквенционными экзерсисами, а просто сообщить любознательной публике, что нормализованная в современном мире римская система имела несколько другую форму в древности, то кто от этого пострадает? Почему бы не прийти к уважительному диалогу с публикой? Ну, возможно, будет чуть меньше плюсиков и минусиков, но, поверьте, оно того стоит.

cupraer
05.12.2025 13:24поверьте, оно того стоит
Во-первых, не сто́ит. Не на этом форуме уж точно. Во-вторых, я с этого и начал, ровно как вы завещаете: сообщил любознательной публике. Знаете, что сделала любознательная публика в ответ? — Правильно, вместо того, чтобы сказать спасибо, — бросилась хором мне доказывать, что «более лучшая римская нотация, которой пользуется (?) 99.9% (???) населения — это какая-то другая нотация». Так вот, более лучшей римской нотации не существует. Написание «LIIII» устарело, но не было отменено. Спросите любого носителя латыни.
Как вы полагаете, почему у меня в гистах лежало готовое решение, в котором учтены варианты с четырьмя появлениями минорной базы этой системы счисления? — Да просто я более-менее образованный человек (ну или придется предположить, что столкнувшись с этой задачей я пошел и освежил свои знания на предмет римской записи). Если по вашим оценкам — образованных людей на этой планете осталось менее десятой доли процента — ну что ж… Пора в петлю, стало быть.

tenzink
05.12.2025 13:24Вот вам вполне современные часы. И там используется IIII для четвёрки


cupraer
05.12.2025 13:24Это запрещенный приём, я надеялся переспорить свидетелей «современной римской нумерологии» словами.

aborouhin
05.12.2025 13:24Прочтение статьи "Roman numerals" в английской википедии открывает ещё больше ужасающих фактов :) Ладно IIII, но бывает ещё и IIX... И, прости Господи, MS Excel в зависимости от аргумента функции ROMAN() может представить число 499 как CDXCIX, LDVLIV, XDIX, VDIV или ID (это уже, конечно, IMHO, перебор :)

cupraer
05.12.2025 13:24Ну вот нормально же общались, зачем было это на ночь глядя показывать-то? Как теперь заснуть без кошмаров?

cupraer
05.12.2025 13:24MS Excel в зависимости от аргумента функции ROMAN()
Я уже давно уверен, что еще со времён Спольски в экселе есть пасхально-яичная функция, которая, будучи вызвана с особенными параметрами, инициирует конец света.

demitel
05.12.2025 13:24Оригинально. Нужно было пойти дальше, и 9 написать как VIIII, а не IX

tenzink
05.12.2025 13:24Так делают для читабельности. IIII в перевёрнутом виде на позиции 4 часа читается хорошо, а IV будет смотреться плохо. IX на позиции 9 часов читается нормально и менее громоздко чем VIIII

mvv-rus
05.12.2025 13:24Но если заказчик живет в наше время...
В наше время бывает всякое. Например в нумерации корпусов геманской армии ещё в 40-е годы использовалось XXXX вместо XL. И если заказчик - военный историк, то его эта ваша правильность его ни разу не порадует.
Я ни разу не историк - так, книжки немного читаю. Но когда я в неком тексте встретил в описании боев в Крыму в 1941 XLIV горный корпус, то тормознулся и долго думал - что это такое? Потом сообразил, что это - тот самый XXXXIX корпус, название которого в описании боевых действий в полосе Южного фронта РККА я встречал неоднократно.

Andrey_Solomatin
05.12.2025 13:24@spec numeral(pos_integer()) :: String.t()Этот код принимает строку и возвращает число?

Andrey_Solomatin
05.12.2025 13:24Не существует более насквозь римской нотации для чисел,
И декабрь это 10 месяц в году потому, что у него римское слово десять в названии.
То что в примере использовалась другая римская нотация, чем та что была при постройке Колизея к задаче рефакторинга отношения не имеет.

cupraer
05.12.2025 13:24другая римская нотация
Угу. Конечно, конечно. Я тоже, когда рефакторю код, всегда аккуратно переношу все ошибки в новую версию.
Какая нахрен «другая римская нотация»? Что вы несете вообще? Написание «IIII» для четырех вышло из употребления, а не было отменено. Оно остаётся на 100% легитимным. Любой носитель латыни вам подтвердит.

Andrey_Solomatin
05.12.2025 13:24Угу. Конечно, конечно. Я тоже, когда рефакторю код, всегда аккуратно переношу все ошибки в новую версию
Это как раз правильный рефакторинг. Код делает тот-же самое. В этом подходе исправление будет уже следующий шаг. Так исправление будет легче откатить если это фича.
Какая нахрен «другая римская нотация»? Что вы несете вообще? Написание «IIII» для четырех вышло из употребления, а не было отменено. Оно остаётся на 100% легитимным. Любой носитель латыни вам подтвердит.
Нотация которая использует текущее употребление. Ну или нотация которую выбрали для этой задачи. Ну или упрощённая нотация, которую иногда используют в алгоримических задачах.

RichardMerlock
05.12.2025 13:24Вот вы тут срётесь всласть, а проблема решается "подшивкой" к ТЗ выдерки из стандарта представления чисел. Всё! Административная проблема же.

Andrey_Solomatin
05.12.2025 13:24Мы вообще должны дискутировать про рефакторинг, а не уточнять требование к задаче поставленной не нам.

cupraer
05.12.2025 13:24Если вы кому-то что-то должны, то я — нет. Требования я озвучил в какой-то из соседних веток. В книге инфоцыгана нет никакого ТЗ, лучше чем «перевести риские цифры в десятичную запись». Точка.

cupraer
05.12.2025 13:24Нет :)
Я скопипастил это из своих гистов, не вчитываясь. Этот код делает обратную трансляцию, прямая еще проще.

Andrey_Solomatin
05.12.2025 13:24Ваш код мне не нравится. Невнятные имена для функций: fours numeral. Много анонимных функций. Логика перемешанна с низкоуровневыми операциями.

Dhwtj
05.12.2025 13:24Издеваешься?
Это вообще другая задача: переводить арабские в римские
Write only code
Y-комбинатор головного мозга
Регекс
Бинарный паттерн-матчинг

cupraer
05.12.2025 13:24А что не так с бинарным паттерн-матчингом? Одна из самых крутых и часто используемых возможностей эрланга, протоколы парсить, например, никакой сраный протобуф не нужен.

ImagineTables
05.12.2025 13:24Как я слышал, позже это было связано с суевериями торгового люда, который не любил вычитать. Слышал я это от гида на венецианской площади перед Дворцом дожей, так как раз такие часики висят. (За что купил, за то продаю).
P.S. Когда я работал над калькулятором, умевшим в римские числа и конверсии, мы решили на вход принимать всё, что можно однозначно интерпретировать, а вот выводить только по правилам. По-моему, глупо делать иначе.

cupraer
05.12.2025 13:24Найти резкое фото часов на Дворце Дожей мне с полпинка не удалось, но всё равно, скорее всего, байка про суеверия — миф, городская легенда. Во-первых, с торговцами архитекторы не советуются (но это бы и ладно). Главное — из-за суеверия пришлось бы 9 записывать как
VIIII, а это уже вряд ли поместится. Придираться закончил, разрешите идти!на вход принимать всё, что можно однозначно интерпретировать, а вот выводить только по правилам
Разумеется. Хороший разработчик сделает именно так. Разработчик экселя, впрочем, с радостью примет и интерпретирует и Песню Песней на арамейском.

ImagineTables
05.12.2025 13:24Я делал фотки, но они на другом диске в другом городе. Поэтому прошу извинить, что ссылаюсь на чужие. И как оно там расположено, я уже забыл. Например, слова «северная сторона» в описании сейчас для меня, как говорится, don't ring any bells.
Собственно, вот: https://en.wikipedia.org/wiki/St_Mark's_Clocktower. Там девятка как раз
VIIII. По времени — ранее Возрождение. И ещё другая, других часов: https://commons.wikimedia.org/wiki/File:Courtyard_of_the_Doge's_Palace_(Venice)_-_North_Side_Clock.JPG. На этой уже смешение стилей: девятка современная, четвёркаIIII.Гид говорила, что венецианские купцы всё в таком ключе оформляли. А архитекторы просто делали то, за что им платили. Платили те, у кого были деньги (то есть, купцы). Заказывали ли купцы суеверия — это, конечно, неизвестно, но в наши дни ведь заказывают отсутствие 13-ого и 4-ого этажей. Руководствуясь сложным балансом суеверий и рацио (нежеланием отпугивать клиентов).

Kelbon
05.12.2025 13:24наконец понял откуда бывший коллега взял это. Писать "классы" которые по сути просто функции и их методы надо вызвать в правильном порядке, чтобы получить результат, а поля это аргументы функции
Оказывается он чистого кода начитался

AdrianoVisoccini
05.12.2025 13:24Мне особенно понравилась его гипотетическая шутка на больную тему о том, как код стал настолько плохим, что пришлось создать отдельный рабочий процесс для его переписывани
Читал этот эпос недавно и помню на моменте где он рассказывал как "некоторая компания" название которой он конечно не может озвучить, даже обанкротилась из-за грязного кода. И вот прям читаешь ты это и такой


Format-X22
05.12.2025 13:24А чего бы нет? Сложнее понять - сложнее внедрять фичи - больше времени на разработку - больше расходы - расходы стали больше доходов. Не такая и редкость.

AdrianoVisoccini
05.12.2025 13:24Не такая и редкость
можешь привести хотя бы один точно подтвержденный случай?

DenSigma
05.12.2025 13:24Код Дяди Боба я понял сразу. Код автора этой писанины (кстати, кто он?) мне пришлось разбирать построчно.

cupraer
05.12.2025 13:24Код Дяди Боба я понял сразу.
Это стокгольмский синдром.

Andrey_Solomatin
05.12.2025 13:24"Что делает этот код" у Дяди Боба написанно прямо в коде единственного публичного метода в классе. Чтобы понять, что глобально происходит достаточно 4х строк. Это крайне полезно, если ты ищешь где проблема в чужом коде.

cupraer
05.12.2025 13:24прямо в коде единственного публичного метода в классе
Во-первых, у него еще за каким-то хреном конструктор публичный, что вообще ни в какие вопроса не лезет. Во-вторых, этот метод назван по-идиотски и закопан в недра класса, почти ровно в середину. Пока глазами отыщешь — проще самому набросать решение. В-третьих, вот тело метода:
return new FromRoman2(roman).doConversion()Что делает этот код? Создаёт инстанс какого-то класса с эзотерическим названием (зачем? — это отложим на разобраться потом, наверное, зачем-то надо). Потом вызывает на инстансе метод
doConversion. Публичный статический методconvertвызывает приватный метод на инстансеdoConversion. Конечно, всё полностью прояснилось, какие тут могут быть вопросы.
Andrey_Solomatin
05.12.2025 13:24Это техническая обёртка, которая прокликивается за пару секунд, не включая мозг. Согласен, что лучше бы попроще сделать.

cupraer
05.12.2025 13:24Вы окончательно запутались в собственных недомолвках, пытаясь защитить человека, который за всю жизнь не написал ни единой строки внятного кода, и принёс своими безумными фантазиями бездну вреда. Погуглите публичный репозиторий Мартина, чиста поржать.

Andrey_Solomatin
05.12.2025 13:24Я отстаиваю свою точку зрения, сформированную под влиянием разных людей.
С публичными репозиториями у Raymond Hettinger всё ок.
Главное достоинство Мартина, в том что он провоцирует споры про важные вещи. Не важно как вы к нему относитесь, но вы сейчас здесь.

cupraer
05.12.2025 13:24он провоцирует споры про важные вещи. Не важно как вы к нему относитесь, но вы сейчас здесь
Это очень достойный аргумент, с которым я согласен.
Для протокола все-таки вынужден добавить, что я ввязываюсь в такие дискуссии всегда, когда моё мнение идет вразрез с медианой общественного. Просто энтропию поддерживаю на должном уровне. Потому что если никто не озвучит тезисы, противоположные тезисам толпы, то рано или поздно засовывать язык в жопу — войдет в привычку, а потом — распнут еще какого-нибудь назаретянина.
Я этому в меру сил сопротивляюсь. И Мартин тут ни при чем: просто именно ему удалось по неведомым причинам продать свой воздух и развести на ровном месте хайп. Я так же точно ввязался бы в любую тему, где хвалят статическую типизацию, которая такой же воздух, только в профиль.
Кто такой Хеттингер, я не знаю. Я про вот этот гитхаб, которого Мартин даже не стесняется: https://github.com/unclebob

CrazyOpossum
05.12.2025 13:24Я так же точно ввязался бы в любую тему, где хвалят статическую типизацию, которая такой же воздух, только в профиль.
Эм, а что с ней не так? Она конечно не решает все проблемы, но пользы от неё больше чем вреда.

cupraer
05.12.2025 13:24пользы от неё больше чем вреда
От контекста (типа проекта) очень сильно зависит. Кроме того, польза «обычной» типизации — без завтипов и пруверов — очень сильно преувеличена. Она даже off-by-one отловить не может же.

CrazyOpossum
05.12.2025 13:24Статическую со строгой (сильной) не путаете? Статическая просто не даёт менять тип в той же области видимости. А строгая - мощная штука, но только если типы математики писали, а не "ээ, ну строка, ну число, ну число побольше"

Kahelman
05.12.2025 13:24Не провоцирует. Он фактически инфоцыган. Читайте
John Ousterhout: A Philosophy of Software Design, 2nd Edition
Тав вам «реально авторитетный пацан за Мартина разъяснит»
Единственная польза от чистого кода это ткнуть хомячков мордой и заставить писать боле-менее нормальный код, поскольку они вообще не имеют представление о разбиении на функции и прочих базовых вещах. Объяснять с сотый раз невозможно, поэтому можно взять книгу и сказать на вопрос почему? Ответить: потому …
На большее он не годится.

novoselov
05.12.2025 13:24Все 3 варианта говно, особенно первая версия написаная ИИ:
запрещает последовательность
roman.contains("MCM")которая является валидным числом 1900неправильно обрабатываются невалидные
IXV, XCL, CMC, CMD, MCMCпревращая их в число14и подобныесчитает
OGNF94валидным числом
По идее там достаточно описать только
I → 1, V → 5, X → 10, ..., а все правила парсинга выводятся через функции. Все этиcombinations,repetitionsи прочее дичь из-за которой получается неподдерживаемый код

Andrey_Solomatin
05.12.2025 13:24Передавать аргументы и возвращать значения идея хорошая. Это действительно упрощает понимание кода.
А вот лепить всю логику в один большой метод мне не нравится, это усложняет чтение. С декомпозицией я могу ревьюить маленькими шагами: прочитал главную функцию: ок логика устраивает, пошел попил чайку. Глянул что за шаги по проверки валидности синтаксиса, раз-два и готово (там всего две строки). Тривиальные функции, делающие одну вещь. Читая их мне даже синтаксис языка не недо знать, чтобы найти логические ошибки.

cupraer
05.12.2025 13:24усложняет чтение
Господи, это тривиальный код, который разработчик чуть умнее табуретки должен на ассемблере за пять минут разобрать, какое еще «усложняет» к дедам? Не, я понимаю, когда в недрах криптографии на эллиптических кривых удается выкусить почти независимый кусок арифметики и отчудить в функцию с понятным названием. Но тут?!

victor_1212
05.12.2025 13:24удобство review вообще слабый аргумент, типа хирург режет лишнее, чтобы лучше видно было, если написано так по делу, проблем с пониманием быть не должно

mvv-rus
05.12.2025 13:24А вот лепить всю логику в один большой метод мне не нравится, это усложняет чтение.
А мне - в зависимости от того, как слеплено. Если слеплено в виде простой последовательности операций, без возвратов назад и без лишних зависимостей по локальным переменным - ни разу не усложняет, а наоборот. Разбивать мысленно длинный код на блоки я обучен, особенно - если приметы для границ, комментарии например(содержание даже не важно), и меня это не напрягает, а вот лазать по коду в поисках где этот чертов метод (который был вызван здесь, с его замечательным говорящим названием) - тоже обучен, конечно, но меня это напрягает. А когда этот метод ещё и общается с другими методами через приватные поля, да ещё без какой-либо явной группировки этих полей по назначению - напрягает ещё сильней.
И, в очередной раз напоминаю: сложность чтения - понятие субъективное, короче - как научили. И кого-то наверное научили и на книгах Мартина.

Andrey_Solomatin
05.12.2025 13:24Если слеплено в виде простой последовательности операций, без возвратов назад и без лишних зависимостей по локальным переменным - ни разу не усложняет, а наоборот.
К сожалению такой код встречается не так часто как бы хотелось. После пару-тройку изменений, которые делаются в спешке или людьми других взглядов, код имеет тенденцию превращаться в лапшу.

d_ilyich
05.12.2025 13:24Я когда на литкоде решал задачу "13. Roman to Integer", сделал через суффиксы. Медленно, но коротко и понятно.

apcs660
05.12.2025 13:24посмотрел в архив, нашел 2 решения - даже не помню как делал эту задачу.
Один вариант компактнее:
public int romanToInt(String s) { Map<Character, Integer> values = new HashMap<>(); values.put('I', 1); values.put('V', 5); values.put('X', 10); values.put('L', 50); values.put('C', 100); values.put('D', 500); values.put('M', 1000); int sum = 0; int length = s.length(); int currVal; int nextVal; for (int i = 0; i < length - 1; i++) { currVal = values.get(s.charAt(i)); nextVal = values.get(s.charAt(i + 1)); if (currVal < nextVal) { sum -= currVal; } else { sum += currVal; } } sum += values.get(s.charAt(length - 1)); return sum; } public int romanToInt2(String s) { int sum=0; int prev=0; for(char c:s.toCharArray()) { switch(c) { case 'I': sum+=1;prev=1;break; case 'V': sum+=(prev==1)?3:5;prev=5;break; case 'X': sum+=(prev==1)?8:10;prev=10;break; case 'L': sum+=(prev==10)?30:50;prev=50;break; case 'C': sum+=(prev==10)?80:100;prev=100;break; case 'D': sum+=(prev==100)?300:500;prev=500;break; case 'M': sum+=(prev==100)?800:1000;prev=1000;break; } } return sum; }
Dhwtj
05.12.2025 13:24Первый вариант отличный. Второй плохо читается, но компактный.
Парсинг дело специфическое, без подготовки туда лучше не лезть. И Боб и автор понадеялись что здравого ума хватит без алгоритмического рефа. Но это не так.

apcs660
05.12.2025 13:24Валидации в этих методах нет, вроде у римских цифр правила есть что после чего можно.
Парсинг любят все. На собесе в Яндексе обе задачки были про парсинг. Причем я точно решал задачу на литкоде но тупил и тупил, в итоге конечно сделал но спинным мозгом.
Прошел у Яндекса контекст алгоритмический - только на последнем задании узнал про бот в телеге где можно тестовые кейсы вытащить, без него бы не прошел -последняя задачка меня заела. Для неопытных в алгоритмических задачах как я, яндексовский контест хорошая разминка. Намного лучше литкода, более жесткий на mem/time constraints - на литкоде можно нарушать и читить.
Знакомый пошел тоже в Яндекс "пособесится" - затишье, что то надо поделать - дали ту же задачу на live coding что и мне, позже решения сравнили, обсудили.
Яндексу большое спасибо за прогрев - если не будет результата то сам процесс полезен разогреться; финальные интервью тоже познавательные, начинаешь понимать как все устроено у них (скажу сразу - непривычно для меня).

megadrugo2009
05.12.2025 13:24Критикуешь - предлагай.
Почему-то у всех критиков Чистого кода и Чистой архитектуры нет сопоставимой книги, где бы они предложили что-то лучше, для поддержания больших проектов.
Сейчас в эпоху микросервисов проблема не столь явная, так как сами по себе микросервисы физически делят комплекс приложения на "модули", а когда вышла первая книга, этого не понимали.

cupraer
05.12.2025 13:24Учиться разработке и архитектуре по книгам — всё равно, что заниматься сексом по переписке: вроде бы всё сделал, как надо, а результат — как будто дров на зиму наколол.

Dhwtj
05.12.2025 13:24Список срач тем
ООП
Микросервисы
Чистый код
Пора добавить тег

Andrey_Solomatin
05.12.2025 13:24Солид забыли. И серию язык плохой/скоро умрёт.

Dhwtj
05.12.2025 13:24язык плохой/скоро умрёт.
PHP?
;)
Ещё: язык хороший, скоро вырастет
Rust, например. А он не растёт, гад

Andrey_Solomatin
05.12.2025 13:24Java хоронят каждый год. Питон ругают.
Про го давно не видел, может дженерики завезли?
Rust, например. А он не растёт, гад
Rust неплохо решает проблемы зависимостей для Питона. На нем пишут модули для которых нет зависимостей.

VladimirFarshatov
05.12.2025 13:24Спасибо автору за повторный разбор. Не всё то золото, что блестит. А самое забавное тут то, что в каких-то проектах это применимо, а где-то нет вовсе. Особенно в т.н. "bigdata", где парсеры с "чистым кодом" работают сутками.

Andrey_Solomatin
05.12.2025 13:24Чистый код он действительно не про производительность. На моей практике правда оптимизации делались через алгоритмы. Заинлайнить всё в одну функцию не поможет сильно, да и компиляторы это сами делают в некоторых языках.

VladimirFarshatov
05.12.2025 13:24В предыдущей статье показывал как растет конгитивная сложность при переработке в чистый код по Холстеду. По его книжке вполне возможно реализовать програмку, считающую когнитивную сложность и не холиварить прав или не прав тот или иной автор.

mvv-rus
05.12.2025 13:24Программку-то написать можно, наверное, и она там что-тодаже посчитает, цифру выдаст. Но вопрос в том, какое отношение будет иметь эта посчитанная цифра к воспринимаемой конкретной человеком сложности конкретного кода.
Это я ещё раз повторяю мысль, что сложность восприятия человеком кода является субъективной характеристикой.

Andrey_Solomatin
05.12.2025 13:24Программку писать не надо, если метрика годная, то уже должна быть. Для Питона есть.
Это я ещё раз повторяю мысль, что сложность восприятия человеком кода является субъективной характеристикой.
Вполне можно. Вложенные условия будут сложней плоских условий. Одна функция на 1000 строк сложнее десяти по сто. Ну и по ней не читаемость оценивают, а потенциально проблемные места: метод или функцию.

jurikolo
05.12.2025 13:24Читаешь такой обзор на книгу и задаёшься вопросом - а как после этого можно книги читать для обучения? Когда ты уже специалист и можешь критически относиться к написанному, то вопросов нет, читай, критикуй, что-то полезное подчёрпывай. А если джун почитает эту книгу, то он скорее ухудшит свой код, нежели улучшит и будет думать в неправильном направлении.

Andrey_Solomatin
05.12.2025 13:24Автор в критикует пример, но не всю книгу.
Если смотреть в общем, то я согласен с большинством взглядов Дяди Боба.
Я видел код джунов, читайте, не бойтесь. Читайте разное. Пробуйте, анализируйте результат.

Format-X22
05.12.2025 13:24Он коверкает определение чистой функции, по сути, применяя её только с позиции публичных методов.
Ну вообще по ООП в идеале у тебя во вне смотрят публичные методы и свойства, а то что происходит внутри - оно на то и внутри. Просто не надо считать метод функцией, даже если в вашем языке программирования префикс fn, func или что-то подобное. Функция в объекте привязана к данным в объекте. А вот если она ходит куда-то во вне - вот это уже проблема и грязь, в контексте чистоты функций. Функциональное программирование и объекто-ориентированное это разные вещи. Конечно никто не мешает делать все методы у класса чистыми функциями, но тогда это модуль или неймспейс, а не класс в его основном понимании.

Format-X22
05.12.2025 13:24Второй — это иммутабельность, то есть отсутствие повторных присваиваний переменных и изменений существующих значений.
А потом чего это у нас код тормозит, надо серверов то добавить, да и базу подтюнить. И клиент как-то много памяти ест, но ничего, пользователь планочку памяти всегда может докупить.
Ох, как часто я встречал тормоза и неадекватное потребление памяти из-за того что выбрали иммутабельность как правило. И нет, не только потому что его готовить не умеют. Компьютер он абстрактно работать не умеет, там на дне нули и единицы и если ты их оставляешь лежать потому что иммутабельность - они так и лежат. Сборщик мусора, возможно их очистит. Возможно. Съев пачку тактов процессора на анализ. Впрочем, засорить можно и на том же Расте.
Если вы можете - никогда не используете иммутабельность, пожалейте других людей.

CrazyOpossum
05.12.2025 13:24Если сервер жуёт такие объёмы памяти, что страдает из-за иммутабельности - проблема не в ней. Собственно, базы данных вам для чего? У всего свои приложения, в Хаскелле повальная иммутабельность, но никто не смотрит косо на ST за нарушения, всё работает на своём месте.

Format-X22
05.12.2025 13:24Хаскелл в продакшене встречается на столько редко что пример не подходящий. И база данных не для всего решение. Потому что результат из базы тоже можно начать весело обрабатывать, вот половина проблем из моего опыта оптимизации тормозов - именно на этом этапе.

Andrey_Solomatin
05.12.2025 13:24Если вы можете - никогда не используете иммутабельность, пожалейте других людей.
Кажется, что вы применили подход не к месту, получили проблем от этого и поставили крест на технологии. Не надо так.

Format-X22
05.12.2025 13:24Наоборот получалось обычно - была проблема с производительностью, я заходил, а там помимо всего прочего не редко массивы с пачкой данных копировались вместо модификаций, цепочки вызовов над массивами с преобразованиями, порождающими новые массивы и прочее такое. И вот когда в один проход переписывалось или с мутабельностью - скорость росла вот прямо по метрикам было видно. А кое-какой код вообще не работал в моменте, потому что бывает что полтора гигабайта json в приложение на nodejs заходит за раз без потока и там все способы работы привычные уже не способны существовать в принципе. Ну и конечно порождение квадратичных алгоритмов. В общем я видел некоторое волшебство и, конечно же, концепт иммутабельности решает множество проблем, меньше багов, но вот когда возводится идея в абсолют - потом приходится ходить и переписывать код.

DedAnton
05.12.2025 13:24А что если вот так попробовать?
int ConvertToArabic(string romanNumber, int? lastDigit = null) { var (digit, tail) = romanNumber switch { [] => (0, ""), ['I', 'X', 'I', ..] => throw new Exception("Invalid subtractive sequence: IXI"), ['I', 'X', 'V', ..] => throw new Exception("Invalid subtractive sequence: IXV"), ['I', 'X', ..] => (9, romanNumber[2..]), ['I', 'V', 'I', ..] => throw new Exception("Invalid subtractive sequence: IVI"), ['I', 'V', ..] => (4, romanNumber[2..]), ['I', 'I', 'I', 'I', ..] => throw new Exception("Too many consecutive I"), ['I', 'I', 'I', ..] => (3, romanNumber[3..]), ['I', 'I', ..] => (2, romanNumber[2..]), ['I', ..] => (1, romanNumber[1..]), ['V', 'I', 'V', ..] => throw new Exception("Invalid sequence: VIV"), ['V', 'V', ..] => throw new Exception("Too many consecutive V"), ['V', ..] => (5, romanNumber[1..]), ['X', 'C', 'X', ..] => throw new Exception("Invalid subtractive sequence: XCX"), ['X', 'C', 'L', ..] => throw new Exception("Invalid subtractive sequence: XCL"), ['X', 'C', ..] => (90, romanNumber[2..]), ['X', 'L', 'X', ..] => throw new Exception("Invalid subtractive sequence: XLX"), ['X', 'L', ..] => (40, romanNumber[2..]), ['X', 'X', 'X', 'X', ..] => throw new Exception("Too many consecutive X"), ['X', 'X', 'X', ..] => (30, romanNumber[3..]), ['X', 'X', ..] => (20, romanNumber[2..]), ['X', ..] => (10, romanNumber[1..]), ['L', 'X', 'L', ..] => throw new Exception("Invalid sequence: LXL"), ['L', 'L', ..] => throw new Exception("Too many consecutive L"), ['L', ..] => (50, romanNumber[1..]), ['C', 'M', 'C', ..] => throw new Exception("Invalid sequence: CMC"), ['C', 'M', 'D', ..] => throw new Exception("Invalid sequence: CMD"), ['C', 'M', ..] => (900, romanNumber[2..]), ['C', 'D', 'C', ..] => throw new Exception("Invalid sequence: CDC"), ['C', 'D', ..] => (400, romanNumber[2..]), ['C', 'C', 'C', 'C', ..] => throw new Exception("Too many consecutive C"), ['C', 'C', 'C', ..] => (300, romanNumber[3..]), ['C', 'C', ..] => (200, romanNumber[2..]), ['C', ..] => (100, romanNumber[1..]), ['D', 'C', 'D', ..] => throw new Exception("Invalid sequence: DCD"), ['D', 'D', ..] => throw new Exception("Too many consecutive D"), ['D', ..] => (500, romanNumber[1..]), ['M', 'M', 'M', 'M', ..] => throw new Exception("Too many consecutive M"), ['M', 'M', 'M', ..] => (3000, romanNumber[3..]), ['M', 'M', ..] => (2000, romanNumber[2..]), ['M', ..] => (1000, romanNumber[1..]), _ => throw new Exception($"Invalid Roman number sequence starting with '{romanNumber[0]}'") }; if (lastDigit != null && digit >= lastDigit) { throw new Exception($"Invalid order: {digit} after {lastDigit}"); } if (digit == 0) { return 0; } return digit + ConvertToArabic(tail, digit); }
Gromilo
Перевод. Но как же я согласен с автором. Код должен быть понятным.
У меня ещё с первой книги (стоит в шкафу, если что) вызывало недоумение использование общего состояния и сверхмалых функций.
Когда я начал использовать их, оказалось, что читать код нисколько не проще, а даже сложнее. То что могло быть просто быть просто строчкой кода, теперь вызов функции. И я не знаю, что она там меняет. В итоге, только полное чтение всего кода.
Идеи Мартина не прошли проверки временем и практикой.
А вот чистые функции - это сила. Они снижают нагрузку на мозг, потому что функцию можно заменить результатом вычисления от того, что ты видишь в вызове
Очень хорошая статья на тему Функциональное программирование — это не то, что нам рассказывают.
И вообще, Хватит писать «чистый» код. Пора писать понятный код