Factory pattern в С#: Как обеспечить, чтобы экземпляр объекта мог быть создан только классом Factory?
Недавно я думал о том, чтобы защитить некоторые из моего кода. Мне любопытно, как можно было бы убедиться, что объект никогда не может быть создан напрямую, но только с помощью некоторого метода класса factory. Скажем, у меня есть класс "бизнес-объект", и я хочу убедиться, что любой экземпляр этого класса будет иметь правильное внутреннее состояние. Для этого мне нужно будет выполнить некоторую проверку перед созданием объекта, возможно, в его конструкторе. Все в порядке, пока я не решила, что хочу сделать эту проверку частью бизнес-логики. Итак, как я могу организовать создание бизнес-объекта только с помощью какого-либо метода в моем классе бизнес-логики, но никогда напрямую? Первым естественным желанием использовать старое "дружеское" ключевое слово С++ будет не хватать С#. Поэтому нам нужны другие варианты...
Попробуем пример:
public MyBusinessObjectClass
{
public string MyProperty { get; private set; }
public MyBusinessObjectClass (string myProperty)
{
MyProperty = myProperty;
}
}
public MyBusinessLogicClass
{
public MyBusinessObjectClass CreateBusinessObject (string myProperty)
{
// Perform some check on myProperty
if (true /* check is okay */)
return new MyBusinessObjectClass (myProperty);
return null;
}
}
Все в порядке, пока вы не вспомните, что вы можете создать экземпляр MyBusinessObjectClass напрямую, не проверяя ввод. Я хотел бы вообще исключить эту техническую возможность.
Итак, что думает об этом сообщество?
Ответы
Ответ 1
Похоже, вы просто хотите запустить некоторую бизнес-логику перед созданием объекта - так почему бы вам не создать статический метод внутри "BusinessClass", который выполняет всю грязную проверку "myProperty", и сделать конструктор закрытым?
public BusinessClass
{
public string MyProperty { get; private set; }
private BusinessClass()
{
}
private BusinessClass(string myProperty)
{
MyProperty = myProperty;
}
public static BusinessClass CreateObject(string myProperty)
{
// Perform some check on myProperty
if (/* all ok */)
return new BusinessClass(myProperty);
return null;
}
}
Вызов будет довольно простым:
BusinessClass objBusiness = BusinessClass.CreateObject(someProperty);
Ответ 2
Вы можете сделать конструктор частным, а factory вложенным типом:
public class BusinessObject
{
private BusinessObject(string property)
{
}
public class Factory
{
public static BusinessObject CreateBusinessObject(string property)
{
return new BusinessObject(property);
}
}
}
Это работает, потому что вложенные типы имеют доступ к закрытым членам своих закрывающих типов. Я знаю это немного ограничительно, но, надеюсь, это поможет...
Ответ 3
Или, если вы хотите по-настоящему выглядеть, инвертируйте управление: верните класс factory и инструмент factory с делегатом, который может создать класс.
public class BusinessObject
{
public static BusinessObjectFactory GetFactory()
{
return new BusinessObjectFactory (p => new BusinessObject (p));
}
private BusinessObject(string property)
{
}
}
public class BusinessObjectFactory
{
private Func<string, BusinessObject> _ctorCaller;
public BusinessObjectFactory (Func<string, BusinessObject> ctorCaller)
{
_ctorCaller = ctorCaller;
}
public BusinessObject CreateBusinessObject(string myProperty)
{
if (...)
return _ctorCaller (myProperty);
else
return null;
}
}
:)
Ответ 4
Вы можете сделать конструктор в своем классе MyBusinessObjectClass внутренним и перенести его и factory в свою собственную сборку. Теперь только factory должен иметь возможность построить экземпляр класса.
Ответ 5
Помимо того, что предложил Джон, вы могли бы либо использовать метод factory (включая проверку) как статический метод BusinessObject. Затем сделайте конструктор закрытым, и все остальные будут вынуждены использовать статический метод.
public class BusinessObject
{
public static Create (string myProperty)
{
if (...)
return new BusinessObject (myProperty);
else
return null;
}
}
Но реальный вопрос: почему у вас есть это требование? Допустимо ли переносить метод factory или factory в класс?
Ответ 6
Еще одна (легкая) опция заключается в создании статического метода factory в классе BusinessObject и сохранения закрытого конструктора.
public class BusinessObject
{
public static BusinessObject NewBusinessObject(string property)
{
return new BusinessObject();
}
private BusinessObject()
{
}
}
Ответ 7
Итак, похоже, что я хочу, не может быть сделано "чистым" способом. Это всегда какой-то "вызов" в логический класс.
Возможно, я мог бы сделать это простым способом, просто создайте метод contructor в классе объектов, сначала вызовите логический класс для проверки ввода?
public MyBusinessObjectClass
{
public string MyProperty { get; private set; }
private MyBusinessObjectClass (string myProperty)
{
MyProperty = myProperty;
}
pubilc static MyBusinessObjectClass CreateInstance (string myProperty)
{
if (MyBusinessLogicClass.ValidateBusinessObject (myProperty)) return new MyBusinessObjectClass (myProperty);
return null;
}
}
public MyBusinessLogicClass
{
public static bool ValidateBusinessObject (string myProperty)
{
// Perform some check on myProperty
return CheckResult;
}
}
Таким образом, бизнес-объект не создается напрямую, а метод общедоступной проверки в бизнес-логике тоже не повредит.
Ответ 8
В случае хорошего разделения между интерфейсами и реализациями
protected-constructor-public-initializer pattern позволяет очень аккуратное решение.
Для бизнес-объекта:
public interface IBusinessObject { }
class BusinessObject : IBusinessObject
{
public static IBusinessObject New()
{
return new BusinessObject();
}
protected BusinessObject()
{ ... }
}
и бизнес factory:
public interface IBusinessFactory { }
class BusinessFactory : IBusinessFactory
{
public static IBusinessFactory New()
{
return new BusinessFactory();
}
protected BusinessFactory()
{ ... }
}
следующее изменение в BusinessObject.New()
инициализатор дает решение:
class BusinessObject : IBusinessObject
{
public static IBusinessObject New(BusinessFactory factory)
{ ... }
...
}
Здесь для вызова инициализатора BusinessObject.New()
требуется ссылка на конкретный бизнес factory. Но единственный, у кого есть необходимая ссылка, - это сам бизнес factory.
Мы получили то, что хотели: единственный, кто может создать BusinessObject
, - BusinessFactory
.
Ответ 9
public class HandlerFactory: Handler
{
public IHandler GetHandler()
{
return base.CreateMe();
}
}
public interface IHandler
{
void DoWork();
}
public class Handler : IHandler
{
public void DoWork()
{
Console.WriteLine("hander doing work");
}
protected IHandler CreateMe()
{
return new Handler();
}
protected Handler(){}
}
public static void Main(string[] args)
{
// Handler handler = new Handler(); - this will error out!
var factory = new HandlerFactory();
var handler = factory.GetHandler();
handler.DoWork(); // this works!
}
Ответ 10
Я бы поместил factory в ту же сборку, что и класс домена, и пометьте внутренний конструктор класса домена. Таким образом, любой класс в вашем домене может создать экземпляр, но вы доверяете себе, не так ли? Любой, кто пишет код за пределами уровня домена, должен будет использовать ваш factory.
public class Person
{
internal Person()
{
}
}
public class PersonFactory
{
public Person Create()
{
return new Person();
}
}
Однако я должен подвергнуть сомнению ваш подход: -)
Я думаю, что если вы хотите, чтобы ваш класс Person был действительным при создании, вы должны поместить его в конструктор.
public class Person
{
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
Validate();
}
}
Ответ 11
Это решение основано на использовании munificents идеи использования токена в конструкторе. Сделано в этом ответе убедитесь, что объект создан только factory (С#)
public class BusinessObject
{
public BusinessObject(object instantiator)
{
if (instantiator.GetType() != typeof(Factory))
throw new ArgumentException("Instantiator class must be Factory");
}
}
public class Factory
{
public BusinessObject CreateBusinessObject()
{
return new BusinessObject(this);
}
}
Ответ 12
Были упомянуты несколько подходов с различными компромиссами.
- Вложение класса factory в приватно построенный класс позволяет factory построить 1 класс. В этот момент вам будет лучше с помощью метода
Create
и частного ctor.
- Использование наследования и защищенного ctor имеет такую же проблему.
Я хотел бы предложить factory как частичный класс, который содержит частные вложенные классы с общедоступными конструкторами. Вы на 100% скрываете объект, который ваш factory создает и только разоблачает то, что вы выбираете, через один или несколько интерфейсов.
Случай использования, который я слышал для этого, будет состоять в том, что вы хотите отслеживать 100% экземпляров в factory. Эта конструкция гарантирует, что никто, кроме factory, не имеет доступа к созданию экземпляров "химических веществ", определенных в "factory", и это устраняет необходимость в отдельной сборке для достижения этого.
== ChemicalFactory.cs ==
partial class ChemicalFactory {
private ChemicalFactory() {}
public interface IChemical {
int AtomicNumber { get; }
}
public static IChemical CreateOxygen() {
return new Oxygen();
}
}
== Oxygen.cs ==
partial class ChemicalFactory {
private class Oxygen : IChemical {
public Oxygen() {
AtomicNumber = 8;
}
public int AtomicNumber { get; }
}
}
== Program.cs ==
class Program {
static void Main(string[] args) {
var ox = ChemicalFactory.CreateOxygen();
Console.WriteLine(ox.AtomicNumber);
}
}
Ответ 13
Я не понимаю, почему вы хотите отделить "бизнес-логику" от "бизнес-объекта". Это звучит как искажение ориентации объекта, и в конечном итоге вы привяжете себя к узлам, приняв этот подход.
Ответ 14
Я не думаю, что есть решение, которое не хуже проблемы, все, что он выше, требует публичного статического factory, который ИМХО представляет собой худшую проблему и не заставит людей просто называть factory использовать ваш объект - он ничего не скрывает. Лучше всего выставить интерфейс и/или сохранить конструктор как внутренний, если вы можете, что лучшая защита, поскольку сборка является доверенным кодом.
Один из вариантов - иметь статический конструктор, который регистрирует factory где-то с чем-то вроде контейнера IOC.
Ответ 15
Вот еще одно решение в духе "просто потому, что вы можете не значит, что вам нужно"...
Он удовлетворяет требованиям сохранения частного конструктора бизнес-объектов и размещения логики factory в другом классе. После этого он становится немного отрывочным.
Класс factory имеет статический метод для создания бизнес-объектов. Он происходит от класса бизнес-объекта, чтобы получить доступ к статическому защищенному методу построения, который вызывает частный конструктор.
factory является абстрактным, поэтому вы не можете фактически создать его экземпляр (поскольку он также будет бизнес-объектом, поэтому это было бы странно), и у него есть частный конструктор, поэтому код клиента не может быть получен от него.
То, что не предотвращено, - это код клиента, также полученный из класса бизнес-объекта и вызывающий защищенный (но неутвержденный) статический метод построения. Или, что еще хуже, вызов защищенного конструктора по умолчанию, который мы должны были добавить, чтобы получить класс factory для компиляции в первую очередь. (Что, кстати, может быть проблемой для любого шаблона, который отделяет класс factory от класса бизнес-объекта.)
Я не пытаюсь предложить, чтобы кто-то в здравом уме должен делать что-то подобное, но это было интересное упражнение. FWIW, моим предпочтительным решением было бы использовать внутренний конструктор и границу сборки в качестве защиты.
using System;
public class MyBusinessObjectClass
{
public string MyProperty { get; private set; }
private MyBusinessObjectClass(string myProperty)
{
MyProperty = myProperty;
}
// Need accesible default constructor, or else MyBusinessObjectFactory declaration will generate:
// error CS0122: 'MyBusinessObjectClass.MyBusinessObjectClass(string)' is inaccessible due to its protection level
protected MyBusinessObjectClass()
{
}
protected static MyBusinessObjectClass Construct(string myProperty)
{
return new MyBusinessObjectClass(myProperty);
}
}
public abstract class MyBusinessObjectFactory : MyBusinessObjectClass
{
public static MyBusinessObjectClass CreateBusinessObject(string myProperty)
{
// Perform some check on myProperty
if (true /* check is okay */)
return Construct(myProperty);
return null;
}
private MyBusinessObjectFactory()
{
}
}