В ходе разработки возникают классы, объекты которых создаются уж слишком тяжело и громоздко. Для этих случаев разрабатывают другие методы/объекты, за которыми лежит ответственность за их созданием
Фабричный метод - разделение логики и создания объектов на иерархию типов
Пример: у нас есть объект заказа (Order
), хранящий различные позиции (OrderItem
); мы хотим передавать этот заказ в калькулятор платежа (PaymentCalculator
), применять всякие скидки и купоны, и возвращать готовый платеж наличными (CashPayment
) с просчитанным значением:
public record OrderItem(decimal Price, int Amount)
{
public decimal Cost => Price * Amount;
}
public record Order(IEnumerable<OrderItem> Items)
{
public decimal TotalCost => Items.Sum(x => x.Cost);
}
public record CashPayment(decimal Amount);
public class PaymentCalculator
{
public CashPayment Calculate(Order order)
{
var totalCost = order.TotalCost;
// Apply discounts and coupons
...
return new CashPayment(totalCost);
}
}
Здесь мы хотим ввести возможность оплаты не только наличными, но и банковской картой, поэтому создаем объект BankPayment
(от интерфейса IPayment
) и различные калькуляторы для них
public interface IPayment
{
decimal Amount { get; }
}
public record CashPayment(decimal Amount) : IPayment;
public record BankPayment(
decimal Amount,
string ReceiverAccountId) : IPayment;
public abstract class PaymentCalculator
{
public IPayment Calculate(Order order)
{
var totalCost = order.TotalCost;
// Apply discounts and coupons
...
return CreatePayment(totalCost);
}
protected abstract IPayment CreatePayment(decimal amount);
}
public class CashPaymentCalculator : PaymentCalculator
{
protected override IPayment CreatePayment(decimal amount)
=> new CashPayment(amount);
}
public class BankPaymentCalculator : PaymentCalculator
{
private readonly string _currentReceiverAccountId;
public BankPaymentCalculator(string currentReceiverAccountId)
{
_currentReceiverAccountId = currentReceiverAccountId;
}
protected override IPayment CreatePayment(decimal amount)
{
return new BankPayment(amount, _currentReceiverAccountId);
}
}
Здесь же можно выделить в паттерне фабричного метода две сущности:
В данном примере продукт - это реализация интерфейса IPayment
, а создатель - метод CreatePayment
в абстрактном классе PaymentCalculator
.
Фабричный метод применяется для переиспользования логики создания на наборе типов. Но при этом фабричный метод считается антипаттерном из-за следующих недостатков:
Calculate
для разных типовАбстрактная фабрика (или просто фабрика) - вынесение логики создания объектов в отдельные типы, объекты которых будут ответственны только за это
При использовании фабричного метода возникает такая проблема: мы хотим использовать логику создания не только в пределах нашего PaymentCalculator
, но и где-то еще - приходится переиспользовать логику. Поэтому здравой идеей будет вынести логику из фабричного метода в отдельный класс - фабрику:
public interface IPaymentFactory
{
IPayment Create(decimal amount);
}
public class CashPaymentFactory : IPaymentFactory
{
public IPayment Create(decimal amount) => new CashPayment(amount);
}
public class BankPaymentFactory : IPaymentFactory
{
private readonly string _currentReceiverAccountId;
public BankPaymentFactory(string currentReceiverAccountId)
{
_currentReceiverAccountId = currentReceiverAccountId;
}
public IPayment Create(decimal amount)
{
return new BankPayment(amount, _currentReceiverAccountId);
}
}
public interface IPaymentCalculator
{
IPayment Calculate(Order order);
}
public class PaymentCalculator : IPaymentCalculator
{
private readonly IPaymentFactory _paymentFactory;
public PaymentCalculator(IPaymentFactory paymentFactory)
{
_paymentFactory = paymentFactory;
}
public IPayment Calculate(Order order)
{
var totalCost = order.TotalCost;
// Apply discounts and coupons
...
return _paymentFactory.Create(totalCost);
}
}
Здесь все фабрики с разными логикам создания нашего Payment
реализуются от интерфейса IPaymentFactory
. Поэтому мы можем какой-нибудь другой калькулятор FixedPaymentCalculator
, который этим пользуется:
public class FixedPaymentCalculator : IPaymentCalculator
{
private readonly decimal _fixedPrice;
private readonly IPaymentFactory _paymentFactory;
public FixedPaymentCalculator(decimal fixedPrice, IPaymentFactory paymentFactory)
{
_fixedPrice = fixedPrice;
_paymentFactory = paymentFactory;
}
public IPayment Calculate(Order order)
{
var totalCost = order.Items.Sum(item =>_fixedPrice * item.Amount);
// Apply discounts and coupons
...
return _paymentFactory.Create(totalCost);
}
}
При этом заметить следующие преимущества у абстрактной фабрики:
Билдер (строитель) - объект, при помощи которого мы можем создать составной объект. В билдере мы можем разбить логику сбора аргументов на методы, уменьшая мутабельность, задавать некоторые значения по умолчанию
Разделяют 2 вида билдеров:
С помощью Convenience Builder мы упрощаем создание объектов с гигантским конструктором, предполагая, что некоторые аргумент можем сделать по умолчанию. Пример:
class Service
{
public Service(IDependency1? one, IDependency2 two, IDependency3 three) { ... }
...
}
internal interface IDependency3 { ... }
internal interface IDependency2 { ... }
internal interface IDependency1 { ... }
class ServiceBuilder
{
private IDependency1? _one;
private IDependency2? _two;
private IDependency3? _three;
public ServiceBuilder()
{
_one = null;
_two = new Dependency2();
_three = new Dependency3();
}
public ServiceBuilder WithOne(IDependency1 one) { ... }
public ServiceBuilder WithTwo(IDependency2 two) { ... }
public ServiceBuilder WithThree(IDependency3 three) { ... }
public Service Build()
{
return new Service(
_one,
_two ?? throw new InvalidOperationException(),
_three ?? throw new InvalidOperationException());
}
}
С помощью Stateful Constructor Builder мы можем принимать аргументы через методы билдера. В итоге вместо такого вызова конструктора:
var model = new Model(arg1, arg2, arg3, arg4, arg5);
мы получаем:
var builder = new ModelBuilder()
.AddArg1(arg1)
.AddArg2(arg2)
.AddArg3(arg3)
.AddArg4(arg4)
.AddArg5(arg5);
var model = new builder.Build();
Пример такого билдера (здесь мы его вложили в сам класс, чтобы он мог пользоваться приватным конструктором):
public class Model
{
private Model(IReadOnlyCollection<Data> data, ...)
{
Data = data;
...
}
public IReadOnlyCollection<Data> Data { get; }
public static ModelBuilder Builder => new ModelBuilder();
public class ModelBuilder
{
private readonly List<Data> _data;
...
public ModelBuilder AddData(Data data)
{
_data.Add(data);
return this;
}
public Model Build()
{
return new Model(_data, ...);
}
}
}
Конечно же, билдер можно наследовать от интерфейса, чтобы иметь возможность создавать разные модели и осуществить полиморфизм.
public interface IModelBuilder {
...
Model Build();
}
public class ConcreteBuilderA : IModelBuilder
{
...
public Model Build() { ... }
}
public class ConcreteBuilderB : IModelBuilder
{
...
public Model Build() { ... }
}
Заметим, что билдер - инфраструктурный код, неприоритетный при проектировании.
Здесь же можем к билдеру внедрить директора:
public static class BuilderDirector
{
public static Builder DirectNumeric(
this Builder builder,
int count)
{
var enumerable = Enumerable.Range(0, count);
foreach (var i in enumerable)
{
var data = new DataA(i);
builder = builder.WithDataA(data);
}
return builder;
}
}
public interface IBuilderDirector
{
Builder Direct(Builder builder);
}
public class InstanceDirector : IBuilderDirector
{
private readonly int _size;
private IEnumerable<Model> _prototypes;
...
public Builder Direct(Builder builder) { ... }
}
Или же сделать цепочку из интерфейсов для получения данных:
public interface IAddressBuilder
{
ISubjectBuilder WithAddress(string address);
}
public interface ISubjectBuilder
{
IEmailBuilder WithSubject(string subject);
}
public interface IEmailBuilder
{
IEmailBuilder WithBody(string body);
Email Build();
}
public class Email
{
public static IAddressBuilder Builder => new EmailBuilder();
private class EmailBuilder : IAddressBuilder, ISubjectBuilder, IEmailBuilder { }
}
var email = Email.Builder
.WithAddress("aboba@email.com")
.WithSubject("subject")
.Build();
Здесь мы принуждаем к порядку сбора данных: адрес -> тема -> тело письма (опционально)
Примером работы билдера может явялется процесс создания пиццы из моей любимой франшизы пиццерий Додо Пицца: в билдере мы можем принять такие типы, как соус, начинка, топпинги, чтобы билдер сам сбилдил нам пиццу
С помощью прототипа мы можем упростить себе копирование объекта. Почему не пользоваться просто конструктором:
Примитивный прототип может быть таким:
public class Prototype
{
private readonly IReadOnlyCollection<int> _relatedEntityIds;
public Prototype(IReadOnlyCollection<int> relatedEntityIds)
{
_relatedEntityIds = relatedEntityIds;
}
public Prototype Clone()
{
return new Prototype(_relatedEntityIds);
}
}
Заметим, что здесь в методе Clone
передаем ссылку на коллекцию, то есть не копируем ее. Сделаем прототип с глубокой копией:
public class WrappedValue
{
public int Value { get; set; }
public WrappedValue Clone()
=> new WrappedValue{ Value = Value };
}
public class DeepCopyPrototype
{
private readonly List<WrappedValue> _values;
public DeepCopyPrototype(List<WrappedValue> values)
{
_values = values;
}
public DeepCopyPrototype Clone()
{
List<WrappedValue> values = _values.Select(x =>x.Clone()).ToList();
return new DeepCopyPrototype(values);
}
}
Теперь внедрим прототип в иерархию классов:
public abstract class Prototype
{
public void DoSomeStuff() { ... }
public abstract Prototype Clone();
}
public class ClassPrototype : Prototype
{
public void DoOtherStuff() { ... }
public override Prototype Clone() => new ClassPrototype();
}
Ну и напишем какой-нибудь скриптик для этого:
public class Scenario
{
public static Prototype CloneAndDoSomeStuff(Prototype prototype)
{
var clone = prototype.Clone();
clone.DoSomeStuff();
return clone;
}
public static void TopLevelScenario()
{
var prototype = new ClassPrototype();
Prototype clone = CloneAndDoSomeStuff(prototype);
clone.DoOtherStuff();
}
}
Здесь строка clone.DoOtherStuff();
вызовется ошибкой, так как у базового класса нет метода DoOtherStuff
. Ладно, попробуем сделать прототип при помощи интерфейса:
public interface IPrototype
{
IPrototype Clone();
void DoSomeStuff();
}
public class InterfacePrototype : IPrototype
{
IPrototype IPrototype.Clone() => Clone();
public InterfacePrototype Clone() => new InterfacePrototype();
public void DoSomeStuff() { ... }
public void DoOtherStuff() { ... }
}
И точно такой же скриптик:
public class Scenario
{
public static IPrototype CloneAndDoSomeStuff(IPrototype prototype)
{
var clone = prototype.Clone();
clone.DoSomeStuff();
return clone;
}
public static void TopLevelScenario()
{
var prototype = new InterfacePrototype();
IPrototype clone = CloneAndDoSomeStuff(prototype);
clone.DoOtherStuff();
}
}
Здесь опять же в clone.DoOtherStuff();
возникнет ошибка - мы ничего не знаем про класс-наследник. В этом случае мы можем скастить интерфейс к известному нами типу:
InterfacePrototype clone = (InterfacePrototype)CloneAndDoSomeStuff(prototype);
Но решим это при помощи рекурсивного параметр-типа - параметр-типа, ссылающегося на себя. Реализуем это при помощи дженериков в C#
public interface IPrototype<T> where T : IPrototype<T>
{
T Clone();
void DoSomeStuff();
}
public class Prototype : IPrototype<Prototype>
{
public Prototype Clone() => new Prototype();
public void DoSomeStuff() { ... }
public void DoOtherStuff() { ... }
}
Тот же самый скриптик:
public class Scenario
{
public static T CloneAndDoSomeStuff<T>(T prototype) where T : IPrototype<T>
{
var clone = prototype.Clone();
clone.DoSomeStuff();
return clone;
}
public static void TopLevelScenario()
{
var prototype = new Prototype();
Prototype clone = CloneAndDoSomeStuff(prototype);
clone.DoOtherStuff();
}
}
И здесь метод Clone
возвращает именно тип наследника
Синглтон - объект, для которого мы гарантируем, что одновременно будет существовать не больше одного экземпляра. Синглтоном может быть, например, глобальный кеш. Пример:
public class Singleton
{
private static readonly object _lock = new();
private static Singleton? _instance;
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance is not null)
return _instance;
lock (_lock) // с помощью lock гарантируем, что
{ // код ниже выполнится только в одном потоке
if (_instance is not null)
return _instance;
return _instance = new Singleton();
}
}
}
}
Также существует реализация через встроенный объект Lazy
:
public class Singleton
{
private static readonly Lazy<Singleton> _instance;
static Singleton()
{
_instance = new Lazy<Singleton>(()
=> new Singleton(), LazyThreadSafetyMode.ExecutionAndPublication);
}
private Singleton() { }
public static Singleton Instance => _instance.Value;
}
Синглтон считается антипаттерном и вот почему: