Знаете старую преподскую шутку - "Я им 5 раз объяснил, сам уже начал понимать, а они - не в какую!"?
Так вот я тут в попытках объяснить ИИ как писать хороший код, сам сделал хороший шаг в понимании этого:)
Жемчужину откопал там где не ждал - в "Чистом коде" анкл Боба в разделе про единственный уровень абстракции. Который, кстати, перечитывал недавно, но в тот раз пропустил.
Так вот жемчужина - техника объяснения кода в терминах "Чтобы ...".
To say this differently, we want to be able to read the program as though it were a set of TO paragraphs, each of which is describing the current level of abstraction and referencing subsequent TO paragraphs at the next level down.
To include the setups and teardowns, we include setups, then we include the test page content, and then we include the teardowns.
To include the setups, we include the suite setup if this is a suite, then we include the regular setup.
To include the suite setup, we search the parent hierarchy for the “SuiteSetUp” page and add an include statement with the path of that page.
To search the parent. . .
—
Иными словами, мы хотим иметь возможность читать программу так, как если бы она представляла собой набор абзацев "ЧТОБЫ...", каждый из которых описывает текущий уровень абстракции и ссылается на последующие абзацы "ЧТОБЫ" следующего, более низкого уровня.
Чтобы включить подготовительные и завершающие действия, мы сначала включаем setups, затем содержимое тестовой страницы, а затем teardowns.
Чтобы включить setups, мы включаем suite setup, если это набор тестов (suite), затем включаем обычный setup.
Чтобы включить suite setup, мы ищем в иерархии родительских страниц страницу “SuiteSetUp” и добавляем директиву include с путём к этой странице.
Чтобы выполнить поиск в родительской иерархии…
— Robert C. Martin, Clean Code, p. 37
Но это крутой инструмент не только для определения уровня абстракции функции, но и для определения её связности (cohesion).
Если функция имеет несколько несвязанных эффектов (коммуникационная и ниже связность), то её не получится описать в таком виде и без "и".
Например функция создания записей в дневнике в Проекте Э:
override operator fun createEvents(
userId: Int,
eventsRqs: List<EditEventRq<out EventData>>,
): List<Event> {
val profileId = profilesRepo.lockOnProfile(userId)
val deduplicatedEvents = deduplicateEvents(eventsRequest, profileId)
when (deduplicatedEvents) {
is EventListWithInvalidDuplicates -> throw DifferentEventsWithSameId(userId, deduplicatedEvents.values)
is EventListWithFullDuplicates -> {
log.warn("...")
}
else -> {}
}
val existEventsIds = eventsRepo.findAllIdsByIdIn(deduplicatedEvents.values.map { it.id })
var actuallyNewEvents = deduplicatedEvents.values.filter { it.id !in existEventsIds }
eventsRepo.saveAll(actuallyNewEvents)
val lastUsedDeviceSerialNumber = eventsRepo.getLastUsedDeviceSerialNumberByProfile(profileId)
val domainEvents = actuallyNewEvents
.mapNotNull { DiaryEventCreated.fromDto(userId, it, lastUsedDeviceSerialNumber) }
eventsCreatedQueue.publishEvents(domainEvents)
return savedEvents
}
Её связность на первый взгляд может функциональной - сохранить записи дневника.
Но на самом деле - коммуникационная: Чтобы создать события мы блокируем профиль, дедуплицируем события, находим действительно новые события, сохраняем их в БД и публикуем сообщение об их создании. А публикуем мы сообщение - точно для того, "чтобы создать события"? Нет. Эта операция имеет две ответственности: создать события и оповестить об этом заинтересованные лица - "чтобы создать события и оповестить об этом мы...".