Применяем функциональную композицию к ООП коду


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

И все же в большинстве случаев мы имеем дело с типичным ООП похдодом к написанию программ (несмотря даже на то, что для языка с динамической типизацией, многие типичные для ООП концепции, такие как полиморфизм подтипов, не обеспечиваются при “компиляции” программы).

Рассмотрим следующий пример:

Допустим, мы имеем класс, который расчитывает данные по наиболее близким по расстоянию строкам. Один из методов этого класса сохраняет результат в DataFrame (библиотека pandas). Если подумать, этот метод противоречит принципу SRP, так как сохранение результата в какой-то формат это уже отдельная задача и потенциально этих форматов может быть множество. Кроме того, можно рассмотреть ситуацию, когда нам дальше нужно использовать этот результат и сохранить его в файл.

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

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

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

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

class LSC(DataHandler):
    ...
    def to_dataframe(self):
        df = pd.DataFrame({
            'client_sku': self.client_sku,
            'base_sku': self.base_sku,
            'common_string': self.common_string
        })
        res_grp = df.groupby('client_sku', as_index=False).agg({'common_string': self.max_len_str})
        res = res_grp.merge(df, how='inner', on=['client_sku', 'common_string'])
        return res

Преобразуем в:

def datahander_res_to_dataframe(dh: DataHander) -> pd.DataFrame:
    df = pd.DataFrame({
        'client_sku'   : dh.client_sku,
        'base_sku'     : dh.base_sku,
        'common_string': dh.common_string
    })
    res_grp = df.groupby('client_sku', as_index=False).agg({'common_string': dh.max_len_str})
    res = res_grp.merge(df, how='inner', on=['client_sku', 'common_string'])
    return res

def dataframe_to_csv(path: str, df: pd.DataFrame) -> None:
    df.to_csv(path)

"""
Пример вызова
"""

dataframe_to_csv(
    "res.csv",
    datahander_res_to_dataframe(lsc_handler),
)

Общая идея заключается в том, что мы выносим части логики работы программы:

  • которые автономны от остальных частей класса.
  • могут меняться часто.

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

Описанный подход позволяет сократить значительно объём кода, сделав операции, которые не совсем укладываются в SRP основного класса, не создавая при этом большие нагромождения иерархий новых классов. При этом мы:

  • избегаем прегруженности класса методами, напрямую не относящимся к его обязанностям (соблюдаем SRP).
  • делаем зависимости более простыми, читаемыми и понятными.
  • явно выражаем логику действий.

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

Кроме того, таким образом код выглядит гораздо читабельнее, ведь композиция из 2-3 функций довольно легко воспринимается, в отличие от случая, когда мы используем несколько классов в составе композиции другого класса.

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