Введение
В предыдущей статье "Адаптированный паттерн 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% времени, без проблем покрыть код проекта, хотя бы самые критические части, модульными тестами.