Введение

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

Комментарии (5)


  1. BeceJlb4ak
    08.10.2025 10:53

    Зачем нужна обертка над лямбдой - только ради инжекта?


    1. Naf2000
      08.10.2025 10:53

      Функциональный объект может в себе хранить, например, другие объекты. Безусловно все можно реализовать и чисто лямбдами. Насколько удобно - дело вкуса. В конце концов замыкания лямбд это и есть анонимные (скрытые) объекты.

      Как пример вычисление скидки, где объект либо является элементарной скидкой, либо коллекцией скидок, которые могут браться по максимуму или суммироваться или приоритет... Причем сами элементы коллекции сами являются такими же скидками - получаем дерево скидок, которое вычитывается из базы данных, например, динамически.


  1. Dhwtj
    08.10.2025 10:53

    Откуда вы эти антипатерны достали? Хватит по чуланам шариться.

    Даже у Шарп, который в основе ООП умеет с функциями как

    First-class citizen — функция может всё, что и другие значения:

    • Присвоить переменной

    • Передать в функцию

    • Вернуть из функции

    • Создать "на лету"

    И вместо вашего кода такие варианты

    // Вместо интерфейса делегаты
    Action ExecuteOperation = () => invoiceProvider.Pay();
    Func<Task<Result>> AsyncOperation = async () => await ProcessPayment();
    
    //Railway-oriented (как у Влашина):
    var result = await GetInvoice(id)
        .Bind(ValidateInvoice)
        .Map(CalculateDiscount)
        .Bind(ProcessPayment)
        .Match(
            onSuccess: inv => Ok(inv),
            onFailure: err => BadRequest(err)
        );
    
    //3. С discriminated unions (через OneOf или свои):
    public OneOf<Success, ValidationError, PaymentError> PayInvoice(int id) =>
        GetInvoice(id) switch {
            NotFound => new ValidationError("Invoice not found"),
            Invoice inv when !inv.IsValid => new ValidationError("Invalid"),
            Invoice inv => ProcessPayment(inv)
        };
    
    //Новый C# 12 с collection expressions
    var operations = [
        () => invoiceProvider.Pay(),
        () => logger.Log("Paid"),
        () => notifier.Send()
    ];
    
    //Source generators для pipeline
    [Pipeline]
    partial class PaymentPipeline {
        Step1 ValidateUser(Request r) => ...
        Step2 CheckBalance(Step1 s) => ...
        Step3 ProcessPayment(Step2 s) => ...
    }


    1. antonb73 Автор
      08.10.2025 10:53

      Честно говоря, имел бы возможность вас забанить, то с удовольствием сделал бы это. Не люблю хамов любого пошива.

      Ещё не люблю хитрецов подменяющих понятия. В ваших примерах это не C# умеет с функциями так, а это конкретные типы имеют соответствующие методы, а последний пример, это вообще отдельная библиотека которая занимается кодогенерацией.

      Прежде чем писать подумайте в чём ценность вашего комментария.


      1. Cregennan
        08.10.2025 10:53

        Странный аргумент "это не C# умеет с функциями так".
        => - конструкция языка
        return switch Invoice inv when ... => - конструкция языка
        [() => return notifier.Send()] - конструкция языка

        Про "конкретные типы имеют соответствующие методы" - у вас FunctionObject.Execute это конечно же не тип с методом?

        Переименуйте PayInvoiceOperation в PayInvoiceService, Invoke в PayAsync, получите обычный "сервис" которые прямо сейчас пишут в любых проектах и не только на дотнете.