Введение

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