Флент-Білдер (Fluent Builder) — потужний шаблон проектування, який дозволяє створювати складні об'єкти через більш читабельний та зручний інтерфейс. У цій статті ми детально розглянемо впровадження шаблону Fluent Builder на C#, досліджуючи як базові, так і більш складні сценарії, а також аналізуючи реальні приклади з бібліотек .NET Standard.
Чому варто використовувати шаблон Fluent Builder?
Перед тим як заглиблюватися в реалізації, давайте з'ясуємо, чому ви можете захотіти використовувати шаблон Fluent Builder:
- Покращує читабельність коду завдяки ланцюжку викликів методів
- Окремо розділяє побудову складних об'єктів від їхньої репрезентації
- Забезпечує незмінність об'єктів, одночасно зберігаючи гнучкість у їх побудові
- Елегантно обробляє необов'язкові параметри
- Забезпечує чіткий API для побудови об'єктів
Реальний приклад: конфігурація HttpClient
Один з найпоширеніших прикладів використання шаблону Fluent Builder в .NET — це HttpClientBuilder
. Подивимося, як Microsoft реалізує це:
var client = new HttpClient(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 10
});
Хоч це і працює, але може бути більш елегантним. Ось як ми могли б реалізувати fluent builder для HttpClient:
public class HttpClientBuilder
{
private readonly SocketsHttpHandler _handler;
private HttpClientBuilder()
{
_handler = new SocketsHttpHandler();
}
public static HttpClientBuilder Create()
{
return new HttpClientBuilder();
}
public HttpClientBuilder WithConnectionLifetime(TimeSpan lifetime)
{
_handler.PooledConnectionLifetime = lifetime;
return this;
}
public HttpClientBuilder WithIdleTimeout(TimeSpan timeout)
{
_handler.PooledConnectionIdleTimeout = timeout;
return this;
}
public HttpClientBuilder WithMaxConnections(int maxConnections)
{
_handler.MaxConnectionsPerServer = maxConnections;
return this;
}
public HttpClient Build()
{
return new HttpClient(_handler);
}
}
Тепер ми можемо створювати HttpClient ось так:
var client = HttpClientBuilder.Create()
.WithConnectionLifetime(TimeSpan.FromMinutes(10))
.WithIdleTimeout(TimeSpan.FromMinutes(5))
.WithMaxConnections(10)
.Build();
Складніший сценарій: Вкладені Builders з контекстом конфігурації
Розглянемо більш складний сценарій, який включає вкладені builders.
Уявімо, що ми створюємо систему конфігурації для веб-додатку:
public class WebAppConfiguration
{
public DatabaseSettings Database { get; }
public CacheSettings Cache { get; }
public AuthenticationSettings Authentication { get; }
private WebAppConfiguration(WebAppConfigurationBuilder builder)
{
Database = builder.DatabaseBuilder.Build();
Cache = builder.CacheBuilder.Build();
Authentication = builder.AuthenticationBuilder.Build();
}
public class WebAppConfigurationBuilder
{
internal readonly DatabaseSettingsBuilder DatabaseBuilder;
internal readonly CacheSettingsBuilder CacheBuilder;
internal readonly AuthenticationSettingsBuilder AuthenticationBuilder;
public WebAppConfigurationBuilder()
{
DatabaseBuilder = new DatabaseSettingsBuilder(this);
CacheBuilder = new CacheSettingsBuilder(this);
AuthenticationBuilder = new AuthenticationSettingsBuilder(this);
}
public DatabaseSettingsBuilder ConfigureDatabase()
{
return DatabaseBuilder;
}
public CacheSettingsBuilder ConfigureCache()
{
return CacheBuilder;
}
public AuthenticationSettingsBuilder ConfigureAuthentication()
{
return AuthenticationBuilder;
}
public WebAppConfiguration Build()
{
return new WebAppConfiguration(this);
}
}
}
public class DatabaseSettings
{
public string ConnectionString { get; }
public int MaxConnections { get; }
public TimeSpan CommandTimeout { get; }
internal DatabaseSettings(string connectionString, int maxConnections, TimeSpan commandTimeout)
{
ConnectionString = connectionString;
MaxConnections = maxConnections;
CommandTimeout = commandTimeout;
}
}
public class DatabaseSettingsBuilder
{
private readonly WebAppConfiguration.WebAppConfigurationBuilder _parentBuilder;
private string _connectionString;
private int _maxConnections;
private TimeSpan _commandTimeout;
internal DatabaseSettingsBuilder(WebAppConfiguration.WebAppConfigurationBuilder parentBuilder)
{
_parentBuilder = parentBuilder;
}
public DatabaseSettingsBuilder WithConnectionString(string connectionString)
{
_connectionString = connectionString;
return this;
}
public DatabaseSettingsBuilder WithMaxConnections(int maxConnections)
{
_maxConnections = maxConnections;
return this;
}
public DatabaseSettingsBuilder WithCommandTimeout(TimeSpan timeout)
{
_commandTimeout = timeout;
return this;
}
public WebAppConfiguration.WebAppConfigurationBuilder Done()
{
return _parentBuilder;
}
internal DatabaseSettings Build()
{
return new DatabaseSettings(_connectionString, _maxConnections, _commandTimeout);
}
}
public class CacheSettings
{
public string RedisConnection { get; }
public TimeSpan DefaultExpiration { get; }
internal CacheSettings(string redisConnection, TimeSpan defaultExpiration)
{
RedisConnection = redisConnection;
DefaultExpiration = defaultExpiration;
}
}
public class CacheSettingsBuilder
{
private readonly WebAppConfiguration.WebAppConfigurationBuilder _parentBuilder;
private string _redisConnection;
private TimeSpan _defaultExpiration;
internal CacheSettingsBuilder(WebAppConfiguration.WebAppConfigurationBuilder parentBuilder)
{
_parentBuilder = parentBuilder;
}
public CacheSettingsBuilder WithRedisConnection(string redisConnection)
{
_redisConnection = redisConnection;
return this;
}
public CacheSettingsBuilder WithDefaultExpiration(TimeSpan defaultExpiration)
{
_defaultExpiration = defaultExpiration;
return this;
}
public WebAppConfiguration.WebAppConfigurationBuilder Done()
{
return _parentBuilder;
}
internal CacheSettings Build()
{
return new CacheSettings(_redisConnection, _defaultExpiration);
}
}
public class AuthenticationSettings
{
public string JwtSecret { get; }
public TimeSpan TokenExpiration { get; }
internal AuthenticationSettings(string jwtSecret, TimeSpan tokenExpiration)
{
JwtSecret = jwtSecret;
TokenExpiration = tokenExpiration;
}
}
public class AuthenticationSettingsBuilder
{
private readonly WebAppConfiguration.WebAppConfigurationBuilder _parentBuilder;
private string _jwtSecret;
private TimeSpan _tokenExpiration;
internal AuthenticationSettingsBuilder(WebAppConfiguration.WebAppConfigurationBuilder parentBuilder)
{
_parentBuilder = parentBuilder;
}
public AuthenticationSettingsBuilder WithJwtSecret(string jwtSecret)
{
_jwtSecret = jwtSecret;
return this;
}
public AuthenticationSettingsBuilder WithTokenExpiration(TimeSpan tokenExpiration)
{
_tokenExpiration = tokenExpiration;
return this;
}
public WebAppConfiguration.WebAppConfigurationBuilder Done()
{
return _parentBuilder;
}
internal AuthenticationSettings Build()
{
return new AuthenticationSettings(_jwtSecret, _tokenExpiration);
}
}
Це дозволяє створити дуже виразну конфігурацію:
var config = new WebAppConfiguration.WebAppConfigurationBuilder()
.ConfigureDatabase()
.WithConnectionString("Server=myserver;Database=mydb")
.WithMaxConnections(100)
.WithCommandTimeout(TimeSpan.FromSeconds(30))
.Done()
.ConfigureCache()
.WithRedisConnection("localhost:6379")
.WithDefaultExpiration(TimeSpan.FromMinutes(10))
.Done()
.ConfigureAuthentication()
.WithJwtSecret("your-secret-key")
.WithTokenExpiration(TimeSpan.FromHours(1))
.Done()
.Build();
Реальний приклад: StringBuilder
Клас StringBuilder
у .NET є ще одним відмінним прикладом патерну Fluent Builder:
var message = new StringBuilder()
.Append("Hello")
.Append(" ")
.Append("World")
.AppendLine("!")
.ToString();
Розширений патерн: Генеричні будівельники з обмеженнями
Іноді необхідно створювати будівельники, які працюють з генеричними типами.
Ось приклад будівельника для генеричних колекцій:
public class CollectionBuilder where T : class
{
private readonly List _items = new List();
private readonly List>> _configurations = new List>>();
public CollectionBuilder Add(T item)
{
_items.Add(item);
return this;
}
public CollectionBuilder AddRange(IEnumerable items)
{
_items.AddRange(items);
return this;
}
public CollectionBuilder Configure(Action> configuration)
{
_configurations.Add(configuration);
return this;
}
public CollectionBuilder WithFilter(Func predicate)
{
_configurations.Add(items =>
{
var itemsToRemove = items.Where(x => !predicate(x)).ToList();
foreach (var item in itemsToRemove)
{
items.Remove(item);
}
});
return this;
}
public IReadOnlyList Build()
{
foreach (var configuration in _configurations)
{
configuration(_items);
}
return _items.AsReadOnly();
}
}
Приклад використання:
var numbers = new CollectionBuilder()
.Add("1")
.Add("2")
.Add("3")
.AddRange(new[] { "4", "5", "6" })
.Configure(list => list.Sort())
.WithFilter(x => int.Parse(x) > 2)
.Build();
Кращі практики та рекомендації
Коли ви реалізуєте патерн Fluent Builder, звертайте увагу на наступні кращі практики:
- Робіть фінальний об’єкт незмінним
- Використовуйте описові назви методів, які починаються з "With" або подібних префіксів
- Повертаючи екземпляр будівельника з методів конфігурації
- Забезпечте чіткий метод
Build()
- Розгляньте можливість зробити конструктор будівельника приватним та надати статичний метод створення
- Використовуйте ланцюжкове викликання методів для більш природного API
- Реалізуйте валідацію в методі
Build()
- Розгляньте додавання методу
Reset()
для повторного використання будівельника
Поширені проблеми, яких слід уникати
- Ускладнення простого створення об’єктів
- Неналежне оброблення значень
null
- Зміна будівельника після побудови об’єкта
- Відсутність належної валідації
- Створення надмірної кількості вкладених будівельників
- Відсутність можливості скидання стану будівельника
Коли використовувати патерн Fluent Builder
Патерн Fluent Builder є особливо корисним, коли:
- Для створення об’єктів необхідно багато параметрів
- Процес створення об’єкта складається з кількох етапів
- Потрібно забезпечити незмінність об’єктів
- Потрібно надати більш читабельний API
- Необхідно створювати об’єкти з необов’язковими параметрами
- Працюєте з складними вкладеними об’єктами
Висновок
Патерн Fluent Builder є потужним інструментом у вашій C# коробці інструментів. Коли він реалізований належним чином, він може значно покращити читабельність і підтримуваність вашого коду. Вивчаючи реальні приклади з бібліотек .NET Standard та розуміючи розширені сценарії, ви можете приймати обґрунтовані рішення щодо того, коли і як впроваджувати цей патерн у вашому коді.
Не забувайте, що, хоча цей патерн є потужним, просте створення об’єктів не завжди вимагає його використання. Використовуйте його, коли переваги покращеної читабельності та підтримуваності переважають додаткову складність його реалізації.
Перекладено з: Mastering the Fluent Builder Pattern in C#: From Basics to Advanced Scenarios