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


94

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

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


неправильно написан метод MyBoClass?
— pradeeptp

2
Я сбит с толку, почему бы вам НЕ хотеть, чтобы ваши бизнес-объекты несли ответственность за защиту своих инвариантов? Смысл шаблона фабрики (насколько я понимаю) состоит в том, чтобы либо А) иметь дело с созданием сложного класса со многими зависимостями, или Б) позволить фабрике решать, какой тип среды выполнения возвращать, выставляя только интерфейс / абстракцию. класс к клиенту. Помещение бизнес-правил в бизнес-объекты - это САМАЯ СУТЬ ООП.
— Сара

@kai Верно, бизнес-объект отвечает за его инвариантность, но вызывающий конструктор должен убедиться, что аргументы действительны, прежде чем передавать их в конструктор ! Цель CreateBusinessObjectметода - проверить аргументы И для (вернуть новый действительный объект ИЛИ вернуть ошибку) в одном вызове метода, когда вызывающий не сразу узнает, допустимы ли аргументы для передачи в конструктор.
— Лайтман

2
Я не согласен. Конструктор отвечает за выполнение всех необходимых проверок заданных параметров. Если я вызываю конструктор и не возникает исключения, я верю, что созданный объект находится в допустимом состоянии. Физически невозможно создать экземпляр класса с использованием «неправильных» аргументов. Создание Distanceэкземпляра с отрицательным аргументом должно вызвать, ArgumentOutOfRangeExceptionнапример, вы ничего не получите от создания, DistanceFactoryкоторый выполняет ту же проверку. Я все еще не понимаю, что вы здесь выиграете.
— Сара

Ответы:


55

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

6
Ах, вы также можете создать исключение вместо возврата null.
— Рикардо Нольде

5
нет смысла иметь частный конструктор по умолчанию, если вы объявили собственный. кроме того, в этом случае от использования статического «конструктора» буквально ничего не получится, вместо того, чтобы просто выполнять проверку в реальном конструкторе (поскольку он в любом случае является частью класса). это даже ХУЖЕ, так как теперь вы можете получить возвращаемое значение null, открывающееся для NRE и «что, черт возьми, это null означает?» вопросы.
— Сара

Есть ли хороший способ потребовать эту архитектуру через интерфейс / абстрактный базовый класс? т.е. интерфейс / абстрактный базовый класс, который требует, чтобы разработчики не открывали ctor.
— Араш Мотамеди

1
@Arash Нет. Интерфейсы не могут определять конструкторы, а абстрактный класс, который определяет защищенный или внутренний конструктор, не может помешать наследующим классам раскрывать его через собственный общедоступный конструктор.
— Рикардо Нольде

66

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

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

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

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


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

@ so-tester: у вас все еще могут быть проверки на заводе, а не на тип BusinessObject.
— Джон Скит,

3
@JonSkeet Я знаю, что этот вопрос действительно старый, но мне любопытно, в чем преимущество помещения CreateBusinessObjectметода внутри вложенного Factoryкласса вместо того, чтобы этот статический метод был методом непосредственно BusinessObjectкласса ... можете ли вы вспомнить свой мотивация для этого?
— Кили Наро

3
@KileyNaro: Ну, вот о чем спрашивал вопрос :) Это не обязательно дает много преимуществ, но дает ответ на вопрос ... хотя бывают случаи, когда это полезно - на ум приходит шаблон строителя. (В этом случае построитель будет вложенным классом, и у него будет вызван метод экземпляраBuild .)
— Джон Скит

53

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

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

:)


Очень красивый фрагмент кода. Логика создания объекта находится в отдельном классе, и объект может быть создан только с помощью фабрики.
— Николай Самтеладзе

Круто. Есть ли способ ограничить создание BusinessObjectFactory статическим BusinessObject.GetFactory?
— Феликс Кейл

1
В чем преимущество этой инверсии?
— Яцковский

1
@yatskovsky Это просто способ гарантировать, что объект может быть создан только контролируемым образом, при этом сохраняя возможность разложить логику создания на выделенный класс.
— Фабиан Шмид

15

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


7

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

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

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


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

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

7

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

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

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

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


4

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

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

    private BusinessObject()
    {
    }
}

2

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

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

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

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


2

В случае хорошего разделения интерфейсов и реализаций
шаблон защищенного конструктора-общедоступного-инициализатора позволяет получить очень изящное решение.

Учитывая бизнес-объект:

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.


Итак, вы убедитесь, что единственный рабочий общедоступный конструктор объекта требует Factory, и что необходимый параметр Factory может быть создан только путем вызова статического метода Factory? Похоже на рабочее решение, но у меня такое чувство, что такие фрагменты public static IBusinessObject New(BusinessFactory factory) вызовут у многих брови, если кто-то еще будет поддерживать код.
— Flater

@Flater Согласен. Я бы изменил имя параметра factoryна asFriend. В моей кодовой базе это будет отображаться какpublic static IBusinessObject New(BusinessFactory asFriend)
— Реувен Басс

Я вообще этого не понимаю. Как это работает? Фабрика не может создавать новые экземпляры IBusinessObject и не имеет намерения (методов) сделать это ...?
— Lapsus

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

Пожалуйста, попробуйте мой подход. «Фабрика» - это контейнер обработчика, и только он может создать для себя экземпляр. с некоторыми изменениями он может соответствовать вашим потребностям.
— лин

1
В вашем примере не было бы возможным следующее (что не имело бы смысла) ?: factory.DoWork ();
— Уайзер

1

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

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

1

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

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

  • Вложение фабричного класса в частный класс позволяет фабрике создать только 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);
    }
}

0

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


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

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

@Jim: Я лично согласен с вашей точкой зрения, но в профессиональном плане это происходит постоянно. Мой текущий проект - это веб-сервис, который принимает строку HTML, очищает ее в соответствии с некоторыми предустановленными правилами, а затем возвращает очищенную строку. Мне нужно пройти через 7 слоев моего собственного кода, «потому что все наши проекты должны быть построены на основе одного и того же шаблона проекта». Само собой разумеется, что большинство людей, задающих эти вопросы на SO, не получают свободы изменять весь поток своего кода.
— Flater

0

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

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


0

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

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

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

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

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

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

0

Был бы признателен за некоторые мысли об этом решении. Единственный, кто может создать 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);
            }
        }
    }
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.