Фабричный шаблон в C#: как гарантировать, что экземпляр объекта может быть создан только фабричным классом?
недавно я думал о защите некоторых из моего кода. Мне любопытно, как можно убедиться, что объект никогда не может быть создан напрямую, но только с помощью некоторого метода класса фабрики. Допустим, у меня есть класс" бизнес-объект", и я хочу убедиться, что любой экземпляр этого класса будет иметь допустимое внутреннее состояние. Для этого мне нужно будет выполнить некоторую проверку перед созданием объекта, возможно, в его конструкторе. Все в порядке, пока я не решу, что хочу сделать эту проверку быть частью бизнес-логики. Итак, как я могу организовать создание бизнес-объекта только с помощью какого-либо метода в моем классе бизнес-логики, но никогда напрямую? Первое естественное желание использовать старое доброе" дружеское " ключевое слово C++ не будет соответствовать C#. Поэтому нам нужны другие варианты...
давайте попробуем пример:
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 напрямую, без проверки ввода. Я хотел бы исключите эту техническую возможность вообще.
Итак, что думает об этом сообщество?
16 ответов:
Похоже, вы просто хотите запустить некоторую бизнес-логику перед созданием объекта - так почему бы вам просто не создать статический метод внутри "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);
вы можете сделать конструктор частным, а фабрику вложенным типом:
public class BusinessObject { private BusinessObject(string property) { } public class Factory { public static BusinessObject CreateBusinessObject(string property) { return new BusinessObject(property); } } }
это работает, потому что вложенные типы имеют доступ к закрытым членам своих вложенных типов. Я знаю, что это немного ограничительно, но надеюсь, это поможет...
или, если вы хотите пойти действительно фантазии, инвертировать управление: есть класс вернуть фабрику, и инструмент фабрики с делегатом, который может создать класс.
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; } }
:)
вы можете сделать конструктор на своем классе MyBusinessObjectClass внутренним и переместить его и фабрику в свою собственную сборку. Теперь только фабрика должна быть в состоянии построить экземпляр класса.
помимо того, что предложил Джон, вы также можете иметь заводской метод (включая проверку) быть статическим методом BusinessObject в первую очередь. Затем сделайте конструктор частным, и все остальные будут вынуждены использовать статический метод.
public class BusinessObject { public static Create (string myProperty) { if (...) return new BusinessObject (myProperty); else return null; } }
но реальный вопрос - почему у вас есть это требование? Допустимо ли переместить завод или заводской метод в класс?
еще один (легкий) вариант-сделать статический метод фабрики в классе BusinessObject и сохранить конструктор приватным.
public class BusinessObject { public static BusinessObject NewBusinessObject(string property) { return new BusinessObject(); } private BusinessObject() { } }
после стольких лет это было задано, и все ответы, которые я вижу, к сожалению, говорят вам, как вы должны делать свой код, а не давать прямой ответ. Фактический ответ, который вы искали, заключается в том, что ваши классы имеют частный конструктор, но общедоступный экземпляр, что означает, что вы можете создавать новые экземпляры только из других существующих экземпляров... которые доступны только на заводе:
интерфейс для своего класса:
public interface FactoryObject { FactoryObject Instantiate(); }
ваш класс:
public class YourClass : FactoryObject { static YourClass() { Factory.RegisterType(new YourClass()); } private YourClass() {} FactoryObject FactoryObject.Instantiate() { return new YourClass(); } }
и, наконец, фабрика:
public static class Factory { private static List<FactoryObject> knownObjects = new List<FactoryObject>(); public static void RegisterType(FactoryObject obj) { knownObjects.Add(obj); } public static T Instantiate<T>() where T : FactoryObject { var knownObject = knownObjects.Where(x => x.GetType() == typeof(T)); return (T)knownObject.Instantiate(); } }
затем вы можете легко модифицировать этот код, если вам нужны дополнительные параметры для создания или для предварительной обработки созданных экземпляров. И этот код позволит вам принудительно создать экземпляр через фабрику, поскольку конструктор класса является частным.
Итак, похоже, что то, что я хочу, не может быть сделано "чистым" способом. Это всегда какой-то" обратный звонок " в логический класс.
может быть, я мог бы сделать это простым способом, просто сделать метод 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; } }
таким образом, бизнес-объект не может быть создан напрямую, и метод публичной проверки в бизнес-логике также не повредит.
в случае хорошего разделения между интерфейсами и реализациями
шаблон protected-constructor-public-initializer позволяет очень аккуратное решение.данного бизнес-объекта:
public interface IBusinessObject { } class BusinessObject : IBusinessObject { public static IBusinessObject New() { return new BusinessObject(); } protected BusinessObject() { ... } }
и бизнес-фабрика:
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()
инициализатор. Но только тот, кто имеет необходимые ссылки является сама бизнес-фабрика.мы получили то, что хотели: единственный, кто может создать
BusinessObject
иBusinessFactory
.
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! }
Я бы поместил фабрику в ту же сборку, что и класс домена, и отметил конструктор класса домена внутри. Таким образом, любой класс в вашем домене может создать экземпляр, но Вы доверяете себе, не так ли? Любой, кто пишет код за пределами доменного уровня, должен будет использовать вашу фабрику.
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(); } }
это решение основано наmunificents идея использования токена в конструкторе. Сделано в этом ответе убедитесь, что объект создан только фабрикой (C#)
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); } }
были упомянуты несколько подходов с различными компромиссами.
- вложение класса фабрики в частный класс позволяет фабрике построить только 1 класс. В этот момент вам лучше с
Create
метод и частный ctor.- использование наследования и защищенного ctor имеет ту же проблему.
Я хотел бы предложить фабрику как частичный класс, который содержит частные вложенные классы с публичными конструкторами. Вы на 100% скрываете объект, который создает ваша фабрика, и только раскрываете то, что вы выбираете, через один или несколько интерфейсов.
вариант использования, который я слышал для этого, был бы, когда вы хотите отслеживать 100% экземпляров на заводе. Эта конструкция гарантирует, что никто, кроме фабрики, не имеет доступа к созданию экземпляров "химических веществ", определенных в "фабрике", и это устраняет необходимость в отдельной сборке для достижения этого.
== 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); } }
Я не понимаю, почему вы хотите отделить "бизнес-логику" от "бизнес-объекта". Это звучит как искажение ориентации объекта, и вы в конечном итоге свяжете себя узлами, приняв этот подход.
Я не думаю , что есть решение, которое не хуже, чем проблема, все, что он выше требует публичной статической фабрики, которая IMHO является худшей проблемой и не остановит людей, просто вызывающих фабрику, чтобы использовать ваш объект - он ничего не скрывает . Лучше всего предоставить интерфейс и / или сохранить конструктор как внутренний, если это возможно, это лучшая защита, так как сборка является доверенным кодом.
один из вариантов - иметь статический конструктор, который регистрирует фабрику где-то с помощью что-то вроде контейнера МОК.
вот еще одно решение в духе "только потому, что вы можете не означает, что вы должны" ...
Он соответствует требованиям сохранения конструктора бизнес-объекта в частном порядке и помещает логику фабрики в другой класс. После этого он становится немного отрывочным.
класс factory имеет статический метод для создания бизнес-объектов. Он является производным от класса бизнес-объектов для доступа к статическому защищенному методу построения, который вызывает частный конструктор.
фабрика абстрактна, поэтому вы не можете фактически создать ее экземпляр (потому что это также будет бизнес-объект, так что это было бы странно), и у нее есть частный конструктор, поэтому клиентский код не может быть получен из него.
что не помешало это клиентский код и вывод из класса бизнес-объектов и вызов защищенного (но не проверенного) статического метода построения. Или еще хуже, вызывая защищенный конструктор по умолчанию, который мы должны были добавить получить заводской класс для компиляции в первую очередь. (Что, кстати, может быть проблемой с любым шаблоном, который отделяет класс фабрики от класса бизнес-объекта.)
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() { } }