Фабричный шаблон в 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 напрямую, без проверки ввода. Я хотел бы полностью исключить эту техническую возможность.

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

17 ответов

Решение

Похоже, вы просто хотите запустить некоторую бизнес-логику перед созданием объекта - так почему бы вам просто не создать статический метод внутри "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 внутренним и переместить его и фабрику в их собственную сборку. Теперь только фабрика должна иметь возможность создавать экземпляр класса.

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

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

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

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

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

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

В основе этого решения лежит идея использования токена в конструкторе. Готово в этом ответе, убедитесь, что объект создан только фабрикой (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);
    }
}

Был бы признателен услышать некоторые мысли об этом решении. Единственный, кто может создать '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);
            }
        }
    }
}

Вот еще одно решение в духе "просто потому, что ты не можешь означать, что должен"...

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

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

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

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

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

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

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

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

Другие вопросы по тегам