На настоящий день наиболее популярным стилем программирования на 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 функций довольно легко воспринимается, в отличие от случая, когда мы используем несколько классов в составе композиции другого класса.
В целом, этот подход наверное более “ленивый”, чем корректная композиция из нужных классов, однако если нам нужно всего лишь добавить некоторые несложные операции, которые не входят в обязанности класса, мы выигрываем значитльно в простоте и читаемости итоговой программы.