Продолжая тему с применением традиционных подходов из ООП практики в функциональном программировании.
Мы уже рассмотрели, как принцип разделения интерфейсов из 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 в объектно-ориентированном программировании. Это позволяет легко подменять зависимости для разных контекстов, таких как тестирование и реальная работа. Теперь мы ещё раз убедились, что фундаментальные подходы в программировании относятся к нему в общем и целом, и постигая хорошие и проверенные принципы разработчик может стать лучше в общем, а не просто в рамках какой-то одной парадигмы.