Связность и связанность в современных системах

Связность и связанность в современных системах

Меня зовут Иван Башарин, руководитель лаборатории AI и архитектор решений в компании «Электронная торговая площадка Газпромбанка». Я расскажу о достаточно абстрактных понятиях — связанности и связности.

Связанность — это мера зависимости между модулями или компонентами системы. Она описывает, насколько сильно один модуль зависит от другого.

В программировании существует несколько видов связанности, которые могут варьироваться от сильной (высокой) до слабой (низкой):

– Сильная связанность: модули имеют много зависимостей друг от друга, что делает систему менее гибкой и сложной для изменения. Например, если изменение в одном модуле требует изменений в других модулях, это свидетельствует о высокой связанности

– Слабая связанность: модули минимально зависят друг от друга. Это позволяет вносить изменения в один модуль без необходимости изменения других, что улучшает гибкость и сопровождаемость системы

Существует много видов связанности, выделяют три основных:

  1. По данным — модули обмениваются данными напрямую или через параметризацию

  2. По управлению — один модуль управляет поведением другого

  3. По содержимому — один модуль напрямую меняет данные другого

Давайте попробуем разобрать на примерах.

Существует класс процессора, существует класс отправителя.

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. Это делает код менее гибким и увеличивает вероятность ошибок.

Непредсказуемость: если несколько классов используют один и тот же метод с различными командами, это может привести к путанице и трудностям в отладке.

И второстепенный, но тоже плохой приведенный тут вариант — зацепление по данным, т. к. команды в примере мы передаем текстом.

Что делать с таким кодом и как реализовывать схожие задачи, расскажу чуть позже.

Немного теории: что же лучше?

Сильная связанность приводит к сложности понимания логики системы.

Связность и связанность в современных системах - 1

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

Такой модуль невозможно переиспользовать, его очень сложно расширять и поддерживать.

Поэтому, конечно, ответ: «Нам нужна слабая связанность между модулями проекта».

Связность и связанность в современных системах - 2

Почему это не совсем так, тоже расскажу позже.

Связность — это абстрактная величина «однонаправленности» элементов модуля. Правда ведь, звучит логично: все элементы модуля должны быть связаны и выполнять одну задачу.

Выделяют три основных вида связности:

  • Функциональная — все элементы выполняют одну задачу

  • Логическая — элементы модуля выполняют схожие функции, но необязательно одновременно

  • m, Процедурная — элементы модуля выполняются в определенной последовательности

До этого были примеры кода, в связности упростим демонстрацию.

Представим модуль, который в каком-либо виде присутствует у каждого — рассылки пользователям каких-либо уведомлений.

Связность и связанность в современных системах - 3
  • Нам надо получит само событие — по таймауту, пользовательскому действию

  • Проверить его на корректность: мало ли что нам туда отдали

  • Дополнить данными. Вряд ли у нас в событии есть все, что надо, — почтовые адреса, темы, дополнительные данные контрагентов и т. п.

  • Сформировать собственно текст рассылки

  • Положить куда-то историю и логи. Например, в ЛК БСК мы должны хранить все рассылки минимум три года

  • И собственно отправить письмо

Модуль выглядит логично, не правда ли? Выполняет одно действие — генерирует рассылку. Действия выполняет последовательно — еще лучше!

Но теперь почему это не так.

  • Валидация событий относится к событийной модели сервиса

  • Непосредственно отправка сообщения является внешней зависимостью и даже блокирующим фактором

  • Хранение статистики и логов — два отдельных решения: одно на аналитических паттернах, а другое системное

  • Дополнение данных — так же внешнее подключение и внешняя зависимость

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

Связность и связанность в современных системах - 4

Почему и это не совсем так, расскажу дальше.

Теория связности очень похожа на связанность. Такие же степени и такие же проблемы, только наоборот:

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

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

Связность и связанность в современных системах - 6

Кратко расскажу о способах нормализации связанности и связности в проектах, приведу базовые общие практики.

Широко используются шаблоны проектирования.

  • Компонент иногда фигурирует отдельным шаблоном. Это достаточно простой подход приведения кода к атомарным частям. Буквально из примера про белочек — взяли часть, отвечающую за подключение к СМТП, и вынесли в отдельный компонент

  • Очередь событий отлично подходит для дополнения данными или валидации. Последовательность действий для формирования результата позволит контролировать весь процесс без лишних зависимостей

  • Нельзя забывать о базовых, самых первых паттернах: фабрики, одиночки и наблюдатели популярны, удобны и используются повсеместно

Возвращаемся к нашему реестру платежей.

Просто использование паттерна «Наблюдатель» позволит реализовать тот же функционал, но с сохранением адекватной связи:

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

  • Устойчивость к изменениям. Шаблон, защищающий элементы от изменения другими элементами. Должен реализовывать отдельный интерфейс для изменений и доступа

Модульность

Связность и связанность в современных системах - 7

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

И для того чтобы обеспечить предыдущий слайд, существуют интерфейсы и абстракции.

Связность и связанность в современных системах - 8

Интерфейсы не в концепции разработки и имплементации, а в модульности и едином интерфейсе взаимодействия с таким модулем

Не могу не остановиться отдельно на тех самых принципах SOLID, которые всегда спрашивают на собеседованиях.

В целом вся концепция SOLID рассчитана на создание системы, которую легко поддерживать и расширять — и тут ключевой момент — в течение долгого времени.

Отвечающих теме принципов два:

  1. Принцип единой ответственности. Каждый объект должен решать одну задачу, и эта задача должна быть полностью инкапсулирована в класс. Генерирующий отчет класс должен меняться только по двум причинам: может измениться содержание или формат отчета. Это не включает в себя добавление новых форматов отчета, профилей экспорта. А строго — сам отчет, его содержание и формат

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

В целом задача выглядит достаточно прозрачной, не правда ли?

Создаем модули, внутри которых зависимые друг от друга компоненты, а сами модули связываем друг с другом через RPC, DTO или другие «отвязанные» от происходящего внутри модуля объекты.

Все, к сожалению, не так просто. Представьте сервис, состоящий из сотен отдельных модулей или микросервисов, — все связаны между собой через API и DTO, вся передача через кафку, все максимально отвязаны один от другого.

  • В такой схеме наверняка появится избыточность кода: при низкой связанности модули и компоненты будут дублировать код друг друга

  • Сложности в управлении интеграцией. Даже пару десятков микросервисов связать между собой сложно, что говорить о сотне

  • Проблемы согласованности данных — модули могут ожидать одной и той же информации, но не согласовывать между собой средства валидации

  • Трудность понимания общего контекста проекта

Именно такая история произошла в сервисах Амазона в 2010-х годах. Сервисы были максимально декомпозированы, функционал вынесен в отдельные связанные между собой микросервисы и после краткой радости и последующих проблем с разработкой и даже функционированием итогового продукта Амазон запустил обратный процесс и начал собирать монолиты.

Про излишне высокую связность сложно сказать что-то плохое, но я скажу!

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

Связность и связанность в современных системах - 9

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

Связность и связанность в современных системах - 10

У каждого проекта, команды и выбранной архитектуры свои отношения.

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

Автор: basharinIv

Источник

Оставить комментарий