Схожеcть принципов в ООП и ФП - Часть 2


Продолжая тему с применением традиционных подходов из ООП практики в функциональном программировании.

Мы уже рассмотрели, как принцип разделения интерфейсов из SOLID вполне успешно может использоваться и в ФП. Для убедительности, рассмотрим еще одну букву из знаменитого набора принципов, и попробуем применить принцип “Инверсии управления”, он же Dependency Inversion в функциональном коде.

Основная идея DI заключается в том, чтобы зависимости (внешние компоненты или сервисы) передавались через аргументы конструктора класса, а не создавались внутри них. Это позволяет легче тестировать код и подменять зависимости на другие реализации.

Если мы рассмотрим эту идею и попробуем перенести её на ФП, то обнаружится следующее: да, у нас нет классов/конструкторов классов, всё это заменяют собой функции (функции, ещё функции, больше функций!). Так как использовать DI здесь? Просто вынести элементы из тела функции в ёё параметры. Вот так просто. Также часто функциональные языки (такие, как раз OCaml) дают нам ещё механизмы для этого подхода, например систему модулей и возможность (ограниченную) операций над ними.

Рассмотрим пример DI на OCaml с использованием модулей и функций:

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

Создаем интерфейс через подпись модуля:

module type Database = sig
  val get_user : int -> string
end

Реализуем (кхм) “““настоящую””” базу данных:

module RealDatabase : Database = struct
  let get_user user_id =
    (* Имитация запроса к реальной базе данных *)
    Printf.sprintf "User with id: %d" user_id
end

Создаем заглушку для тестирования:

module MockDatabase : Database = struct
  let get_user user_id =
    (* Заглушка для тестирования *)
    "Test user"
end

Используем зависимости через внедрение:

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

let print_user_info (module Db : Database) user_id =
  let user_info = Db.get_user user_id in
  Printf.printf "User info: %s\n" user_info

Использование реальной базы данных:

let () =
  print_user_info (module RealDatabase) 1

Использование заглушки для тестирования:

let () =
  print_user_info (module MockDatabase) 1

Внедрение зависимостей через функции.

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

let get_user_from_db db_get_user user_id =
  let user_info = db_get_user user_id in
  Printf.printf "User info: %s\n" user_info

(* Используем функцию для реальной базы данных *)
let () =
  get_user_from_db RealDatabase.get_user 1

(* Используем функцию для заглушки *)
let () =
  get_user_from_db MockDatabase.get_user 1

(* В общем такой подход принципиально не отличается от рассмотренного выше
   просто демонстрируем, что есть разные способы подхода к одной и той же проблеме *)

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

Таким образом, в функциональном стиле зависимости могут быть переданы через аргументы функций или модулей, что является аналогом Dependency Injection в объектно-ориентированном программировании. Это позволяет легко подменять зависимости для разных контекстов, таких как тестирование и реальная работа. Теперь мы ещё раз убедились, что фундаментальные подходы в программировании относятся к нему в общем и целом, и постигая хорошие и проверенные принципы разработчик может стать лучше в общем, а не просто в рамках какой-то одной парадигмы.