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

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

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

Ответы [ 17 ]

60 голосов
/ 05 февраля 2009

Вы можете сделать конструктор частным, а фабрика - вложенным:

public class BusinessObject
{
    private BusinessObject(string property)
    {
    }

    public class Factory
    {
        public static BusinessObject CreateBusinessObject(string property)
        {
            return new BusinessObject(property);
        }
    }
}

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

56 голосов
/ 05 февраля 2009

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

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

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

:)

15 голосов
/ 05 февраля 2009

Вы можете сделать конструктор вашего класса MyBusinessObjectClass внутренним и переместить его и фабрику в их собственную сборку. Теперь только фабрика должна иметь возможность создавать экземпляр класса.

7 голосов
/ 05 февраля 2009

Помимо того, что предложил Джон, вы также можете использовать метод фабрики (включая проверку) как статический метод BusinessObject. Затем оставьте конструктор закрытым, и все остальные будут вынуждены использовать статический метод.

public class BusinessObject
{
  public static Create (string myProperty)
  {
    if (...)
      return new BusinessObject (myProperty);
    else
      return null;
  }
}

Но реальный вопрос - почему у вас есть это требование? Допустимо ли переносить фабрику или фабричный метод в класс?

4 голосов
/ 05 февраля 2009

Еще один (легкий) вариант - создать статический метод фабрики в классе BusinessObject и сохранить конструктор закрытым.

public class BusinessObject
{
    public static BusinessObject NewBusinessObject(string property)
    {
        return new BusinessObject();
    }

    private BusinessObject()
    {
    }
}
3 голосов
/ 19 мая 2018

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

Интерфейс для ваших классов:

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

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

2 голосов
/ 23 апреля 2015
    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!
    }
2 голосов
/ 08 ноября 2011

В случае хорошего разделения между интерфейсами и реализациями
Шаблон 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.

2 голосов
/ 05 февраля 2009

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

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

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

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

...