Введение
В предыдущей статье "Адаптированный паттерн Command с использованием Dependency Injection", я описывал как инкапсуляция логики приложений в отдельные объекты-функции позволяет получить преимущества в архитектуре приложений.
В качестве основы для концепции объекта-функции мной был выбран известный паттерн Command, но обсуждение статьи показало, что читателям тяжело отказатся от слишком узкой специфики паттерна Command и это мешяет восприятию материала.
Эта статья пытается исправиль допущенную автором ошибку.
Статья является дополнением к предыдущей.
Примеры кода и демо проект
Все примеры в статье и демо проект даны на C#. Демо проект показывает архитектуру приложения состоящую исключительно из Function Object с использованием DI.
https://github.com/abaula/guess_number
Демо проект может служить пособием для изучения и экспериментов.
Краткая справка о термине
Термин Функция-объект (Function Object, или Functor) в контексте объектно-ориентированного программирования появился как естественное развитие концепции функций как объектов из функционального программирования и возможностей ООП.
Концепция функциональных объектов присутствует в теории объектно-ориентированной и функциональной парадигм программирования задолго до формализации конкретных паттернов в таких трудах, как «Gang of Four» (1994), где этот паттерн в классическом понимании явно не выделялся, но был широко применяем в практике программирования.
Идея функции-объекта возникла главным образом с появлением и развитием языков программирования, поддерживающих перегрузку операторов и возможность создавать объекты, которые можно вызывать как функции. Одним из первых таких языков был C++, разработанный Бьёрном Страуструпом в 1980-х годах, в котором появилась возможность перегружать оператор вызова (operator()) для создания функций-объектов. Это позволило инкапсулировать состояние и поведение в одном объекте, который можно использовать как функцию.
Таким образом, термин и концепция Function Object не имеют единственного изобретателя или конкретной даты введения — это результат развития языка C++ и идей функционального программирования, где функции рассматриваются как объекты первого класса. Эти идеи развивались в 1980-х и 1990-х годах в процессе эволюции объектно-ориентированных языков.
Кратко история:
Истоки в функциональном программировании и концепции функций как сущностей (lambda calculus, 1936; язык LISP, 1950-е).
Эволюция ООП с развитием языков Simula, Smalltalk, Objective-C и особенно C++.
Введение в C++ возможности перегрузки оператора вызова функции позволило создавать функцию-объекты, которые стали широко использоваться.
Термин "Function Object" как такой стал употребляться в сообществе разработчиков C++ и смежных языков с конца 1980-х — начала 1990-х годов.
Это был естественный шаг эволюции языков, обобщающий функциональные и объектные концепции.
Преимущества реализации бизнес логики приложения с использованием Function Object
Использование Function Object для реализации бизнес-логики приложения дает значительные преимущества:
Разделение ответственности и повторное использование
Function Object позволяет выносить бизнес-логику в отдельные сущности, которые легко переиспользовать в разных частях приложения и даже в других проектах. Такой подход способствует соблюдению принципа единственной ответственности (SRP) и предотвращает дублирование логики.
Улучшение тестируемости
Function Object — это изолированные сущности, их просто тестировать отдельно от остального приложения. Из-за отсутствия глобального состояния и явной передачи зависимостей тесты становятся более простыми и надежными.
Повышение читаемости и структуры кода
Код с Function Object становится более декларативным и структурированным. Каждый объект отвечает за отдельную бизнес-операцию, что упрощает сопровождение: изменения в одной функции-объекте минимально затрагивают остальной код.
Гибкость и расширяемость
Function Object легко комбинировать, декорировать или изменять через композицию, что позволяет просто реализовывать сложные бизнес-процессы и сценарии без усложнения архитектуры.
Инкапсуляция состояния и зависимостей
Function Object может содержать внутреннее состояние или хранить параметры, необходимые для выполнения бизнес-операции, что делает его удобным инструментом для реализации логики с учетом различных контекстов и условий выполнения.
Все эти преимущества особенно проявляются в сервисных и распределённых приложениях с интенсивным повторным использованием бизнес-кода.
Примеры Function Object в реальных C# проектах
В реальных C# проектах Function Object часто реализуются через классы, содержащие бизнес-логику, и используются для инкапсуляции отдельных операций, что упрощает повторное использование и тестирование, например, обработка платежа, проверка пользователя или запуск сложной бизнес-функции как объекта.
Вот несколько распространённых примеров, которые демонстрируют подходы, при которых бизнес-операции приложения реализуются и вызываются как объекты, позволяя формировать гибкую, масштабируемую и тестируемую архитектуру.
Реализация бизнес-операции через функциональный объект
public interface IBusinessOperation { void Execute(); } // Function Object: каждая операция инкапсулируется в отдельном классе public class PayInvoiceOperation : IBusinessOperation { private readonly InvoiceProvider _invoiceProvider; public PayInvoiceOperation(InvoiceProvider invoiceProvider) { _invoiceProvider = invoiceProvider; } public void Execute() { // Логика оплаты счета _invoiceProvider.Pay(); // Обработка ошибок, логирование и т.д. // ... } }
Такой объект можно прокидывать в сервисы, коллекции, запускать по очереди, тестировать отдельно и расширять.
Использование делегатов и лямбда-выражений
Function Object в C# зачастую реализуют через делегаты, позволяя инкапсулировать не только методы класса, но и любые функции, подходящие по сигнатуре:
public interface IBusinessOperation { void Execute(); } public class FunctionObject : IBusinessOperation { private readonly Action _action; public FunctionObject(Action action) { _action = action; } public void Execute() => _action(); }
Такой подход часто используется, например, для событий, коллбеков или обработки команд из UI. Также из примеров видно, что такой подход позволяет сохранять состояние отдельных методов для последующего использования и объединять различные методы под единым интерфейсом, что практически реализует известный шаблон Command.
Мой опыт применения Function Object в реальных проектах
Мой последний проект состоял из более чем 250 микросервисов, и включал в себя такие группы функций как ETL, Search, RAG, и только в части из них были использованы Function Object. Если бы можно было начать проект сначала, то я бы предпочёл реализовать все модули без исключения на Function Object.
Можно спорить хорошо это или не очень, но на мой субъективный взгляд удалось бы сэкономить 10-15% времени, без проблем покрыть код проекта, хотя бы самые критические части, модульными тестами.
