Связность и связанность в современных системах
Связность и связанность в современных системах
Меня зовут Иван Башарин, руководитель лаборатории AI и архитектор решений в компании «Электронная торговая площадка Газпромбанка». Я расскажу о достаточно абстрактных понятиях — связанности и связности.
Связанность — это мера зависимости между модулями или компонентами системы. Она описывает, насколько сильно один модуль зависит от другого.
В программировании существует несколько видов связанности, которые могут варьироваться от сильной (высокой) до слабой (низкой):
– Сильная связанность: модули имеют много зависимостей друг от друга, что делает систему менее гибкой и сложной для изменения. Например, если изменение в одном модуле требует изменений в других модулях, это свидетельствует о высокой связанности
– Слабая связанность: модули минимально зависят друг от друга. Это позволяет вносить изменения в один модуль без необходимости изменения других, что улучшает гибкость и сопровождаемость системы
Существует много видов связанности, выделяют три основных:
-
По данным — модули обмениваются данными напрямую или через параметризацию
-
По управлению — один модуль управляет поведением другого
-
По содержимому — один модуль напрямую меняет данные другого
Давайте попробуем разобрать на примерах.
Существует класс процессора, существует класс отправителя.
class DataProcessor:
def process_data(self, data):
return [d * 2 for d in data]
class DataSender:
def send_data(self, data):
print("Sending data:", data)
class Application:
def init(self):
self.processor = DataProcessor()
self.sender = DataSender()
def run(self):
data = [1, 2, 3, 4]
processed_data = self.processor.process_data(data)
self.sender.send_data(processed_data)
if name == "__main__":
app = Application()
app.run
()
Первый в этом примере просто умножает элементы массива на два.
Второй при этом просто выводит массив.
Application создает экземпляры DataProcessor и DataSender.
Зацепление по данным: если мы захотим изменить способ обработки или отправки данных, нам придется модифицировать не сам метод отправки, а класс Application.
Давайте разберем более интересный случай.
class PaymentRegistry:
def init(self):
self.is
_on = False
def toggle(self, command):
if command == “MAKE":
self.is
_on = True
print(“выполнили платежи")
elif command == “Cancel":
self.is
_on = False
print(“запретили платежи")
class Switch:
def init(self, light):
self.light = light
def makeYourDreamComeTrue(self, command):
self.light.toggle(command)
if name == "__main__":
rp = PaymentRegistry()
switch = Switch(rp)
switch. makeYourDreamComeTrue(" Cancel ")
Все мы работаем с данными, банком, платежами и документами.
Посмотрим на псевдокод, реализующий процесс выполнения реестра платежей.
Существует класс реестра, класс вынесения решения по реестру платежей и собственно код для вызова.
PaymentRegistry представляет объект реестра платежей и содержит метод toggle.
Toggle принимает команду для выполнения или отклонения платежей
Switch — это «выполнитель», имеет метод makeYourDreamComeTrue. makeYourDreamComeTrue передает команду в метод toggle класса PaymentRegistry.
Попробуйте угадать, какой из плохих вариантов я демонстрирую.
Представлен основной вариант — плохой вариант зацепления по управлению.
Метод toggle зависит от переданной команды отклонения или выполнения.
Сложность изменения: если в будущем потребуется изменить логику управления светом (например, добавить новые команды), это потребует изменения в классе Switch. Это делает код менее гибким и увеличивает вероятность ошибок.
Непредсказуемость: если несколько классов используют один и тот же метод с различными командами, это может привести к путанице и трудностям в отладке.
И второстепенный, но тоже плохой приведенный тут вариант — зацепление по данным, т. к. команды в примере мы передаем текстом.
Что делать с таким кодом и как реализовывать схожие задачи, расскажу чуть позже.
Немного теории: что же лучше?
Сильная связанность приводит к сложности понимания логики системы.

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

Почему это не совсем так, тоже расскажу позже.
Связность — это абстрактная величина «однонаправленности» элементов модуля. Правда ведь, звучит логично: все элементы модуля должны быть связаны и выполнять одну задачу.
Выделяют три основных вида связности:
-
Функциональная — все элементы выполняют одну задачу
-
Логическая — элементы модуля выполняют схожие функции, но необязательно одновременно
-
m, Процедурная — элементы модуля выполняются в определенной последовательности
До этого были примеры кода, в связности упростим демонстрацию.
Представим модуль, который в каком-либо виде присутствует у каждого — рассылки пользователям каких-либо уведомлений.

-
Нам надо получит само событие — по таймауту, пользовательскому действию
-
Проверить его на корректность: мало ли что нам туда отдали
-
Дополнить данными. Вряд ли у нас в событии есть все, что надо, — почтовые адреса, темы, дополнительные данные контрагентов и т. п.
-
Сформировать собственно текст рассылки
-
Положить куда-то историю и логи. Например, в ЛК БСК мы должны хранить все рассылки минимум три года
-
И собственно отправить письмо
Модуль выглядит логично, не правда ли? Выполняет одно действие — генерирует рассылку. Действия выполняет последовательно — еще лучше!
Но теперь почему это не так.
-
Валидация событий относится к событийной модели сервиса
-
Непосредственно отправка сообщения является внешней зависимостью и даже блокирующим фактором
-
Хранение статистики и логов — два отдельных решения: одно на аналитических паттернах, а другое системное
-
Дополнение данных — так же внешнее подключение и внешняя зависимость
И все выглядит достаточно логично. Разделяем модуль по таким вот белочкам, для каждой белочки строим свой домик и получаем высокую связность: каждая белочка будет есть свой орешек.

Почему и это не совсем так, расскажу дальше.
Теория связности очень похожа на связанность. Такие же степени и такие же проблемы, только наоборот:
-
Высокая связность — это и переипользование, и тестирование: ограниченный строгим набором функций модуль легко используется даже в других проектах. Такие модули легко изучать и передавать на поддержку сотрудникам

-
Низкая связность — это точно наоборот: проблемы во всех предыдущих пунктах

Кратко расскажу о способах нормализации связанности и связности в проектах, приведу базовые общие практики.
Широко используются шаблоны проектирования.
-
Компонент иногда фигурирует отдельным шаблоном. Это достаточно простой подход приведения кода к атомарным частям. Буквально из примера про белочек — взяли часть, отвечающую за подключение к СМТП, и вынесли в отдельный компонент
-
Очередь событий отлично подходит для дополнения данными или валидации. Последовательность действий для формирования результата позволит контролировать весь процесс без лишних зависимостей
-
Нельзя забывать о базовых, самых первых паттернах: фабрики, одиночки и наблюдатели популярны, удобны и используются повсеместно
Возвращаемся к нашему реестру платежей.
Просто использование паттерна «Наблюдатель» позволит реализовать тот же функционал, но с сохранением адекватной связи:
class PaymentRegistry:
def init(self):
self.is
_on = False
def turn_on(self):
self.is
_on = True #выполнили платежи
def turn_off(self):
self.is
_on = False #запретили платежи
class Switch:
def init(self):
self.callbacks = []
def add_callback(self, callback):
self.callbacks.append(callback)
def makeYourDreamComeTrue(self, action):
for callback in self.callbacks:
callback(action)
# Основная логика
if name == "__main__":
light = Light()
switch = Switch()
# Регистрация методов управления
switch.add_callback(lambda action: light.turn_on() if action == "ON" else light.turn_off())
# Управляем
switch.press
("ON")
switch.press
("OFF")
-
Уведомляем реестр платежей о необходимости изменения состояния без непосредственной передачи команды
-
Вводим механизм событий и управления этими событиями через Switch
-
Switch содержит список обратных вызовов и метод добавления callback-ов
-
Метод makeYourDreamComeTrue вызывает соответствующее действие
-
В основной логике вызываем через лямбда (функцию обратного вызова) нужные методы
Однако видно увеличение объема кода. В этом случае такое увеличение оправдано — все же в настоящей реализации его будет еще больше.
Возвращаемся к способам нормализации.
Иерархическая декомпозиция и GRASP-паттерны
Шаблоны проектирования, разработанные для решения общих задач по взаимодействию компонентов и модулей сервиса
Выделяют 8 основных GRASP-паттернов:
-
Информационный эксперт. Обязанности назначаются объекту, обладающему максимумом (всей) информацией для выполнения обязанности
-
Создатель. Создать отдельный класс, управляющий созданием объекта другого класса, если он содержит, агрегирует, записывает или использует объекты. Похоже на фабрику, не правда ли?
-
Контроллер. Выполняет операции с внешними инициаторами
-
Слабое зацепление / сильная связанность. Паттерн, соответствующий теме. Описывает в целом подход к модулям системы
-
Полиморфизм. Базовый принцип ООП, о них знают все
-
Чистая выдумка. Создаем полностью отвязанные друг от друга взаимодействия объектов и точек хранения этих объектов
-
Перенаправление. Буквально концепция MVC
-
Устойчивость к изменениям. Шаблон, защищающий элементы от изменения другими элементами. Должен реализовывать отдельный интерфейс для изменений и доступа
Модульность

Это базовый подход к декомпозиции любого сервиса согласно и связанности и связности. То, что практикуется повсеместно, и подробно останавливаться на этом не буду. Разделили сервис на отдельные части, модули внутри взаимодействуют тесно, между собой — событиями.
И для того чтобы обеспечить предыдущий слайд, существуют интерфейсы и абстракции.

Интерфейсы не в концепции разработки и имплементации, а в модульности и едином интерфейсе взаимодействия с таким модулем
Не могу не остановиться отдельно на тех самых принципах SOLID, которые всегда спрашивают на собеседованиях.
В целом вся концепция SOLID рассчитана на создание системы, которую легко поддерживать и расширять — и тут ключевой момент — в течение долгого времени.
Отвечающих теме принципов два:
-
Принцип единой ответственности. Каждый объект должен решать одну задачу, и эта задача должна быть полностью инкапсулирована в класс. Генерирующий отчет класс должен меняться только по двум причинам: может измениться содержание или формат отчета. Это не включает в себя добавление новых форматов отчета, профилей экспорта. А строго — сам отчет, его содержание и формат
-
Принцип открытости/закрытости. Согласно этому принципу, модули и компоненты должны быть открыты для расширения — и снова важный пункт — путем создания новых типов сущностей, т. е. для того же класса с отчетом мы не можем в нем же создавать новые шаблоны экспорта и форматы файлов. Мы должны создавать новые классы: потомков, декораторов, — но не менять сам начальный компонент
В целом задача выглядит достаточно прозрачной, не правда ли?
Создаем модули, внутри которых зависимые друг от друга компоненты, а сами модули связываем друг с другом через RPC, DTO или другие «отвязанные» от происходящего внутри модуля объекты.
Все, к сожалению, не так просто. Представьте сервис, состоящий из сотен отдельных модулей или микросервисов, — все связаны между собой через API и DTO, вся передача через кафку, все максимально отвязаны один от другого.
-
В такой схеме наверняка появится избыточность кода: при низкой связанности модули и компоненты будут дублировать код друг друга
-
Сложности в управлении интеграцией. Даже пару десятков микросервисов связать между собой сложно, что говорить о сотне
-
Проблемы согласованности данных — модули могут ожидать одной и той же информации, но не согласовывать между собой средства валидации
-
Трудность понимания общего контекста проекта
Именно такая история произошла в сервисах Амазона в 2010-х годах. Сервисы были максимально декомпозированы, функционал вынесен в отдельные связанные между собой микросервисы и после краткой радости и последующих проблем с разработкой и даже функционированием итогового продукта Амазон запустил обратный процесс и начал собирать монолиты.
Про излишне высокую связность сложно сказать что-то плохое, но я скажу!
Основное — это те же проблемы общего контекста проекта и сложность модульного тестирования. Даже выполняющий одну «задачу» модуль может быть достаточно объемным, тестировать такой будет достаточно сложно.

Если поискать термины «связанность» и «связность» в интернете, вы найдете такие же, картинки. На них будет нарисована красивая схема «как надо делать» и «как делать не надо». Однако, на мой взгляд, объективно корректного уровня отношения связанности и связности не существует. В каких-то проектах банковского сопровождения можно выделять типы документов в отдельные несвязанные модули и не переживать. Где-то в сервисах работы с МЧД и зависимостями вплоть до ФНС без жесткого контроля всех составляющих процессов просто не обойтись.

У каждого проекта, команды и выбранной архитектуры свои отношения.
Выбирайте комфортный уровень отношений внутри и вовне частей ваших систем исходя из удобства дальнейшей разработки и потенциала к переиспользованию этих частей.
Автор: basharinIv