Фабричний шаблон у C #: Як переконатися, що екземпляр об’єкта може бути створений лише заводським класом?


93

Нещодавно я думав про захист свого коду. Мені цікаво, як можна переконатись, що об’єкт ніколи не може бути створений безпосередньо, а лише за допомогою якогось методу заводського класу. Скажімо, у мене є клас «бізнес-об’єкт», і я хочу переконатися, що будь-який екземпляр цього класу матиме дійсний внутрішній стан. Для цього мені потрібно виконати певну перевірку перед створенням об'єкта, можливо, в його конструкторі. Це все гаразд, поки я не вирішу, що хочу зробити цю перевірку частиною бізнес-логіки. Отже, як я можу організувати можливість створення бізнес-об’єкта лише за допомогою якогось методу в моєму класі бізнес-логіки, але ніколи безпосередньо? Перше природне бажання використати старе добре ключове слово "друг" на 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
Я збентежений, чому б ви НЕ хотіли, щоб ваші бізнес-об'єкти відповідали за захист власних інваріантів? Суть заводського зразка (як я розумію) полягає в тому, щоб A) мати справу зі створенням складного класу з багатьма залежностями, або B) нехай фабрика сама вирішує, який тип виконання буде повертати, лише виставляючи інтерфейс / реферат клас для клієнта. Внесення Ваших бізнес-правил у Ваші бізнес-об’єкти є суттю ООП.
сара

@kai Правда, бізнес-об'єкт відповідає за захист свого інваріанту, але виклик конструктора повинен переконатися, що аргументи є дійсними, перш ніж передавати їх у конструктор ! Метою CreateBusinessObjectметоду є перевірка аргументів І (повернення нового дійсного об'єкта АБО повернення помилки) в одному виклику методу, коли абонент не відразу знає, чи аргументи є допустимими для передачі в конструктор.
Lightman

2
Я не погоджуюсь. Конструктор відповідає за необхідність перевірки заданих параметрів. Якщо я викликаю конструктор, і не виникає жодного винятку, я вважаю, що створений об'єкт перебуває у допустимому стані. Не повинно бути фізично можливо створити екземпляр класу за допомогою "неправильних" аргументів. Створюючи Distanceекземпляр із негативним аргументом, слід кинути ArgumentOutOfRangeExceptionприклад, наприклад, ви нічого не отримаєте від створення такого, DistanceFactoryякий робить ту саму перевірку. Я досі не бачу, що ви тут здобудете.
sara

Відповіді:


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
немає сенсу мати приватний конструктор за замовчуванням, якщо ви оголосили власний. також, у цьому випадку буквально нічого не можна отримати від використання статичного "конструктора" замість того, щоб просто зробити перевірку в реальному конструкторі (оскільки це все одно частина класу). це навіть ГІРШЕ, оскільки тепер ви можете отримати нульове значення повернення, відкрившись для NRE і "що, біса, означає це нуль?" питання.
сара

Будь-який хороший спосіб вимагати цієї архітектури через базовий клас Interface / abstract? тобто базовий клас інтерфейсу / анотації, який диктує, що реалізатори не повинні піддавати ctor.
Arash Motamedi

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

66

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

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

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

Це працює, оскільки вкладені типи мають доступ до приватних членів своїх закритих типів. Я знаю, що це трохи обмежує, але, сподіваюся, це допоможе ...


Я думав про це, але він ефективно переносить ці перевірки в сам бізнес-об'єкт, чого я намагаюся уникати.
Користувач

@ so-tester: Ви все ще можете мати чеки на заводі, а не в типі BusinessObject.
Джон Скіт,

3
@JonSkeet Я знаю, що це питання справді давнє, але мені цікаво, якою перевагою є розміщення CreateBusinessObjectметоду всередині вкладеного Factoryкласу замість того, щоб статичний метод був методом безпосередньо з BusinessObjectкласу ... чи можете ви згадати ваш мотивація до цього?
Kiley Naro

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

Але справжнє запитання - чому у вас така вимога? Чи прийнятно перенести фабрику чи фабричний метод у клас?


Це навряд чи є вимогою. Я просто хочу чітко розділити бізнес-об'єкт та логіку. Подібно як недоречно мати ці перевірки в кодах сторінок, я вважаю недоречним розміщувати ці перевірки в самих об'єктах. Ну, можливо, основна перевірка, але насправді не ділові правила.
Користувач

Якщо ви хочете розділити логіку та структуру класу лише для перевірки зручності, ви завжди можете використовувати частковий клас і мати обидва в окремих файлах ...
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 погодився. Я б змінив ім'я параметра factoryна asFriend. Тоді в моїй базі коду це відображатиметься якpublic static IBusinessObject New(BusinessFactory asFriend)
Reuven Bass

Я взагалі не розумію. Як це працює? Завод не може створювати нові екземпляри 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!
    }

Будь ласка, спробуйте мій підхід, "фабрика" - це контейнер обробника, і лише він може створити екземпляр для себе. з деякими змінами, це може відповідати вашим потребам.
lin

1
У вашому прикладі, чи не можливо наступне (що не мало б сенсу) ?: factory.DoWork ();
Whyser

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 має ту саму проблему.

Я хотів би запропонувати фабрику як частковий клас, який містить приватні вкладені класи з відкритими конструкторами. Ви на 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

Це лише один підхід, який можна використовувати. Я хочу, щоб бізнес-об’єктом були в основному структури даних, але не з усіма полями, відкритими для читання / запису. Але насправді питання полягало в можливості створення об’єкта лише за допомогою заводського методу, а не безпосередньо.
Користувач

@Jim: Я особисто погоджуюся з вашою думкою, але професійно це робиться постійно. Мій поточний проект - це веб-служба, яка приймає рядок HTML, очищає його за деякими попередньо встановленими правилами, а потім повертає очищений рядок. Мені потрібно пройти 7 рівнів мого власного коду, "оскільки всі наші проекти повинні будуватися з одного шаблону проекту". Зрозуміло, що більшість людей, які задають ці питання на SO, не мають свободи змінювати весь потік свого коду.
Флейтер

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.