Статьи

Глубокие следы стека могут быть признаком хорошего качества кода

Термин «дырявые абстракции» существует уже давно. Чеканить его чаще всего приписывают Джоэлю Спольски, который написал об этом часто цитируемую статью . Теперь я наткнулся на другую интерпретацию негерметичной абстракции, измеряемой глубиной трассировки стека:

Таким образом, трассировки длинных стеков плохие, по мнению Geek & Poke. Я уже видел этот аргумент в блоге Игоря Полевого (он является создателем ActiveJDBC , Java-реализации популярного интерфейса запросов Ruby ActiveRecord ). Подобно тому, как аргументы Джоэла Спольски часто использовались для критики ORM, аргумент Игоря также использовался для сравнения ActiveJDBC с Hibernate . Я цитирую:

Кто-то может сказать: ну и что, почему я беспокоюсь о размере зависимостей, глубине трассировки стека и т. Д. Я думаю, что хороший разработчик должен заботиться об этих вещах. Чем толще каркас, тем сложнее он, чем больше памяти он выделяет, тем больше проблем может пойти не так.

Я полностью согласен с тем, что среда с определенной степенью сложности имеет тенденцию иметь более длинные трассировки стека. Итак, если мы проведем эти аксиомы через наши умственные процессоры Пролога :

  • если Hibernate является дырявой абстракцией, и
  • если Hibernate сложный, и
  • если сложность приводит к длинным трассам стека, то
  • вытекающие абстракции и трассировки длинных стеков коррелируют

Я бы не сказал, что есть формальная причинная связь. Но корреляция кажется логичной.

Но эти вещи не обязательно плохие. На самом деле, длинные трассировки стека могут быть хорошим знаком с точки зрения качества программного обеспечения. Это может означать, что внутренние компоненты программного обеспечения демонстрируют высокую степень сплоченности , высокую степень СУХОСТИ , что опять же означает, что существует небольшой риск для скрытых ошибок в вашей структуре. Помните, что высокая согласованность и высокая степень СУХОСТИ приводят к тому, что большая часть кода чрезвычайно актуальна для всей платформы, что опять же означает, что любая низкоуровневая ошибка в значительной степени взорвет всю структуру, поскольку она приведет к тому, что все будет происходить неправильно. Если вы выполняете разработку через тестирование , вы будете вознаграждены, сразу заметив, что ваша глупая ошибка не проходит 90% ваших тестовых случаев.

Пример из реального мира

Давайте проиллюстрируем это на примере jOOQ , так как мы уже сравниваем Hibernate и ActiveJDBC. Некоторые из самых длинных трасс стека в абстракции доступа к базе данных могут быть достигнуты путем установки точки прерывания на интерфейсе этой абстракции с JDBC. Например, при извлечении данных из ResultSet JDBC.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
Utils.getFromResultSet(ExecuteContext, Class<T>, int) line: 1945
Utils.getFromResultSet(ExecuteContext, Field<U>, int) line: 1938
CursorImpl$CursorIterator$CursorRecordInitialiser.setValue(AbstractRecord, Field<T>, int) line: 1464
CursorImpl$CursorIterator$CursorRecordInitialiser.operate(AbstractRecord) line: 1447
CursorImpl$CursorIterator$CursorRecordInitialiser.operate(Record) line: 1
RecordDelegate<R>.operate(RecordOperation<R,E>) line: 119
CursorImpl$CursorIterator.fetchOne() line: 1413
CursorImpl$CursorIterator.next() line: 1389
CursorImpl$CursorIterator.next() line: 1
CursorImpl<R>.fetch(int) line: 202
CursorImpl<R>.fetch() line: 176
SelectQueryImpl<R>(AbstractResultQuery<R>).execute(ExecuteContext, ExecuteListener) line: 274
SelectQueryImpl<R>(AbstractQuery).execute() line: 322
T_2698Record(UpdatableRecordImpl<R>).refresh(Field<?>...) line: 438
T_2698Record(UpdatableRecordImpl<R>).refresh() line: 428
H2Test.testH2T2698InsertRecordWithDefault() line: 931

По сравнению со следами стека ActiveJDBC, это немного больше, но все же меньше по сравнению с Hibernate (который использует много отражений и инструментов). И это включает довольно загадочные внутренние классы с небольшой перегрузкой методов. Как это интерпретировать? Давайте рассмотрим это снизу вверх (или сверху вниз в трассировке стека)

CursorRecordInitialiser

CursorRecordInitialiser — это внутренний класс, который инкапсулирует инициализацию Записи курсором, и он гарантирует, что соответствующие части SPI ExecuteListener покрыты в одном месте. Это шлюз к различным методам ResultSet в JDBC. Это общая внутренняя реализация RecordOperation которая вызывается…

RecordDelegate

RecordDelegate . Хотя имя класса довольно бессмысленно, его цель — защитить и обернуть все прямые операции записи так, чтобы можно было достичь центральной реализации SPI RecordListener . Этот SPI может быть реализован клиентским кодом для прослушивания событий жизненного цикла активной записи. Цена за реализацию реализации SPI DRY — это пара элементов трассировки стека, поскольку такие обратные вызовы являются стандартным способом реализации замыканий в языке Java . Но сохранение этой логики DRY гарантирует, что независимо от того, как инициализируется запись, SPI всегда будет вызываться. Там (почти) нет забытых угловых случаев.

Но мы инициализировали запись в …

CursorImpl

… CursorImpl, реализация курсора . Это может показаться странным, поскольку курсоры jOOQ используются для «отложенного извлечения», то есть для извлечения записей по одному из JDBC.

С другой стороны, запрос SELECT из этой трассировки стека просто обновляет один элемент UpdatableRecord, эквивалентный jOOQ активной записи. Тем не менее, все же вся ленивая процедура выборки выполняется так же, как если бы мы выбирали большой сложный набор данных. Это снова, чтобы сохранить вещи СУХОЙ при получении данных. Конечно, около 6 уровней трассировки стека можно было бы сохранить, просто прочитав одну запись, поскольку мы знаем, что она может быть только одна . Но опять же, любая незначительная ошибка в курсоре, вероятно, будет обнаруживаться в некотором тестовом примере, даже в удаленном, таком как тестовый случай для обновления записей.

Некоторые могут утверждать, что все это приводит к потере памяти и циклов ЦП. Скорее, наоборот. Современные реализации JVM настолько хороши для управления недолговечными объектами и вызовами методов и сбора мусора, что небольшая дополнительная сложность практически не требует дополнительной работы в вашей среде выполнения.

TL; DR: трассировки длинного стека могут указывать на высокую когезию и сухость

Утверждение, что трассировка длинного стека — это плохо, не обязательно правильно. Трассировка длинного стека — это то, что происходит, когда сложные структуры хорошо реализованы. Сложность неизбежно приведет к «утечкам абстракций» . Но только хорошо продуманная сложность приведет к длинным трассам стека.

И наоборот, короткие трассировки стека могут означать две вещи:

  • Отсутствие сложности: структура проста, с несколькими функциями. Это соответствует заявлению Игоря о ActiveJDBC, поскольку он рекламирует ActiveJDBC как «простую платформу».
  • Отсутствие сплоченности и СУХОГО состояния: среда написана плохо, и, вероятно, имеет плохое тестовое покрытие и множество ошибок.

Древовидные структуры данных

В заключение отметим, что еще один случай, когда следы длинных стеков неизбежны, — это когда древовидные структуры / структуры составных шаблонов пересекаются с помощью посетителей . Любой, кто когда-либо отлаживал XPath или XSLT, будет знать, насколько глубоки эти следы.