Делал презентацию для выступления на внутреннем митапе на нашем IT-заводе. Попробую перевести её в формат статьи. Но получится текста сильно меньше, чем наговорил на митапе.
Связность и связанность кода - это два идущих рука об руку понятия, с помощью которых можно определять, насколько качественной явлется кодовая база проекта.
Отличие в произнесении этих двух понятий - всего в одной букве 'н' и это очень запутывает, особенно, когда пытаешься использовать эти термины при общении голосом. Как следствие, многие пытаются использовать вместо этих двух понятий синонимы, что запутывает желающих разобраться в них ещё больше.
Связность - это взаимосвязь элементов кода (функций, классов, модулей и т.д.).
Связность бывает:
- функциональная (элементы служат выполнению приложением одной общей для них функции),
- логическая (один элемент использует данные, предоставляемые другим элементом),
- последовательная (элементы должны быть выполнены последовательно друг за другом),
- процедурная (элементы должны быть выполнены в строгом порядке),
- веременная (элементы должны быть выполнены в одно время).
Высокая связность - когда элементы, между которыми есть связность, находятся в одном компоненте кода. Высокая связаность считается хорошим признаком.
Низкая связность - когда элементы, между которыми нет связности, находятся в одном компоненте. Это считается признаком кода плохого качества.
Что делать, чтобы было хорошо?
Элементы, имеющие низкую связность, должны быть разнесены в разные компоненты:
- пары поле - метод с низкой связностью должны быть вынесены в отдельные классы,
- классы с низкой связностью должны быть вынесены в отдельные пакеты.
Связанность - когда элементы с низкой связностью имеют связи (вызывают методы друг друга) или находятся в одном компоненте (см. выше пример с пакетом controller).
Понятие связанности используется совместно с таким понятиями как:
- абстрактность,
- нестабильность,
- коннасценция.
Абстрактность - это отношение числа абстрактных классов к конкретным.
Абстрактные классы помогают уменьшить связанность в коде. Но слишком большое значение абстрактности затрудняет разработчикам отслеживание взаимных связей компонентов программы.
Нестабильность определяется по формуле:
I = Ce/(Ce + Ca)
Ce - эфферентность (исходящие связи класса)
Ca - афферентность (входящие связи класса)
Чем выше нестабильность, тем легче кодовая база ломается при внесении изменений.
Между абстрактностью и нестабильностью должен быть баланс.
Сильное отклонение в сторону преобладания абстрактности переносит код в зону бесполезности: слишком абстрактный код создаёт трудности при его использовании.
Сильное отклонение в сторону нестабильности переводит код в зону мучений: слишком большая доля реализации и недостаточная абстрактность приводят к хрупкости кода и сложности в сопровождении.
Конасценция. Два компонента считаются конасцентными, если изменния, внесённые в один из них, потребуют модификации другого для поддержания общей работоспособности системы.
В некоторых источниках отмечают, что связанность - это устаревшее понятие, введёное учёными в 1970-х годах. Более современным считается использовать вместо него термин конасценция.
Характеристики коннасценции:
- сила,
- локальность,
- степень.
Сила конасценции - это лёгкость, с которой в код могут быть внесены изменения (выполнен рефакторинг).
В порядке возрастания сложности рефакторинга кода выделяют следующие виды коннасценции:
- статические – связанность на уровне исходного кода,
- динамические – связанность на уровне выполнения.
Статические конасценции разделяются на конасценции (в порядке возрастания ложности рефакторинга):
- имени (пример: несколько функций завязаны на один enum),
- типа (одна функция зависит от типа данных, возвращаемых дугой функцией),
- смысла (статические константы),
- алгоритма (на одном конце алгоритм расшифровки сообщения зависит от алгоритма шифрования на другом конце),
- позиции (пример из java: нужно соблюдать позицию передаваемых в метод аргументов).
Динамические конасценции разделяются на конасценции (в порядке возрастания ложности рефакторинга):
- исполнения (порядок выполнения имеет значение),
- синхронности (в многопоточном коде),
- значений (значения разных элементов зависят друг от друга. Пример: транзакционность),
- идентичности (несколько элементов работают с одной структурой. Например, с очередью).
Локальность конасценции. Зависит от того, как близко друг от друга в коде находятся связанные элементы.
Чем дальше друг от друга находятся такие элементы и, чем сильнее будет конасценция, тем больше будет ущерб от связанности элементов.
Примеры:
- 2 микросервиса смотрят в одну базу данных (завязаны на её структуру),
- интеграционные тесты зависят от какого-то эталонного наполнения тестовой базы данных.
Степень конасценции - это количество классов, на которые она влияет.
Степень конасценции может увеличиваться с разрастанием кодовой базы.
Пример:
На начальном этапе разработки у нас всего несколько интеграционных тестов и они зависят от эталонного наполнения тестовой базы данных. Мы можем не видеть в этом проблемы, так как степень конасценции пока невелика. Однако проблема есть, так как в коде присутствует динамическая конасценция значения - вторая по неприятным поседствиям конасценция. Хуже неё только динамическая конасценция идентичности.
При разрастании кодовой базы будет увеличиваться количество интеграционных тестов и в каждом из них нам нужно будет учитывать, что база данных должна сохранять своё эталонное состояние, несмотря ни на что. Т.е. степень конасценции будет возрастать. С возрастанием степени конасценции однажды наступает момент, когда уже будет
и переписать старые тесты. И это будет печально.
Что делать с конасценцией? Возможны два пути.
Путь падавана:
1. Не думать о ней и жить дальше.
2. Когда станет совсем невозможно работать - перейти на другой проект. А ещё лучше - перейти на новый проект и с самого начала, основательно разложить там те же грабли, что были и на прошлом проекте.
3. Повторять пункты 1 и 2 до выхода на пенсию.
Путь джедая:
1. Сводить общую коннасценцию к минимуму за счёт разбиения системы на инкапсулированные элементы (классы, пакеты, модули, приложения).
2. Минимизировать коннасценцию, пересекающую границы инкапсуляции.
3. Сильные формы коннасценции переделывать в как можно более слабые. Например, статическую. логическую конасценцию
const val TRUE: Int = 1
const val FALSE: Int = 0
Заменить на статическую конасценцию типа: вместо объявления констант TRUE и FALSE использовать везде значения true и false типа Boolean.
4. По мере увеличения расстояния между элементами использовать более слабые формы коннасценции.