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

user1496062
источник
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);
            }
        }
    }
}
Ронен Гланц
источник