Фабричный шаблон в 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 79

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