Фабричный шаблон в C#:Как гарантировать, что экземпляр объекта может быть создан только фабричным классом?

StackOverflow https://stackoverflow.com/questions/515269

Вопрос

Недавно я задумался о защите части своего кода.Мне любопытно, как можно быть уверенным, что объект никогда не может быть создан напрямую, а только с помощью какого-либо метода фабричного класса.Допустим, у меня есть некий класс «бизнес-объекта», и я хочу убедиться, что любой экземпляр этого класса будет иметь допустимое внутреннее состояние.Чтобы добиться этого, мне нужно будет выполнить некоторую проверку перед созданием объекта, возможно, в его конструкторе.Все в порядке, пока я не решу, что хочу сделать эту проверку частью бизнес-логики.Итак, как я могу обеспечить возможность создания бизнес-объекта только с помощью какого-либо метода в моем классе бизнес-логики, но никогда напрямую?Первое естественное желание использовать старое доброе ключевое слово «друг» в 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 по-прежнему можно создать напрямую, без проверки входных данных.Я бы вообще исключил такую ​​техническую возможность.

Итак, что думает по этому поводу сообщество?

Это было полезно?

Решение

Похоже, вы просто хотите запустить некоторую бизнес-логику перед созданием объекта - так почему бы вам просто не создать статический метод внутри «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();
    }
}

Затем вы можете легко изменить этот код, если вам нужны дополнительные параметры для создания экземпляра или предварительной обработки создаваемых вами экземпляров.И этот код позволит вам принудительно создать экземпляр через фабрику, поскольку конструктор класса является закрытым.

Итак, похоже, что то, что я хочу, невозможно сделать «чистым» способом.Это всегда своего рода «обратный вызов» логическому классу.

Может быть, я мог бы сделать это по-простому: просто сделать так, чтобы метод конструктора в классе объекта сначала вызывал логический класс, чтобы проверить входные данные?

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();
  }
}

Это решение основано на щедрые идея использования токена в конструкторе.Готово в этом ответе убедитесь, что объект создан только фабрикой (С#)

  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 имеет ту же проблему.

Я хотел бы предложить фабрику как частичный класс, содержащий частные вложенные классы с открытыми конструкторами.Вы на 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);
    }
}

Я не понимаю, почему вы хотите отделить «бизнес-логику» от «бизнес-объекта».Это похоже на искажение объектной ориентации, и в конечном итоге вы завяжете себя узлами, применив такой подход.

Я не думаю, что есть решение, которое не хуже, чем проблема, все, что он выше, требует публичной статической фабрики, что, ИМХО, является более серьезной проблемой и не мешает людям просто вызывать фабрику, чтобы использовать ваш объект - это ничего не скрывает.Лучше всего предоставить интерфейс и/или сохранить конструктор как внутренний, если это возможно. Это лучшая защита, поскольку сборка является доверенным кодом.

Один из вариантов — иметь статический конструктор, который где-то регистрирует фабрику с чем-то вроде IOC-контейнера.

Вот еще одно решение в духе «то, что ты можешь, не значит, что ты должен»…

Он соответствует требованиям сохранения конфиденциальности конструктора бизнес-объекта и помещения фабричной логики в другой класс.После этого все становится немного схематично.

Фабричный класс имеет статический метод для создания бизнес-объектов.Он является производным от класса бизнес-объектов и обеспечивает доступ к статическому защищенному методу построения, который вызывает частный конструктор.

Фабрика является абстрактной, поэтому вы не можете фактически создать ее экземпляр (потому что это также будет бизнес-объект, и это было бы странно), и у нее есть частный конструктор, поэтому клиентский код не может быть производным от нее.

Что не предотвращается, так это клиентский код также производный от класса бизнес-объекта и вызывающий защищенный (но непроверенный) статический метод построения.Или, что еще хуже, вызов защищенного конструктора по умолчанию, который нам пришлось добавить, чтобы в первую очередь скомпилировать фабричный класс.(Что, кстати, может стать проблемой для любого шаблона, отделяющего класс фабрики от класса бизнес-объекта.)

Я не пытаюсь предложить кому-то в здравом уме сделать что-то подобное, но это было интересное упражнение.Кстати, моим предпочтительным решением было бы использовать внутренний конструктор и границу сборки в качестве защиты.

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()
    {
    }
}

Был бы признателен услышать некоторые мысли по поводу этого решения.Единственный, кто может создать MyClassPrivilegeKey, — это фабрика.и «MyClass» требует этого в конструкторе.Тем самым избегая размышлений о частных подрядчиках/"прописке" на заводе.

public static class Runnable
{
    public static void Run()
    {
        MyClass myClass = MyClassPrivilegeKey.MyClassFactory.GetInstance();
    }
}

public abstract class MyClass
{
    public MyClass(MyClassPrivilegeKey key) { }
}

public class MyClassA : MyClass
{
    public MyClassA(MyClassPrivilegeKey key) : base(key) { }
}

public class MyClassB : MyClass
{
    public MyClassB(MyClassPrivilegeKey key) : base(key) { }
}


public class MyClassPrivilegeKey
{
    private MyClassPrivilegeKey()
    {
    }

    public static class MyClassFactory
    {
        private static MyClassPrivilegeKey key = new MyClassPrivilegeKey();

        public static MyClass GetInstance()
        {
            if (/* some things == */true)
            {
                return new MyClassA(key);
            }
            else
            {
                return new MyClassB(key);
            }
        }
    }
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top