Разработка ПО и Backend систем.

Позднее Ctrl + ↑

Модульный монолит (4)

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

Для понимания того, как модульность выглядит на уровне приложения обратимся к схеме:

Оранжевый и сиреневый квадраты — это модули Бизнес Логики(БЛ).
Зеленые линии — это тоже модули, но модули технические.

В контексте данной темы технические модули называют «слоями», а модули БЛ «модулями», так как чаще всего изменения и разработка касаются бизнес логики — добавляются новые фичи, изменяется поведение старых и тому подобное.

На схеме отображено взаимодействия модулей бизнес логики с техническими слоями приложения. Каждый модуль реализует собственную функциональность, используя возможности слоев БД, логики приложения(обмен сообщений, запуск фоновых задач, ...) через контракты(интерфейсы).

Мы можем изменить реализацию и способ хранения данных, но модуль «Orders» ничего не заметит, он как использовал нужные ему вызовы методов слоя БД, так и использует. Можем поменять визуальное представление сущностей модуля «Payments», модулю все равно, он отдает со своей стороны данные по контракту и получает объект-представление, как и было оговорено в контракте.

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

Модульный монолит(3)

Продолжим по модульный монолит.

Если монолитная архитектура почти всегда скатывается в «комок грязи», а модульность должна нас от этого комка спасти. Давайте поймем, что такое «модуль».

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

Модулем как правило называют модуль бизнес-логики приложения(модуль почтовой рассылки), но так же есть «инфраструктурные» модули(модуль для работы с БД, очередью и так далее).

Модуль должен обладать следующими свойствами:
1) Независимостью — зависимости модуля сведены к минимуму и частота изменений «родительских» модулей минимальна. Так же снижаем сцепленность и увеличиваем связность.
2) SRP — модуль решает четко определенную задачу.
3) имеет четко определенный интерфейс — контракт для взаимодействия с другими модулями. Хороший контракт — непротиворечивый и минимальный. Интерфейс модуля должен быть стабильным(не подвергаться частому изменению).

Сравнение кода. Опрос.

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

Первый вариант:

def _write_steps(
    self,
    steps: List[SoftwareProfileStep],
    profile_dir_path: Path,
) -> None:
    """
    Записать шаги профиля в файловое хранилище

    Args:
        steps: Список шагов профиля управления ПО.
        profile_dir_path: Путь к директории профиля в файловом хранилище
    """
    steps_j2 = []
    for step in steps:
        if step.type == SoftwareProfileStepType.PACKAGE:
            step_var_uid = self._write_step_vars(
                step, 
                profile_dir_path,
            )
            steps_j2.append(
                self._step_package_template.format(
                    action=step.package_action,
                    step_vars=step_var_uid,
                )
            )
        elif step.type == SoftwareProfileStepType.REPOSITORY:
            steps_j2.append(
                self._step_repo_template.format(
                    step_uid=step.uid,
                )
            )

    steps_list = list(map(str, steps_j2))
    steps_str = self.__to_jinja_statement(
        self._steps_template.format(
            steps=steps_list,
        )
    )

    with Path(profile_dir_path / "steps.j2").open("w") as steps_file:
        steps_file.write(steps_str)

Второй вариант:

def _write_steps(
    self,
    steps: List[SoftwareProfileStep],
    profile_dir_path: Path,
) -> None:
    """
    Записать шаги профиля в файловое хранилище

    Args:
        steps: Список шагов профиля управления ПО.
        profile_dir_path: Путь к директории профиля в файловом хранилище
    """
    steps_j2 = [
        self.__fill_step_template(step)
        for step in steps
    ]

    steps_list = list(map(str, steps_j2))
    steps_str = self.__to_jinja_statement(
        self._steps_template.format(
            steps=steps_list,
        )
    )

    with Path(profile_dir_path / "steps.j2").open("w") as steps_file:
        steps_file.write(steps_str)

@staticmethod
def __fill_step_template(step: SoftwareProfileStep):
    def _package_template(step: SoftwareProfileStep):
        step_var_uid = self._write_step_vars(
            step,
            profile_dir_path,
        )
        return self._step_package_template.format(
            action=step.package_action,
            step_vars=step_var_uid,
        )
    def _repo_template(step: SoftwareProfileStep):
        return self._step_repo_template.format(
            step_uid=step.uid,
        )

    actions = {
        SoftwareProfileStepType.PACKAGE: _packege_template,
        SoftwareProfileStepType.REPOSITORY: _repo_template,
    }
    return actions.get(step.type)(step)

Кто я такой?

Senior Python Developer.
Разрабатываю систему ACM в AstraLinux. Руковожу командой из 8 разработчиков, отвечаю за функциональность установки/конфигурации ПО и выполнения заданных команд/скриптов на целевых компьютерах(контур из 4 микросервисов), управления инфраструктуры(контур из 3 микросервисов), а так же за отказоустойчивость отведенной мне подсистемы. Развиваю проект с MVP версии, спас несколько релизов и провел(и буду проводить дальше) методы повышения качества кода и DevEx от согласования до внедрения.

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

Развиваюсь в направлении архитектуры систем/кода, методологий разработки, технологиях создания качественных Backend-систем.
Вкатываюсь в fullstack-историю, оказываю консультации(в том числе трудоустройство с нуля) и разрабатываю ПО для коммерческих проектов.
Блог в телеграмме — https://t.me/SmotrovDev

Связь со мной:

  1. Консультационные услуги — https://teletype.in/@dsmotrov/y6bjePDDihw
  2. Для сотрудничества — SmotrovDM@yandex.com

Ответ на вопрос

Я не видел еще модульных монолитов, как он должен выглядеть, чтоб считаться достаточно модульным ?) Как обеспечить low coupling ?)

Приведу небольшой пример(код рабочий, можете поиграться)

from dataclasses import dataclass
from typing import Any, Callable


# Тип так же может предоставлять интерфейс, а его внутренности могут быть чем угодно.
# В примере это упущенно, но в последней реализации костыльно показано, что это мы тоже используем.
@dataclass()
class State:
    values: list[Any]

    def extract_value(self) -> Any:
        return self.values[0]

    def __len__(self):
        return len(self.values)
    
    def __getitem__(self, key):
        return self.values[key]

# predicates.py #
def is_done(state: State) -> bool:
    return len(state) == 1

def is_even(state: State) -> bool:
    return len(state) % 2 == 0

# predicates.py #

# transformers.py #
def transform(state: State) -> State:
    new_state = State([state[0] + state[1]] +  state[2:])
    return new_state

def transform_by_head_plus_tail(state: State) -> State:
    new_state = State([state[0] + state[-1]] + state[1:len(state) - 1])
    return new_state

# transformers.py #

# app.py #
# "типами" ниже задан интерфейс для работы метода. По сути мы уже обеспечили модульность данного компонента.
def Iterate(
    state: State,
    is_done: Callable[[State], bool],
    transform: Callable[[State], State]
) -> State:
    if is_done(state):
        return state
    state = transform(state)
    return Iterate(state, is_done,transform)


def iterable_factory(
    iterate_func: Callable[..., State],
    stop_predicate: Callable[[State], bool],
    transform: Callable[[State], State],
    ) -> Callable:
    def decorator(state: State) -> State:
        if not state:
            return State([])
        return iterate_func(state, stop_predicate, transform)
    return decorator

# app.py #


# main.py #

FoldL = iterable_factory(Iterate, is_done, transform)
print(FoldL(State([1,5,3,7,9])))

DummyFoldL = iterable_factory(Iterate, is_even, transform_by_head_plus_tail)
print(DummyFoldL(State([i for i in range(101)])))

GausFoldL = iterable_factory(Iterate, is_done, transform_by_head_plus_tail)
print(GausFoldL(State([i for i in range(101)])))


StringFoldL = iterable_factory(Iterate, is_done, transform)
result = StringFoldL(State(
    "Вызываемые объекты, которые принимают другие вызываемые объекты в качестве аргументов,"
    "могут указывать на то, что их типы параметров зависят друг от друга с помощью typing.ParamSpec.".split()
))
print(result)

# main.py #

В данном примере я хотел достаточно маленьким кусочком кода показать, что конкретнее я имел ввиду в предыдущем посте.

Давайте договоримся, что модулем мы будем называть сущность, которая группирует связанные по смыслу операции. В нашем случае мы имеем модулю: app.py, predicate.py, transformers.py и модуль для запуска main.py .

По сути, модуль app.py — единая точка входа. Для метода Iterate требуются зависимости, принимающие на вход состояние(State) и возвращающие либо bool, либо новое состояние. В данном контексте типизация является интерфейсом. Тут же используется так называемая «функциональная инъекция зависимостей».

Получается обобщенный программный компонент, который благодаря слабой связанности(за счет четких интерфейсов на типах) легко может заменить свои составляющие компоненты. Сложность кода измеряется как последовательная сумма сложностей компонентов этого кода.

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

Это был ответ на вопрос к предыдущей статье.
Задавайте вопросы в комментариях.

Модульный монолит (1)

В моих целях на ближайшие полгода лучше разобраться с практиками и архитектурными подходами, которые применяются в моей текущей работе. Начать я захотел с такого понятия, как «Модульный монолит». Кому интересно аргументированно пообщаться — прошу под кат.

Монолитная архитектура — это традиционная универсальная модель проектирования ПО. Монолитный в данном контексте значит собранный в единое целое. Компоненты программы связаны и взаимозависимы, а не обладают слабой связанностью (_low coupling — >прим. перев._), как в случае модульных программ.

Описание выше — пример очень плохой ситуации, когда происходит «утечка БЛ»(или же сильное сплетение разных компонентов). Подобную систему поддерживать — очень дорого по трудозатратам и деньгам. Все наладом дышит, а капельницы для проекта заканчиваются быстрее, чем новые подвозят.

Я уверен, что читатель в своих проектах с пеной у рта отказывается вливать конструкции с таким количеством связей в разные стороны, в том числе когда код «воюет не в ту сторону». Кроме случаев, когда дедлайн ближе, чем кажется(вы же берете себе задачу для улучшение подобного кусочка кода в ближайшее после хотфиксов время?).

И тут у меня возникает вопрос: а зачем из каждого утюга орут, что монолит — это мерзость, которую всеми правдами и неправдами нужно избегать? Ребят, а вы зачем лезете в микросервисы, когда есть возможность добавить модульность в монолит, использовать между модулями четкие интерфейсы и в определенный момент(он сам к вами придет) вырезать нужные сервисы из достаточно опрятного монолита? Какое учение несет ваша церковь? Кто среди вас главный евангелист?

Разберемся.