Группировка в программах


Методы или функции, которые пишем в наших программах, очень часто имеют несколько логических частей. Например, в рамках одной функции, если нам нужно отправить сообщение на сервер, мы можем 1) проверить его доступность, затем 2) отправить сообщение, и потом 3) проверить что сообщение было доставлено. Но достаточно ли такой подход отвечает принципу единственной ответственности SRP?

Знаменитый Роберт Мартин похоже как раз за то, чтобы разделять методы на как можно более мелкие фрагменты, с минимально возможной ответственностью. Такой подход, естественно, имеет множество преимуществ, например легкость тестирования, а также постоянно небольшое количество различных переменных в нашем пространстве имён. Однако, как замечают очень многие, такой стиль приводит также к тому, что в нашей программе появляется множество перенаправлений/косвенности. Программисту может быть значительно труднее следить за детальной логикой работы метода, когда каждый шаг перенаправляет нас в отдельный метод.

Как я недавно узнал, в большинстве C-подобных языков есть подход, который позволяет немного сгладить те недостатки, которые решает выделение каждого кусочка в свой метод: это заключение каждого такого кусочка в блоки при помощи фигурных скобок { }. Например, если наш метод должен отправлять email-сообщение, то мы можем выделить:

  • checkAccessibility
  • sendMessage
  • checkResponse

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

Похоже, что в каком-нибудь Rust, где блоки также являются выражениями, этот подход выглядит особенно удобно. В блоке можно спокойно объявлять переменные и они никогда не покинут свой scope, а поседнее значение можно при необходимости “вернуть” как значение блока.

let x = {
    let y = 1; // первое выражение
    let z = 2; // второв выражение
    y + z // это *хвост*, его значение будет "возвращено" как значение выражения
};

Мой основной язык Python как раз не позволяет легко выделять логические блоки. Такое свойство языка скорее поощряет создавать больше различных функций/методов.

Однако и здесь мы можем предложить некоторые идеи, как можно использовать подобную технику. Например, если логический шаг в методе предполагает единственное выражение, мы можем не выносить его в отедльную функцию, но вместо этого использовать lambda-выражение, с комментарием, вроде:

def exec_command(self) -> None:

    """
    Блок парсинга команды
    """
    parse_command = lambda: (
        (lambda command_str: (
            command_str.split(" ", 1)[0].strip().lower(),  # command
            command_str.split(" ", 1)[1].strip() if len(command_str.split(" ", 1)) > 1 else None  # args
        ))(self.command_str_list[self.current_command])
        if not self.command_str_list[self.current_command].split(" ", 1)[0].strip().lower().endswith(":")
        else ("mark", None)
    )

    """
    Блок выполнения команды:
    """
    command, args = parse_command()
    if command == "mark":
        print("Mark command processed.")
    elif hasattr(self.command_cls, command):
        getattr(self.command_cls, command)(self, args)
    else:
        raise NonValidProgram(f'"{command}" not implemented')

    ...

Конечно, такой подход не идеален, но может найти применение в отедльных случаях.

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

Борясь с циклическими импортами, мы зачастую вводим различные условия, вроде if TYPE_CHECKING: import some_module и т.д. Но объединение модулей в один позволяет решить и эту проблему.

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

Группировка логики программы в последовательность логических блоков - это очень мощная техника, которая позволяет сделать нашу программу более “императивной”, заменяя множество методов и вызовов четкой последовательностью инструкций внутри единственной функции. Если нет необходимости в переиспользовании методов, такой подход будет разумной альтернативной разделению, так как на мой взгляд приводит к более легко читаемому и воспринимаемому коду.