dimhold.by
← Статьи

15 лет после Simple Made Easy: в 2013 году ни одна из моих 102 локальных переменных не была final, сейчас в живом коде 91.6% это const. Я не выбирал ни того ни другого

10 мая 2013 года я завёл репозиторий, чтобы перевести доклад Рича Хикки на русский. Доклад назывался Clojure Concurrency. План начинался с субтитров и кончался русской озвучкой. В README до сих пор лежит таблица готовности, которую я в него вписал: английские субтитры 5%, перевод на русский 0%, озвучка 0%. До 99% дошли 2 строки. Это те, где я восстанавливал его артефакты: слайды плюс исходник программы, которую он показывал. Последний раз я трогал этот файл 23 октября 2013 года.

Та неделя записана и в других репозиториях. 11 мая я добавил фоновый демон в java-сервер, над которым тогда работал. 15 мая в 21:09 закоммитил в тот же сервер правку с заголовком “Add removeClientHandler and add synchronized collections”.

То есть неделю я переводил доклад про конкурентность и писал ровно то, против чего он направлен. Доклад со списком это другой доклад, Simple Made Easy, Strange Loop, сентябрь того же года. Его словарём я пользуюсь с 2013 года и ни разу не прогнал его список по своему коду.

Что дал коммит 15 мая

В Simple Made Easy на 00:28:50 есть слайд с заголовком What’s in your Toolkit. В одну колонку он кладёт конструкции, которые сплетают, в другую замены попроще. Со стороны сложности: состояние, объекты, методы, наследование, switch, переменные, циклы, ORM и условия. На слайде Debugging, между 00:15:32 и 00:17:14, он даёт имя тому, что дают тесты. “All of our guardrails will have failed us.”

Тот сервер был командным репозиторием. Класс, о котором речь, не мой. Это был синглтон, державший 4 изменяемые коллекции. Каждый метод, менявший любую из них, делал это внутри synchronized. Геттеры отдавали живое представление вместо копии.

BenchDemon мой, добавлен 11 мая, 63 строки, под всеми стоит моё имя. Его работа обходит одно из таких представлений, bench.addAll(Beeper.getInstance().getBench().keySet()), в своём потоке и без замка. Через 4 дня мой коммит поменял другую сторону. Он вынул блоки synchronized и поставил на их место Collections.synchronizedSet и Hashtable. Каждая отдельная операция стала атомарной. Мой обход остался ровно таким же несинхронизированным.

Я собрал обе формы заново и померил на javac и java 21.0.12, на 4 ядрах. Пишущий поток добавляет и удаляет. Читающий поток обходит коллекцию 50000 раз. Считаю чтения, которые упали. Пауза это то, сколько пишущий ждёт между мутациями. В каждой ячейке 5 прогонов.

пауза записидо 15 маяпосле 15 маякопия под замком
нетот 80.1 до 88.4%от 79.7 до 90.7%0
10 мксот 0.60 до 0.82%от 0.60 до 0.99%0
1 мсот 23 до 25 паденийот 23 до 25 падений0

Каждое падение это ConcurrentModificationException. Верхняя строка говорит больше про планировщик, чем про мой код. Она расходится на 8-11 пунктов между прогонами одного и того же класс-файла. На 1 мс, при самой медленной записи, какую я мерил, обе формы падают около 24 раз на 50000 чтений и между собой не различаются. Эта запись всё равно куда занятее, чем тот сервер когда-либо был, потому что демон спит между проходами 1000 мс. Копия под замком не упала ни в одном из 15 замеров.

beeper-server, май 2013 bench = HashMap synchronized (bench) addUserToBench() removeUserFromBench() getBench() отдаёт живой вид BenchDemon, свой поток processBench() addAll(.keySet()) без замка упавших чтений на 50000, писатель раз в 1 мс до 15 мая от 23 до 25 после 15 мая от 23 до 25 копия под замком 0
Где стоит замок и куда идёт чтение. Оба метода, меняющих карту, закрыты. Демон, который её обходит, нет, потому что геттер отдаёт живой вид. Падения на 50000 чтений при паузе писателя 1 мс, 5 прогонов.

В сервере, каким он был, ничего этого всё равно случиться не могло. Main запускает сетевой сервер и больше ничего. new BenchDemon встречается только в тестах, а они зовут метод напрямую, без потока. Обе группы netty собраны с 1 потоком каждая, поэтому все обработчики шли по одному event loop. Защиту от конкурентности я написал для программы, обработчики которой никогда не шли параллельно. И так и не запустил ту часть, которой защита понадобилась бы.

Список, сегодня

В 2026 году набирает здесь агент, а правила, по которым он это делает, пишу я. Значит корпус записывает мои требования. Моих привычек в нём нет.

Строки его таблицы я считал через TypeScript compiler API, 5.9.3 на node 22.23.1. Регулярка не отличит for внутри строки от for в коде. Для 2013 года взял javalang 0.13.0 на python 3.12.3. Всё снято на коммите d287db2, git 2.43.0. Старые репозитории командные, поэтому сначала атрибуция: 42 java-файла и 2268 строк, из них blame отдаёт мне 51.4%. В счёт идут только файлы, где моего больше половины. Остаётся 24 файла и 1200 строк.

java, 2013, 1200 строкts и js, 2026, 15632 строки
объекты23 класса, 107 методов0 классов, 0 методов класса
наследование4 extends0
switch11
переменные102 локальные, из них final 03071 const против 621 let

Последняя ячейка это 83.2% по всему корпусу. В тех 2303 строках, которые я держу и переписываю, счёт 373 против 34, то есть 91.6%. Рядом с 0 final в 2013 году это выглядит как 13 лет следования его совету. Было не так. Правила про const я не писал. Линтера, который бы его требовал, здесь нет, а tsc --noEmit на это вообще не смотрит. Он берётся оттуда, что так пишут на TypeScript в 2026 году. И оттуда, что пишу я мелкие скрипты на Node, где классу просто неоткуда взяться. Эти строки вышли чистыми, решения не принимал никто.

Строка про ORM показывает 0 аннотаций. Зато pom.xml тянет spring-data-jpa вместе с hibernate-entitymanager, а в XML спринга подняты фабрика менеджера сущностей и транзакционный менеджер JPA. Конфигурацию написали, а разметку так и не написал никто.

Единственная строка, которая всё ещё задаёт вопрос

Условия это строка, где решение встаёт на каждой строчке кода. Взамен он предлагает правила. Код 2026 года я разделил надвое. То, что держу и переписываю, это 20 файлов. Скрипты замеров, которые прогоняю один раз и бросаю, это 89.

В живом коде 16.7 ветвления на 100 строк. В выбрасываемых скриптах 14.6. Медианы по файлам говорят то же, 17.0 против 15.0, значит сумму не тащит один жирный файл. На единственной строке его таблицы, которая всё ещё чего-то мне стоит, плотнее оказался тот код, которому предстоит выжить, то есть наоборот. Ветвление здесь это if, тернарник, switch и короткое замыкание, взятые из синтаксического дерева. Пару дней назад я публиковал по этому же репозиторию 21.9, счётом по ключевым словам, по другому набору путей и вместе с циклами.

Мест, где я сделал по его совету, ровно 1. Попал я туда случайно. В src/lib/tells.ts лежат 19 механических проверок моего текста. 15 из них это записи в таблице данных, в каждой идентификатор и регулярка. В тех 53 строках 0 ветвлений. У файла 8.1 ветвления на 100 строк. Это меньше всех среди моих файлов от 100 строк, а следующий снизу 12.0.

Остальные 4 в таблицу не легли. Это 3 проверки тире плюс проверка сращивания слов дефисом. Каждой нужно то, чего в самой найденной строке нет, вроде длины предложения или языка, на котором она написана. Они живут в 2 функциях на 40 строк с 12 ветвлениями. Выходит 3.5 строки на правило внутри таблицы против 10 строк и 3 ветвлений на правило снаружи.

src/lib/tells.ts, 19 проверок данными 15 правил, 53 строк, 0 ветвлений руками 4 правила, 40 строк, 12 ветвлений строк на правило 3.5 10.0 те 4, что не влезли: 3 правила тире и правило сращивания
19 механических проверок моего текста, разделённые по способу записи. 15 это строки данных, каждая стоит 3.5 строки без единого ветвления. Те 4, что не влезли, стоят 10 строк и 3 ветвления каждое.

То, что они данные, не сделало их верными. Пару дней назад я опубликовал, как 6 из этих проверок сверили с тем, что обещает руководство. Разошлись 2, обе это строки таблицы. Сравнение слабое, потому что 5 из 6 сверенных были строками таблицы, но другого у меня нет. Проверки тире это 3 из тех 4, а руководство пересказывает их прозой в 5 документах помимо кода.

Чего я не проверял

Есть ли 1200 строк выборка хоть чего-нибудь. Считается ли курс по Clojure. Не считается: blame отдаёт мне все 850 строк, но репозиторий собран одним коммитом. Отделить мои решения от скелета, который выдал курс, он не может. Ещё 2 популяции кода 2026 года различаются не только сроком жизни, но и по существу. Скрипты замеров это длинные однократные молотилки данных, а живое это мелкие связующие модули. Плотность ветвлений я нарочно не поставил в колонку 2013 года, потому что java той поры тащит геттеры и сеттеры, которых в ts нет.

Эту строку я цитирую годами, а применил один раз, на 53 строках, не заметив. Перевод так и стоит на 0%. Машина, которая пишет эти эссе, доделала бы те субтитры за вечер. Отдельный вопрос: будет ли это применением его совета?