Современное программирование — это не только решение функциональных задач, но и создание кода, который удобно поддерживать, расширять и читать. Цепочки методов и Fluent-синтаксис в C# стали мощным инструментом, который значительно повышает читабельность и выразительность кода, особенно при работе со сложными объектами или последовательностями операций. Что же представляют собой цепочки методов? Это программная техника, которая позволяет вызывать несколько методов последовательно в одном выражении. Каждый метод в цепочке возвращает объект (обычно тот же самый), что позволяет следующему методу быть вызванным на этом возвращенном объекте. Процесс продолжается до завершения всей цепочки.
Fluent-синтаксис — это расширение концепции цепочек методов, которое делает код более естественным, почти "текучим" (отсюда и название "fluent" — плавный, беглый). Он позволяет описывать действия с объектами в форме, близкой к естественному языку. Вместо серии отдельных команд программист получает возможность выстраивать выразительные цепочки операций.
C# | 1
2
3
4
5
6
7
8
9
10
11
| // Традиционный подход
var settings = new Settings();
settings.SetTheme("Dark");
settings.EnableNotifications();
settings.SetFontSize(14);
// Fluent-подход
var settings = new Settings()
.SetTheme("Dark")
.EnableNotifications()
.SetFontSize(14); |
|
Преимущества Fluent API в C# проектах очевидны: код становится более лаконичным, самодокументируемым и эстетически привлекательным. Такой стиль программирования улучшает читаемость, особенно при работе с конфигурационными классами или при построении сложных объектов. Кроме того, он естественным образом ведет к более декларативному стилю программирования, где программист описывает что должно быть сделано, а не как именно это делать.
Исторически концепция Fluent API восходит к шаблону проектирования "Строитель" (Builder pattern), который был формализован в книге "Банда четырех" еще в 1994 году. Однако широкое распространение в C# этот подход получил с развитием LINQ (Language Integrated Query) в .NET Framework 3.5, где цепочки методов стали естественным способом построения запросов к данным. В .NET Fluent API прошел значительную эволюцию. От простых цепочек методов в LINQ он расширился до сложных конфигурационных API в Entity Framework, Fluent Validation и других популярных библиотеках. Сегодня это не просто удобный синтаксический сахар, а мощный инструмент проектирования API, который позволяет создавать предметно-ориентированные языки (DSL) внутри C#.
Fluent API особенно ярко демонстрирует свои преимущества при сравнении с традиционным подходом на реальных примерах. Допустим, нам нужно создать и настроить объект калькулятора:
C# | 1
2
3
4
5
6
7
8
9
10
11
| // Традиционный подход
var calculator = new Calculator(10);
calculator.Add(5);
calculator.Multiply(2);
var result = calculator.Result(); // результат = 30
// Fluent-подход
var result = new Calculator(10)
.Add(5)
.Multiply(2)
.Result(); // результат = 30 |
|
В первом случае мы создаем переменную, а затем последовательно вызываем на ней методы. Во втором — выстраиваем единую цепочку, которая выглядит как одна логическая операция. При увеличении количества операций разница в читаемости становится еще более выраженной.
Основы реализации
Чтобы создать эффективный Fluent API в C#, необходимо понимать ключевые принципы его реализации. Сердцевина этого подхода — возврат текущего объекта (this) из каждого метода цепочки, что обеспечивает возможность последовательного вызова методов. Рассмотрим базовую реализацию класса, поддерживающего цепочки методов.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public class Person
{
private string _name;
private int _age;
public Person SetName(string name)
{
_name = name;
return this;
}
public Person SetAge(int age)
{
_age = age;
return this;
}
public void ShowDetails()
{
Console.WriteLine($"Имя: {_name}, Возраст: {_age}");
}
} |
|
Обратите внимание на ключевые компоненты этой реализации:
1. Методы SetName и SetAge возвращают экземпляр класса Person .
2. Внутри каждого метода происходит модификация состояния объекта.
3. Последний метод ShowDetails прерывает цепочку, возвращая void .
Использование выглядит так:
C# | 1
2
3
4
5
| var person = new Person()
.SetName("Мария")
.SetAge(27);
person.ShowDetails(); // Вывод: Имя: Мария, Возраст: 27 |
|
При разработке Fluent API важно учитывать состояние объекта в цепочке вызовов. В простых случаях достаточно модифицировать внутреннее состояние и возвращать this. Однако в более сложных сценариях может потребоваться сохранение промежуточных состояний или валидация после каждого шага.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| public class PersonBuilder
{
private string _name;
private int _age;
public PersonBuilder WithName(string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("Имя не может быть пустым");
_name = name;
return this;
}
public PersonBuilder WithAge(int age)
{
if (age < 0 || age > 150)
throw new ArgumentException("Некорректный возраст");
_age = age;
return this;
}
public Person Build()
{
if (string.IsNullOrEmpty(_name))
throw new InvalidOperationException("Имя не задано");
return new Person(_name, _age);
}
} |
|
Для сохранения контекста в длинных цепочках методов используют несколько техник:
1. Каскадная модель — каждый метод возвращает непосредственно this .
2. Поэтапное построение — методы могут возвращать разные объекты, ограничивая доступные операции на каждом этапе.
3. Вложенные билдеры — для сложных структур используют вспомогательные билдеры, отвечающие за части объекта.
При реализации Fluent API разработчики часто сталкиваются с проблемой ссылочной прозрачности. Ссылочная прозрачность означает, что выражение можно заменить его значением без изменения поведения программы. В случае с изменяемыми объектами в цепочках методов эта прозрачность может нарушаться, создавая трудно отслеживаемые побочные эффекты.
Рассмотрим пример:
C# | 1
2
3
4
5
| var builder = new QueryBuilder().Where("age > 18");
var query1 = builder.OrderBy("name").Build();
var query2 = builder.OrderBy("age").Build();
// Что будет содержать query2? |
|
Если QueryBuilder изменяет внутреннее состояние, то query2 будет содержать условия и из Where("age > 18") , и из обоих вызовов OrderBy . Это может быть неочевидно и привести к ошибкам. Решением проблемы ссылочной прозрачности может быть использование неизменяемых (immutable) объектов, когда каждый метод в цепочке возвращает новый экземпляр с обновленным состоянием:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public class ImmutableQueryBuilder
{
private readonly List<string> _conditions;
private readonly List<string> _orderBy;
public ImmutableQueryBuilder()
{
_conditions = new List<string>();
_orderBy = new List<string>();
}
private ImmutableQueryBuilder(List<string> conditions, List<string> orderBy)
{
_conditions = new List<string>(conditions);
_orderBy = new List<string>(orderBy);
}
public ImmutableQueryBuilder Where(string condition)
{
var newConditions = new List<string>(_conditions) { condition };
return new ImmutableQueryBuilder(newConditions, _orderBy);
}
public ImmutableQueryBuilder OrderBy(string field)
{
var newOrderBy = new List<string>(_orderBy) { field };
return new ImmutableQueryBuilder(_conditions, newOrderBy);
}
} |
|
Такой подход с неизменяемыми объектами делает код более предсказуемым. Теперь наш пример даст ожидаемый результат:
C# | 1
2
3
| var baseBuilder = new ImmutableQueryBuilder().Where("age > 18");
var query1 = baseBuilder.OrderBy("name").Build();
var query2 = baseBuilder.OrderBy("age").Build(); // Содержит только условие + сортировку по возрасту |
|
Еще одним важным аспектом при проектировании Fluent API является оптимизация памяти. При интенсивном использовании цепочек методов, особенно с неизменяемыми объектами, может происходить излишнее создание временных объектов. Это может привести к повышенной нагрузке на сборщик мусора и снижению производительности. Для минимизации этих эффектов можно применить несколько стратегий:
1. Пул объектов — создание заранее некоторого количества объектов и их повторное использование вместо создания новых:
C# | 1
2
3
4
5
6
7
8
| public class ObjectPool<T> where T : class, new()
{
private readonly ConcurrentBag<T> _objects = new ConcurrentBag<T>();
public T Get() => _objects.TryTake(out T item) ? item : new T();
public void Return(T item) => _objects.Add(item);
} |
|
2. Отложенное выполнение — выполнение операций только в момент вызова терминального метода, без создания промежуточных объектов:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class LazyQueryBuilder
{
private readonly List<Func<string, string>> _transformations = new List<Func<string, string>>();
public LazyQueryBuilder Where(string condition)
{
_transformations.Add(query => query + " WHERE " + condition);
return this;
}
public LazyQueryBuilder OrderBy(string field)
{
_transformations.Add(query => query + " ORDER BY " + field);
return this;
}
public string Build()
{
return _transformations.Aggregate("SELECT * FROM Users", (query, transform) => transform(query));
}
} |
|
3. Структуры вместо классов — для простых цепочек можно использовать структуры (struct ), которые размещаются в стеке, а не в куче:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public struct ValueQueryBuilder
{
private string _query;
public ValueQueryBuilder(string tableName)
{
_query = $"SELECT * FROM {tableName}";
}
public ValueQueryBuilder Where(string condition)
{
_query += " WHERE " + condition;
return this;
}
public string Build() => _query;
} |
|
Важно помнить, что структуры передаются по значению, а не по ссылке, поэтому при каждом вызове метода в цепочке создается копия структуры. Это может быть эффективно только для очень маленьких структур.
Особое внимание стоит уделить случаям, когда Fluent API используется в многопоточной среде. Классические реализации с изменяемым состоянием не являются потокобезопасными. Для создания потокобезопасного Fluent API можно использовать:
1. Синхронизацию доступа к изменяемому состоянию объекта.
2. Неизменяемые объекты, которые безопасны для одновременного использования в разных потоках.
3. Thread-local экземпляры билдеров, уникальные для каждого потока.
Пример потокобезопасного билдера с блокировками:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public class ThreadSafeBuilder
{
private readonly object _lock = new object();
private string _name;
public ThreadSafeBuilder WithName(string name)
{
lock (_lock)
{
_name = name;
return this;
}
}
public string Build()
{
lock (_lock)
{
return _name;
}
}
} |
|
Альтернативный подход с использованием неизменяемых объектов:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class ImmutableBuilder
{
private readonly string _name;
public ImmutableBuilder() : this(string.Empty) { }
private ImmutableBuilder(string name)
{
_name = name;
}
public ImmutableBuilder WithName(string name)
{
return new ImmutableBuilder(name);
}
public string Build() => _name;
} |
|
Выбор конкретного подхода зависит от требований к производительности, потребления памяти и структуры приложения. Правильно спроектированный Fluent API должен не только обеспечивать удобство использования, но и минмизировать потребление ресурсов.
Помимо структурных аспектов, при проектировании Fluent API важно уделить внимание обработке ошибок. В традиционном коде мы часто используем блоки try-catch, но как обрабатывать исключения в цепочке методов? Один из подходов, обработка исключений на каждом шаге цепочки с возвратом специального объекта-результата:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| public class Result<T>
{
public bool IsSuccess { get; }
public string Error { get; }
public T Value { get; }
private Result(bool isSuccess, T value, string error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}
public static Result<T> Success(T value) => new Result<T>(true, value, null);
public static Result<T> Failure(string error) => new Result<T>(false, default, error);
}
public class FluentCalculator
{
private int _value;
private string _error;
public FluentCalculator(int initial)
{
_value = initial;
}
public FluentCalculator Divide(int divisor)
{
if (divisor == 0)
{
_error = "Деление на ноль недопустимо";
return this;
}
_value /= divisor;
return this;
}
public Result<int> GetResult()
{
if (_error != null)
return Result<int>.Failure(_error);
return Result<int>.Success(_value);
}
} |
|
Такой подход позволяет продолжать цепочку даже при возникновении ошибки и проверять успешность выполнения только в конце:
C# | 1
2
3
4
5
6
7
8
9
10
| var result = new FluentCalculator(10)
.Divide(2)
.Divide(0) // Ошибка, но цепочка продолжается
.Divide(5)
.GetResult();
if (result.IsSuccess)
Console.WriteLine($"Результат: {result.Value}");
else
Console.WriteLine($"Ошибка: {result.Error}"); |
|
Другой важный аспект — методы-терминаторы. Эти методы завершают цепочку и обычно возвращают конечный результат вместо this . Типичные примеры терминаторов:
Build() — создает финальный объект,
Execute() — выполняет операцию,
ToString() — возвращает строковое представление,
ToList() , ToArray() — преобразуют в коллекции.
Терминаторы играют ключевую роль в дизайне Fluent API, поскольку они:
1. Сигнализируют о завершении конфигурации.
2. Часто запускают фактическое выполнение отложенных операций.
3. Валидируют корректность всей конфигурации перед созданием финального объекта.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| public class EmailBuilder
{
private string _to;
private string _subject;
private string _body;
public EmailBuilder To(string recipient)
{
_to = recipient;
return this;
}
public EmailBuilder WithSubject(string subject)
{
_subject = subject;
return this;
}
public EmailBuilder WithBody(string body)
{
_body = body;
return this;
}
// Метод-терминатор
public Email Build()
{
if (string.IsNullOrEmpty(_to))
throw new InvalidOperationException("Получатель не указан");
return new Email(_to, _subject, _body);
}
// Альтернативный терминатор
public void Send()
{
var email = Build();
// Логика отправки email
Console.WriteLine($"Email отправлен на адрес {_to}");
}
} |
|
Интересная техника расширения возможностей Fluent API — использование extension-методов. Они позволяют добавлять методы к существующим типам без изменения их исходного кода:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public static class StringExtensions
{
public static StringBuilder AppendIf(this StringBuilder builder, bool condition, string value)
{
if (condition)
builder.Append(value);
return builder;
}
public static StringBuilder AppendLineIf(this StringBuilder builder, bool condition, string value)
{
if (condition)
builder.AppendLine(value);
return builder;
}
} |
|
Теперь можно использовать эти методы в цепочках с StringBuilder :
C# | 1
2
3
4
5
6
7
8
9
10
| var isAdmin = true;
var isNewUser = false;
var message = new StringBuilder()
.Append("Здравствуйте, ")
.Append(userName)
.AppendLine("!")
.AppendIf(isAdmin, "У вас есть права администратора. ")
.AppendLineIf(isNewUser, "Добро пожаловать в систему!")
.ToString(); |
|
Еще одна важная концепция — комбинирование Fluent API с функциональным программированием. Это позволяет создавать очень гибкие и выразительные интерфейсы:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| public class Query<T>
{
private readonly IEnumerable<T> _source;
private readonly List<Func<T, bool>> _filters = new List<Func<T, bool>>();
public Query(IEnumerable<T> source)
{
_source = source;
}
public Query<T> Where(Func<T, bool> predicate)
{
_filters.Add(predicate);
return this;
}
public IEnumerable<T> Execute()
{
IEnumerable<T> result = _source;
foreach (var filter in _filters)
{
result = result.Where(filter);
}
return result;
}
} |
|
Использование:
C# | 1
2
3
4
5
6
7
| var users = new List<User>() { /* ... */ };
var activeAdmins = new Query<User>(users)
.Where(u => u.IsActive)
.Where(u => u.Role == "Admin")
.Execute()
.ToList(); |
|
Подобный подход позволяет создавать гибкие и мощные цепочки методов, которые сочетают декларативный стиль Fluent API с выразительностью функционального программирования.
Fluent API One-to-Many в одной структуре Здравствуйте, имею следующий класс
class MenuDB
{
...
public virtual... EF Code First - Fluent api conditional mapping ? При подходе DatabaseFirst есть возможность пропускать значения по некоторому условию (см. скриншот... Комбинирование DataAnnotations и Fluent API (Code First) Есть модель, у которой указываются следующие аттрибуты:
// здесь указываю аттрибуты лишь для... EF Fluent API Всем доброго времени суток!
Скажите пожалуйста в чём суть этого Fluent API заключается, в...
Практическое применение
Теория Fluent API становится по-настоящему полезной, когда мы начинаем применять её в реальных проектах. Одним из самых распространённых сценариев использования цепочек методов является построение строк и конфигурирование объектов. Рассмотрим, как это работает на практике.
Построение строк и конфигураций
Построение сложных строк — классический пример, где Fluent-синтаксис демонстрирует свои преимущества. Помимо стандартного StringBuilder , можно создать специализированные билдеры для различных форматов:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| public class SqlQueryBuilder
{
private string _table;
private readonly List<string> _columns = new List<string>();
private readonly List<string> _conditions = new List<string>();
private readonly List<string> _orderBy = new List<string>();
private int? _limit;
public SqlQueryBuilder FromTable(string tableName)
{
_table = tableName;
return this;
}
public SqlQueryBuilder Select(params string[] columns)
{
_columns.AddRange(columns);
return this;
}
public SqlQueryBuilder Where(string condition)
{
_conditions.Add(condition);
return this;
}
public SqlQueryBuilder OrderBy(string column, bool ascending = true)
{
_orderBy.Add($"{column} {(ascending ? "ASC" : "DESC")}");
return this;
}
public SqlQueryBuilder Limit(int count)
{
_limit = count;
return this;
}
public string Build()
{
if (string.IsNullOrEmpty(_table))
throw new InvalidOperationException("Таблица не указана");
var query = new StringBuilder();
query.Append("SELECT ");
query.Append(_columns.Any() ? string.Join(", ", _columns) : "*");
query.Append($" FROM {_table}");
if (_conditions.Any())
query.Append($" WHERE {string.Join(" AND ", _conditions)}");
if (_orderBy.Any())
query.Append($" ORDER BY {string.Join(", ", _orderBy)}");
if (_limit.HasValue)
query.Append($" LIMIT {_limit}");
return query.ToString();
}
} |
|
Использование такого билдера превращает создание SQL-запросов в интуитивно понятный процесс:
C# | 1
2
3
4
5
6
7
8
9
10
| var query = new SqlQueryBuilder()
.FromTable("Users")
.Select("Id", "Name", "Email")
.Where("Age > 18")
.Where("IsActive = 1")
.OrderBy("LastLogin", ascending: false)
.Limit(10)
.Build();
// Результат: SELECT Id, Name, Email FROM Users WHERE Age > 18 AND IsActive = 1 ORDER BY LastLogin DESC LIMIT 10 |
|
Для конфигурирования приложений Fluent API также показывает себя с лучшей стороны. Вот пример конфигуратора веб-сервиса:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| public class WebServiceConfigurator
{
private readonly WebServiceOptions _options = new WebServiceOptions();
public WebServiceConfigurator WithPort(int port)
{
_options.Port = port;
return this;
}
public WebServiceConfigurator WithMaxConnections(int maxConnections)
{
_options.MaxConnections = maxConnections;
return this;
}
public WebServiceConfigurator EnableLogging(bool enabled = true)
{
_options.LoggingEnabled = enabled;
return this;
}
public WebServiceConfigurator WithAuthenticationProvider(IAuthProvider provider)
{
_options.AuthProvider = provider;
return this;
}
public WebService Create()
{
return new WebService(_options);
}
} |
|
Такой подход значительно упрощает создание и настройку сложных объектов:
C# | 1
2
3
4
5
6
| var service = new WebServiceConfigurator()
.WithPort(8080)
.WithMaxConnections(100)
.EnableLogging()
.WithAuthenticationProvider(new OAuth2Provider())
.Create(); |
|
Кастомные билдеры для сложных бизнес-объектов
В реальных бизнес-приложениях часто встречаются сложные доменные объекты с множеством взаимосвязанных свойств и валидационных правил. Традиционное создание таких объектов может быть запутанным и подверженным ошибкам. Fluent API позволяет структурировать этот процесс. Рассмотрим пример создания заказа в интернет-магазине:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| public class OrderBuilder
{
private readonly Order _order = new Order();
private readonly List<OrderItem> _items = new List<OrderItem>();
private readonly List<string> _validationErrors = new List<string>();
public OrderBuilder ForCustomer(Customer customer)
{
_order.CustomerId = customer.Id;
_order.CustomerEmail = customer.Email;
return this;
}
public OrderBuilder WithShippingAddress(Address address)
{
_order.ShippingAddress = address;
return this;
}
public OrderBuilder WithPaymentMethod(PaymentMethod paymentMethod)
{
_order.PaymentMethod = paymentMethod;
return this;
}
public OrderBuilder AddItem(Product product, int quantity)
{
if (quantity <= 0)
{
_validationErrors.Add($"Количество для продукта {product.Id} должно быть положительным");
return this;
}
var item = new OrderItem
{
ProductId = product.Id,
ProductName = product.Name,
Price = product.Price,
Quantity = quantity
};
_items.Add(item);
return this;
}
public Order Build()
{
// Проверяем наличие ошибок валидации
if (_validationErrors.Any())
throw new OrderValidationException(_validationErrors);
// Проверяем обязательные поля
if (_order.CustomerId == 0)
throw new OrderValidationException("Не указан покупатель");
if (_order.ShippingAddress == null)
throw new OrderValidationException("Не указан адрес доставки");
if (!_items.Any())
throw new OrderValidationException("Корзина пуста");
// Рассчитываем итоговую сумму
_order.TotalAmount = _items.Sum(i => i.Price * i.Quantity);
_order.Items = _items;
_order.CreatedAt = DateTime.UtcNow;
return _order;
}
} |
|
Такой билдер обеспечивает удобный и безопасный способ создания заказов:
C# | 1
2
3
4
5
6
7
| var order = new OrderBuilder()
.ForCustomer(customer)
.WithShippingAddress(address)
.WithPaymentMethod(PaymentMethod.CreditCard)
.AddItem(laptop, 1)
.AddItem(mouse, 2)
.Build(); |
|
Еще одним убедительным примером применения Fluent API является обработка коллекций. Язык интегрированных запросов LINQ в .NET — это, возможно, самый известный пример Fluent-интерфейса, который стал неотъемлемой частью повседневной работы C#-разработчиков.
Обработка коллекций и LINQ
LINQ позволяет выражать сложные операции над коллекциями в декларативном стиле:
C# | 1
2
3
4
5
6
| var adults = persons
.Where(p => p.Age >= 18)
.OrderBy(p => p.LastName)
.ThenBy(p => p.FirstName)
.Select(p => new { p.FullName, p.Age })
.ToList(); |
|
Вместо императивного кода с циклами и условиями мы получаем компактное выражение намерения. Но можно пойти дальше и создать собственный Fluent API для работы с коллекциями, ориентированный на специфические задачи домена:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| public class EmployeeCollection
{
private readonly List<Employee> _employees;
public EmployeeCollection(IEnumerable<Employee> employees)
{
_employees = new List<Employee>(employees);
}
public EmployeeCollection ActiveOnly()
{
return new EmployeeCollection(_employees.Where(e => e.IsActive));
}
public EmployeeCollection InDepartment(string department)
{
return new EmployeeCollection(_employees.Where(e => e.Department == department));
}
public EmployeeCollection WithExperience(int minYears)
{
return new EmployeeCollection(_employees.Where(e => e.YearsOfExperience >= minYears));
}
public EmployeeCollection SortByExperience()
{
return new EmployeeCollection(_employees.OrderByDescending(e => e.YearsOfExperience));
}
public List<Employee> ToList() => new List<Employee>(_employees);
} |
|
Такой домен-специфичный API делает код более читаемым и понятным для людей, не погруженных в детали LINQ:
C# | 1
2
3
4
5
6
| var seniorDevelopers = new EmployeeCollection(allEmployees)
.ActiveOnly()
.InDepartment("Development")
.WithExperience(5)
.SortByExperience()
.ToList(); |
|
Реализация валидации данных в цепочках методов
Валидация данных — еще одна область, где Fluent API может значительно улучшить читаемость кода. Вместо многочисленных проверок if-else можно создать цепочку валидаторов:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| public class Validator<T>
{
private readonly T _obj;
private readonly List<string> _errors = new List<string>();
public Validator(T obj)
{
_obj = obj;
}
public Validator<T> Verify(Func<T, bool> predicate, string errorMessage)
{
if (!predicate(_obj))
_errors.Add(errorMessage);
return this;
}
public Validator<T> VerifyNotNull<TProperty>(Func<T, TProperty> selector, string propertyName)
{
if (selector(_obj) == null)
_errors.Add($"{propertyName} не может быть пустым");
return this;
}
public ValidationResult GetResult()
{
return new ValidationResult(_errors);
}
}
public class ValidationResult
{
public bool IsValid => !Errors.Any();
public IReadOnlyList<string> Errors { get; }
public ValidationResult(IEnumerable<string> errors)
{
Errors = new List<string>(errors);
}
} |
|
Использование:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
| var result = new Validator<User>(user)
.VerifyNotNull(u => u.Name, "Имя")
.VerifyNotNull(u => u.Email, "Email")
.Verify(u => u.Email.Contains("@"), "Email должен содержать символ @")
.Verify(u => u.Age >= 18, "Возраст должен быть не менее 18 лет")
.GetResult();
if (!result.IsValid)
{
foreach (var error in result.Errors)
Console.WriteLine(error);
} |
|
Такой подход делает валидацию более структурированной и самодокументируемой. Особенно он хорош для сложных бизнес-правил.
Создание предметно-ориентированных языков (DSL)
Fluent API может служить основой для создания внутриязыковых DSL — специализированных языков для конкретной предметной области, встроенных в C#. Такие DSL позволяют экспертам предметной области писать код, максимально близкий к их естественному языку. Пример DSL для описания расписания:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| public class ScheduleBuilder
{
private readonly Schedule _schedule = new Schedule();
public DayScheduleBuilder OnMonday => ConfigureDay(DayOfWeek.Monday);
public DayScheduleBuilder OnTuesday => ConfigureDay(DayOfWeek.Tuesday);
public DayScheduleBuilder OnWednesday => ConfigureDay(DayOfWeek.Wednesday);
public DayScheduleBuilder OnThursday => ConfigureDay(DayOfWeek.Thursday);
public DayScheduleBuilder OnFriday => ConfigureDay(DayOfWeek.Friday);
private DayScheduleBuilder ConfigureDay(DayOfWeek day)
{
return new DayScheduleBuilder(this, _schedule, day);
}
public Schedule Build() => _schedule;
}
public class DayScheduleBuilder
{
private readonly ScheduleBuilder _parent;
private readonly Schedule _schedule;
private readonly DayOfWeek _day;
public DayScheduleBuilder(ScheduleBuilder parent, Schedule schedule, DayOfWeek day)
{
_parent = parent;
_schedule = schedule;
_day = day;
}
public TimeSlotBuilder At(TimeSpan time) => new TimeSlotBuilder(this, _schedule, _day, time);
}
public class TimeSlotBuilder
{
private readonly DayScheduleBuilder _parent;
private readonly Schedule _schedule;
private readonly DayOfWeek _day;
private readonly TimeSpan _startTime;
public TimeSlotBuilder(DayScheduleBuilder parent, Schedule schedule, DayOfWeek day, TimeSpan startTime)
{
_parent = parent;
_schedule = schedule;
_day = day;
_startTime = startTime;
}
public DayScheduleBuilder Schedule(string activity, TimeSpan duration)
{
_schedule.AddActivity(_day, _startTime, activity, duration);
return _parent;
}
} |
|
Использование этого DSL для расписания выглядит почти как естественный язык:
C# | 1
2
3
4
5
6
7
8
9
10
| var schedule = new ScheduleBuilder()
.OnMonday
.At(new TimeSpan(9, 0, 0))
.Schedule("Встреча команды", TimeSpan.FromHours(1))
.At(new TimeSpan(13, 0, 0))
.Schedule("Обед", TimeSpan.FromMinutes(45))
.OnWednesday
.At(new TimeSpan(14, 30, 0))
.Schedule("Код-ревью", TimeSpan.FromHours(1.5))
.Build(); |
|
Такие DSL создают "язык в языке", позволяя выражать сложные понятия предметной области в компактной, понятной форме.
Пример модификации объектов
Одно из ценных применений Fluent API — модификация существующих объектов. Рассмотрим пример с объектом изображения, где нам нужно применить серию трансформаций:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| public class ImageProcessor
{
private readonly Image _image;
public ImageProcessor(Image image)
{
_image = image;
}
public ImageProcessor Resize(int width, int height)
{
// Логика изменения размера
Console.WriteLine($"Изображение изменено до {width}x{height}");
return this;
}
public ImageProcessor Rotate(int degrees)
{
// Логика поворота
Console.WriteLine($"Изображение повернуто на {degrees} градусов");
return this;
}
public ImageProcessor ApplyFilter(string filterName)
{
// Логика применения фильтра
Console.WriteLine($"Применен фильтр {filterName}");
return this;
}
public ImageProcessor Crop(int x, int y, int width, int height)
{
// Логика обрезки
Console.WriteLine($"Изображение обрезано: ({x},{y}) {width}x{height}");
return this;
}
public Image GetResult()
{
return _image;
}
} |
|
Применение цепочки модификаций:
C# | 1
2
3
4
5
6
| var processedImage = new ImageProcessor(originalImage)
.Resize(800, 600)
.Rotate(90)
.Crop(50, 50, 700, 500)
.ApplyFilter("Sepia")
.GetResult(); |
|
Этот подход особенно полезен для обработки данных, когда требуется последовательное применение набора трансформаций. Он делает код более лаконичным и самодокументируемым по сравнению с серией отдельных вызовов.
Интеграция Fluent API с асинхронным программированием
Современный C# активно использует асинхронное программирование с ключевыми словами async и await . Интеграция Fluent API с асинхронностью представляет определённые вызовы, но открывает интересные возможности. Рассмотрим асинхронный Fluent API для работы с API веб-сервиса:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| public class ApiRequestBuilder
{
private readonly HttpClient _client;
private readonly string _baseUrl;
private readonly Dictionary<string, string> _headers = new Dictionary<string, string>();
private readonly Dictionary<string, string> _queryParams = new Dictionary<string, string>();
public ApiRequestBuilder(HttpClient client, string baseUrl)
{
_client = client;
_baseUrl = baseUrl;
}
public ApiRequestBuilder WithHeader(string name, string value)
{
_headers[name] = value;
return this;
}
public ApiRequestBuilder WithParameter(string name, string value)
{
_queryParams[name] = value;
return this;
}
public async Task<HttpResponseMessage> GetAsync(string endpoint)
{
var request = CreateRequest(HttpMethod.Get, endpoint);
return await _client.SendAsync(request);
}
public async Task<HttpResponseMessage> PostAsync(string endpoint, HttpContent content)
{
var request = CreateRequest(HttpMethod.Post, endpoint);
request.Content = content;
return await _client.SendAsync(request);
}
private HttpRequestMessage CreateRequest(HttpMethod method, string endpoint)
{
var uriBuilder = new UriBuilder(_baseUrl)
{
Path = endpoint
};
// Добавляем query-параметры
if (_queryParams.Any())
{
var query = string.Join("&", _queryParams.Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));
uriBuilder.Query = query;
}
var request = new HttpRequestMessage(method, uriBuilder.Uri);
// Добавляем заголовки
foreach (var header in _headers)
{
request.Headers.Add(header.Key, header.Value);
}
return request;
}
} |
|
Использовать этот асинхронный Fluent API можно так:
C# | 1
2
3
4
5
6
7
8
9
10
11
| var api = new ApiRequestBuilder(httpClient, "https://api.example.com")
.WithHeader("Authorization", "Bearer token123")
.WithParameter("page", "1")
.WithParameter("limit", "10");
// Выполнение GET-запроса
var response = await api.GetAsync("/users");
// Выполнение POST-запроса
var content = new StringContent("{\"name\":\"John\"}", Encoding.UTF8, "application/json");
var createResponse = await api.PostAsync("/users", content); |
|
Продвинутые техники
После освоения основ построения цепочек методов пора погрузиться в более сложные аспекты Fluent API. Эти продвинутые техники позволяют создавать по-настоящему гибкие, расширяемые и производительные интерфейсы.
Работа с интерфейсами в цепочках методов
При разработке сложных Fluent API использование интерфейсов открывает новые возможности для структурирования кода. Вместо возврата конкретных типов, методы могут возвращать интерфейсы, что повышает гибкость и позволяет контролировать доступные операции на каждом этапе цепочки.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public interface IQueryBuilder
{
IWhereBuilder Select(params string[] columns);
}
public interface IWhereBuilder
{
IOrderBuilder Where(string condition);
IOrderBuilder WhereAll();
}
public interface IOrderBuilder
{
IExecutableBuilder OrderBy(string column);
IExecutableBuilder OrderByDescending(string column);
IExecutableBuilder Skip(int count);
IExecutableBuilder Take(int count);
}
public interface IExecutableBuilder
{
string Build();
Task<IEnumerable<T>> ExecuteAsync<T>();
} |
|
Реализация может выглядеть так:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| public class SqlBuilder : IQueryBuilder, IWhereBuilder, IOrderBuilder, IExecutableBuilder
{
private string _table;
private List<string> _columns = new List<string>();
private List<string> _conditions = new List<string>();
private string _orderBy;
private int? _skip;
private int? _take;
public IWhereBuilder Select(params string[] columns)
{
_columns.AddRange(columns);
return this;
}
public IOrderBuilder Where(string condition)
{
_conditions.Add(condition);
return this;
}
public IOrderBuilder WhereAll()
{
return this;
}
public IExecutableBuilder OrderBy(string column)
{
_orderBy = $"{column} ASC";
return this;
}
public IExecutableBuilder OrderByDescending(string column)
{
_orderBy = $"{column} DESC";
return this;
}
public IExecutableBuilder Skip(int count)
{
_skip = count;
return this;
}
public IExecutableBuilder Take(int count)
{
_take = count;
return this;
}
public string Build()
{
// Формирование SQL-запроса
return "SQL query";
}
public async Task<IEnumerable<T>> ExecuteAsync<T>()
{
// Выполнение запроса
return await Task.FromResult(Enumerable.Empty<T>());
}
} |
|
Использование интерфейсов в этом примере обеспечивает несколько преимуществ:
1. Ясность API: На каждом этапе разработчик видит только те методы, которые имеют смысл в текущем контексте.
2. Статическая типизация: Компилятор предотвращает вызов методов в неправильном порядке.
3. Расширяемость: Можно легко добавить дополнительные реализации интерфейсов без изменения существующего кода.
Код использования такого API строго типизирован и очень наглядно демонстрирует поток операций:
C# | 1
2
3
4
5
6
7
| var builder = new SqlBuilder();
var result = await builder
.Select("Id", "Name")
.Where("Age > 18")
.OrderByDescending("LastVisit")
.Take(10)
.ExecuteAsync<User>(); |
|
Обработка ошибок в цепных вызовах
Традиционная обработка исключений может разрушить плавность Fluent API. Рассмотрим элегантное решение — паттерн "монада результата", который позволяет продолжать цепочку даже при возникновении ошибок:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| public class Result<T>
{
public bool IsSuccess { get; }
public T Value { get; }
public string Error { get; }
private Result(bool isSuccess, T value, string error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}
public static Result<T> Success(T value) => new Result<T>(true, value, null);
public static Result<T> Failure(string error) => new Result<T>(false, default, error);
public Result<TNext> Then<TNext>(Func<T, Result<TNext>> next)
{
if (!IsSuccess)
return Result<TNext>.Failure(Error);
try
{
return next(Value);
}
catch (Exception ex)
{
return Result<TNext>.Failure(ex.Message);
}
}
public Result<T> Catch(Action<string> errorHandler)
{
if (!IsSuccess)
errorHandler(Error);
return this;
}
} |
|
Такой подход позволяет создавать надежные цепочки операций, где ошибки не прерывают выполнение, а аккуратно обрабатываются в конце:
C# | 1
2
3
4
5
6
7
8
9
10
| var result = UserRepository.FindById(userId)
.Then(user => UserValidator.Validate(user))
.Then(user => user.UpdateProfile(profileData))
.Then(user => UserRepository.Save(user))
.Catch(error => Logger.LogError(error));
if (result.IsSuccess)
return Ok(result.Value);
else
return BadRequest(result.Error); |
|
Многоуровневые цепочки с разграничением ответственности
Для сложных API необходимо разграничивать ответственность между компонентами. Многоуровневые цепочки позволяют структурировать Fluent API по функциональным зонам, где каждый уровень отвечает за свой аспект конфигурации.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| public class EmailBuilder
{
private readonly Email _email = new Email();
public RecipientBuilder To(string address)
{
_email.To = address;
return new RecipientBuilder(_email);
}
}
public class RecipientBuilder
{
private readonly Email _email;
public RecipientBuilder(Email email)
{
_email = email;
}
public ContentBuilder WithSubject(string subject)
{
_email.Subject = subject;
return new ContentBuilder(_email);
}
}
public class ContentBuilder
{
private readonly Email _email;
public ContentBuilder(Email email)
{
_email = email;
}
public AttachmentBuilder WithBody(string body)
{
_email.Body = body;
return new AttachmentBuilder(_email);
}
}
public class AttachmentBuilder
{
private readonly Email _email;
public AttachmentBuilder(Email email)
{
_email = email;
}
public AttachmentBuilder Attach(string filename)
{
_email.Attachments.Add(filename);
return this;
}
public Email Build()
{
return _email;
}
} |
|
Такая структура обеспечивает чёткое разграничение обязанностей и направляет пользователя через логический поток конфигурации:
C# | 1
2
3
4
5
6
| var email = new EmailBuilder()
.To("recipient@example.com")
.WithSubject("Важное сообщение")
.WithBody("Текст сообщения")
.Attach("document.pdf")
.Build(); |
|
Генерация цепочек методов с помощью T4-шаблонов
Ручное создание объёмных Fluent API может быть утомительно и подвержено ошибкам. T4-шаблоны (Text Template Transformation Toolkit) позволяют автоматизировать этот процесс, генерируя код на основе метаданных.
Пример T4-шаблона для генерации методов билдера:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| <#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#
var properties = new[] {
new { Name = "FirstName", Type = "string" },
new { Name = "LastName", Type = "string" },
new { Name = "Age", Type = "int" },
new { Name = "Email", Type = "string" }
};
#>
using System;
namespace Builders {
public class PersonBuilder {
private Person _person = new Person();
<# foreach(var prop in properties) { #>
public PersonBuilder With<#= prop.Name #>(<#= prop.Type #> value) {
_person.<#= prop.Name #> = value;
return this;
}
<# } #>
public Person Build() {
return _person;
}
}
} |
|
Этот шаблон автоматически генерирует методы билдера для всех свойств объекта Person . При добавлении новых свойств в класс Person вы просто обновляете список свойств в шаблоне, и новые методы будут добавлены автоматически.
Генерация документации для Fluent API
XML-комментарии в C# — мощный инструмент для документирования Fluent API. Правильно оформленные комментарии сделают ваш API более понятным для пользователей через IntelliSense и автоматически сгенерированную документацию.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| /// <summary>
/// Строитель SQL-запросов с Fluent-интерфейсом.
/// </summary>
public class QueryBuilder
{
/// <summary>
/// Указывает таблицу для запроса.
/// </summary>
/// <param name="tableName">Имя таблицы.</param>
/// <returns>Текущий билдер для продолжения цепочки вызовов.</returns>
/// <example>
/// <code>
/// var query = new QueryBuilder()
/// .FromTable("Users")
/// .Select("Id", "Name");
/// </code>
/// </example>
public QueryBuilder FromTable(string tableName)
{
// Реализация
return this;
}
} |
|
При использовании таких комментариев IntelliSense предоставит пользователям подробные подсказки, включая примеры использования, что значительно упрощает работу с API.
Рекомендации и подводные камни
Несмотря на все преимущества Fluent API, его применение требует взвешенного подхода. Как и любой инструмент в программировании, цепочки методов имеют свои области применения и ограничения. Знание этих нюансов поможет принимать правильные архитектурные решения и избежать распространённых ошибок.
Когда применять, а когда избегать
Fluent API особенно хорош в следующих сценариях:
1. Построение сложных объектов с множеством опциональных параметров.
2. Конфигурирование компонентов приложений .
3. Создание DSL для выразительного описания бизнес-процессов.
4. Работа с коллекциями данных и построение запросов.
5. Цепочки трансформаций, где объект последовательно изменяется.
Однако существуют случаи, когда от цепочек методов лучше отказаться:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Слишком простой случай — перегрузка не нужна
public void SaveUser(User user)
{
// Простая операция сохранения
}
// Лучше, чем избыточный Fluent API
public void SaveUser(User user, bool sendNotification = false, bool logActivity = true)
{
// Реализация с опциональными параметрами
}
// Избыточно сложно для такой задачи
var repository = new UserRepositoryBuilder()
.WithNotifications()
.WithLogging()
.Build()
.SaveUser(user); |
|
Fluent API не оправдан для простых операций с небольшим количеством параметров. В таких случаях традиционные методы с опциональными параметрами часто оказываются более прямолинейным и понятным решением.
Баланс между читаемостью и сложностью
Основная цель Fluent API — повышение читаемости кода. Парадоксально, но чрезмерно сложный Fluent API может привести к противоположному результату. Необходимо найти баланс между выразительностью и простотой:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Избыточно сложный API
var query = new FluentQueryBuilder()
.From<User>()
.Where(q =>
q.Property(u => u.Active).EqualTo(true)
.And()
.Property(u => u.LastLogin).GreaterThan(DateTime.Now.AddDays(-30))
)
.OrderByDescending(u => u.LastLogin)
.Select(u => new { u.Id, u.Name })
.GetResult();
// Более сбалансированный подход
var query = new QueryBuilder<User>()
.Where(u => u.Active && u.LastLogin > DateTime.Now.AddDays(-30))
.OrderByDescending(u => u.LastLogin)
.Select(u => new { u.Id, u.Name })
.GetResult(); |
|
При проектировании Fluent API важно помнить, что конечная цель — сделать код более понятным, а не впечатлить коллег сложной архитектурой. Хороший Fluent API должен быть интуитивно понятным даже для программистов, впервые столкнувшихся с ним.
Особенности тестирования кода с Fluent API
При использовании Fluent API особое внимание следует уделить тестированию. Цепочки методов создают уникальные вызовы для модульного тестирования, так как каждый метод в цепочке должен корректно взаимодействовать со следующим.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // Тестирование отдельных компонентов билдера
[Fact]
public void WithName_ShouldSetName()
{
// Arrange
var builder = new PersonBuilder();
// Act
builder.WithName("Алексей");
var person = builder.Build();
// Assert
Assert.Equal("Алексей", person.Name);
}
// Тестирование всей цепочки
[Fact]
public void FluentChain_ShouldBuildCorrectPerson()
{
// Arrange & Act
var person = new PersonBuilder()
.WithName("Алексей")
.WithAge(25)
.WithEmail("aleksey@example.com")
.Build();
// Assert
Assert.Equal("Алексей", person.Name);
Assert.Equal(25, person.Age);
Assert.Equal("aleksey@example.com", person.Email);
} |
|
При тестировании Fluent API рекомендуется:
1. Тестировать каждый метод цепочки отдельно..
2. Проверять корректность всей цепочки целиком.
3. Тестировать различные комбинации методов.
4. Проверять обработку ошибок и валидацию.
Сложности могут возникать при тестировании интерфейсов с многоуровневым разграничением. В таких случаях полезны тесты интеграции, проверяющие весь жизненный цикл объекта.
Метрики качества кода при использовании Fluent API
Как оценить качество реализации Fluent API? Существуют специфические метрики, помогающие измерить эффективность вашего решения:
1. Когнитивная сложность — насколько легко понять цепочку методов. Хороший Fluent API должен снижать когнитивную нагрузку.
2. Глубина вызовов — количество вложенных уровней в цепочке. Оптимальная глубина редко превышает 3-4 уровня.
3. Коэффициент повторного использования — как часто один и тот же код Fluent API используется в разных частях приложения.
4. Гибкость API — насколько легко добавлять новые функции без изменения существующего кода.
Наблюдение за этими метриками помогает постепенно улучшать качество Fluent API.
Рефакторинг существующего кода к Fluent интерфейсам
Переход от традиционного API к Fluent-синтаксису требует систематического подхода. Рассмотрим стратегию рефакторинга на примере:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // До рефакторинга
public class Report
{
public void SetTitle(string title) { /* ... */ }
public void AddSection(string section) { /* ... */ }
public void SetFormat(Format format) { /* ... */ }
public void Generate() { /* ... */ }
}
// Использование
var report = new Report();
report.SetTitle("Квартальный отчет");
report.AddSection("Доходы");
report.AddSection("Расходы");
report.SetFormat(Format.PDF);
report.Generate(); |
|
Превращаем в Fluent API:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // После рефакторинга
public class Report
{
public Report WithTitle(string title) { /* ... */ return this; }
public Report AddSection(string section) { /* ... */ return this; }
public Report WithFormat(Format format) { /* ... */ return this; }
public void Generate() { /* ... */ }
}
// Использование
new Report()
.WithTitle("Квартальный отчет")
.AddSection("Доходы")
.AddSection("Расходы")
.WithFormat(Format.PDF)
.Generate(); |
|
Альтернативные подходы
Хотя Fluent API предлагает элегантный способ выстраивания цепочек операций, он не всегда оптимальный вариант. Существуют альтернативные подходы к дизайну API, которые могут лучше подойти в определенных ситуациях.
Builder vs Object Initializers
Вместо создания сложного Fluent API часто можно использовать обычные инициализаторы объектов, доступные в C# начиная с версии 3.0:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Fluent API (билдер)
var person = new PersonBuilder()
.WithName("Михаил")
.WithAge(34)
.WithEmail("mikhail@example.com")
.Build();
// Инициализатор объекта
var person = new Person
{
Name = "Михаил",
Age = 34,
Email = "mikhail@example.com"
}; |
|
Инициализаторы объектов работают прекрасно для простых случаев без сложной логики и валидации. Они более компактны и не требуют создания дополнительных классов-билдеров.
Extension Methods vs Fluent Classes
Вместо создания специальных классов с Fluent-интерфейсом, можно использовать расширения существующих типов:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
| // Fluent-класс
var query = new QueryBuilder("Users")
.Where("Age > 18")
.OrderBy("LastVisit")
.Build();
// Extension-методы
var query = "Users"
.AsQuery()
.Where("Age > 18")
.OrderBy("LastVisit")
.Build(); |
|
Extension-методы поддерживают цепочки, но при этом работают с существующими типами без необходимости их оборачивания. Этот подход особенно полезен при работе с типами, которые вы не можете изменить.
Функциональный подход
Вместо объектно-ориентированного Fluent API можно использовать функциональный подход с комбинаторами и функциями высшего порядка:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Fluent OOP-стиль
var result = new Validator<User>(user)
.Verify(u => u.Age >= 18, "Возраст должен быть не менее 18")
.Verify(u => !string.IsNullOrEmpty(u.Email), "Email обязателен")
.GetResult();
// Функциональный стиль
var validateAge = (User u) => u.Age >= 18
? Ok(u)
: Error("Возраст должен быть не менее 18");
var validateEmail = (User u) => !string.IsNullOrEmpty(u.Email)
? Ok(u)
: Error("Email обязателен");
var result = validateAge(user)
.Bind(validateEmail); |
|
Функциональный подход может быть более гибким и компактным для определенных задач, особенно связанных с трансформацией данных и валидацией.
Command Query Separation
Вместо смешивания команд (методов, изменяющих состояние) и запросов (методов, возвращающих данные) в одной цепочке, можно строго разделить их согласно принципу Command-Query Separation:
C# | 1
2
3
4
5
6
7
8
9
10
11
| // Смешивание запросов и команд в Fluent API
var user = repository.GetUser(id)
.Update(updatedInfo)
.CalculateRating()
.Save();
// Разделение запросов и команд
var user = repository.GetUser(id);
user.Update(updatedInfo);
var rating = ratingCalculator.Calculate(user);
repository.Save(user); |
|
Такое разделение делает код более предсказуемым и часто легче тестируемым, хотя жертвует компактностью Fluent API.
Record Types
В C# 9.0 и выше есть поддержка рекордов (records) — неизменяемых ссылочных типов с автоматическим созданием конструкторов и свойств:
C# | 1
2
3
4
5
6
7
8
9
10
11
| // Традиционный Fluent API для неизменяемых объектов
var person = new PersonBuilder()
.WithName("Анна")
.WithAge(29)
.Build();
// Использование рекордов с шаблоном with
record Person(string Name, int Age);
var anna = new Person("Анна", 29);
var olderAnna = anna with { Age = 30 }; |
|
Рекорды предоставляют многие преимущества неизменяемых объектов без необходимости создавать сложный Fluent API, что делает код более компактным и понятным.
Заключительные мысли
Fluent API — это мощный инструмент для создания выразительных, читаемых интерфейсов, но он не является универсальным решением всех проблем. Как и любой архитектурный паттерн, он требует взвешенного применения с учетом конкретного контекста и требований. При проектировании API стоит помнить о главной цели — создании инструментов, которые делают код более понятным, поддерживаемым и надежным. Fluent-синтаксис часто способствует достижению этой цели, позволяя выражать намерения программиста более явно и наглядно. В то же время, чрезмерно усложненный Fluent API может принести больше вреда, чем пользы. Как говорится, "лучшее — враг хорошего". Простота и предсказуемость часто ценнее, чем стремление к элегантности любой ценой.
Использование свойств Fluent API с базой PostgreSQL Доброй вечер форумчане!
Помогите переписать Context
Программа работает с обыкновенным access. В... Microsoft Fluent Interface здраствуйте. где можно взять Microsoft Fluent Interface или Ribbon? Как записать Left Join (Right Join) при помощи Linq, используя fluent-синтаксис Как записать Left Join (Right Join) при помощи Linq используя fluent - синтаксис?
У меня есть... Не получается записать сущности в базу данных (Fluent NHibernate) Здравствуйте, я пытаюсь освоить Fluent NHibernate. На практике решил опробовать то что прочёл.... Маппинг сущности на несколько таблиц (Fluent NHibernate) Здравствуйте, у меня вопрос связанный с описанием маппинга сущности на несколько таблиц. Имеются... Создание новой сессии Fluent nHibernate Здравствуйте, не могу разобраться с одной проблемой. Убиваю сессию в которой выполняется запрос,... Вставка новой записи в таблицу с составным ключом Fluent NHibernate День добрый всем.
Имею сущность Person
Имею сущность Program
У каждого Person может быть... Собственный аттрибут в Fluent protected override ValidationResult IsValid(object value, ValidationContext validationContext)
... Query vs fluent notations идентичны ли в плане функционала?как мне казалось, да, а выбор нотации - кому, как говорится... Маппинг в Fluent NHibernate Доброго времени суток
есть таблица с пользователями
public class User
{
public... Fluent design не убирается стандартная обводка Воспользовался эти проектом: https://github.com/sourcechord/FluentWPF
Растянул окно на весь... Переезд с EF6 на linq2db (Fluent Mapping) Добрый день.
В других ветках уже писал, подведу итоги и здесь, чтобы всем было понятно что хочу.
...
|