Factory pattern в С#: Как обеспечить, чтобы экземпляр объекта мог быть создан только классом Factory?

Недавно я думал о том, чтобы защитить некоторые из моего кода. Мне любопытно, как можно было бы убедиться, что объект никогда не может быть создан напрямую, но только с помощью некоторого метода класса factory. Скажем, у меня есть класс "бизнес-объект", и я хочу убедиться, что любой экземпляр этого класса будет иметь правильное внутреннее состояние. Для этого мне нужно будет выполнить некоторую проверку перед созданием объекта, возможно, в его конструкторе. Все в порядке, пока я не решила, что хочу сделать эту проверку частью бизнес-логики. Итак, как я могу организовать создание бизнес-объекта только с помощью какого-либо метода в моем классе бизнес-логики, но никогда напрямую? Первым естественным желанием использовать старое "дружеское" ключевое слово С++ будет не хватать С#. Поэтому нам нужны другие варианты...

Попробуем пример:

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

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

Ответ 1

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

Ответ 2

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

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

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

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

Ответ 3

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

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

:)

Ответ 4

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

Ответ 5

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

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

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

Ответ 6

Еще одна (легкая) опция заключается в создании статического метода factory в классе BusinessObject и сохранения закрытого конструктора.

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

    private BusinessObject()
    {
    }
}

Ответ 7

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

Возможно, я мог бы сделать это простым способом, просто создайте метод 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;
    }
}

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

Ответ 8

В случае хорошего разделения между интерфейсами и реализациями protected-constructor-public-initializer pattern позволяет очень аккуратное решение.

Для бизнес-объекта:

public interface IBusinessObject { }

class BusinessObject : IBusinessObject
{
    public static IBusinessObject New() 
    {
        return new BusinessObject();
    }

    protected BusinessObject() 
    { ... }
}

и бизнес factory:

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() требуется ссылка на конкретный бизнес factory. Но единственный, у кого есть необходимая ссылка, - это сам бизнес factory.

Мы получили то, что хотели: единственный, кто может создать BusinessObject, - BusinessFactory.

Ответ 9

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

Ответ 10

Я бы поместил factory в ту же сборку, что и класс домена, и пометьте внутренний конструктор класса домена. Таким образом, любой класс в вашем домене может создать экземпляр, но вы доверяете себе, не так ли? Любой, кто пишет код за пределами уровня домена, должен будет использовать ваш factory.

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

Ответ 11

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

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

Ответ 12

Были упомянуты несколько подходов с различными компромиссами.

  • Вложение класса factory в приватно построенный класс позволяет factory построить 1 класс. В этот момент вам будет лучше с помощью метода Create и частного ctor.
  • Использование наследования и защищенного ctor имеет такую ​​же проблему.

Я хотел бы предложить factory как частичный класс, который содержит частные вложенные классы с общедоступными конструкторами. Вы на 100% скрываете объект, который ваш factory создает и только разоблачает то, что вы выбираете, через один или несколько интерфейсов.

Случай использования, который я слышал для этого, будет состоять в том, что вы хотите отслеживать 100% экземпляров в factory. Эта конструкция гарантирует, что никто, кроме factory, не имеет доступа к созданию экземпляров "химических веществ", определенных в "factory", и это устраняет необходимость в отдельной сборке для достижения этого.

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

Ответ 13

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

Ответ 14

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

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

Ответ 15

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

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

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

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

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

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

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