Чому я повинен використовувати заводський клас замість прямого об'єкта?


162

Я бачив історію декількох проектів бібліотек класів С # та Java на GitHub та CodePlex, і бачу тенденцію переходу на фабричні класи на відміну від прямого об'єкта.

Чому я повинен широко використовувати фабричні заняття? У мене досить гарна бібліотека, де об’єкти створюються старомодним способом - шляхом виклику громадських конструкторів класів. В останніх комісіях автори швидко змінили всіх публічних конструкторів тисяч класів на внутрішні, а також створили один величезний заводський клас з тисячами CreateXXXстатичних методів, які просто повертають нові об'єкти, викликаючи внутрішні конструктори класів. API зовнішнього проекту зламаний, молодець.

Чому така зміна була б корисною? Який сенс рефакторингу таким чином? Які переваги замінюють дзвінки конструкторам загальнодоступного класу статичними викликами фабричних методів?

Коли я повинен використовувати громадські будівельники і коли слід використовувати заводи?



1
Що таке клас / метод тканини?
Doval

Відповіді:


56

Заводські класи часто реалізовуються, оскільки дозволяють проекту більш чітко слідувати принципам SOLID . Зокрема, принципи сегрегації інтерфейсу та інверсії залежності.

Заводи та інтерфейси дозволяють значно більше довгострокової гнучкості. Це дозволяє зробити більш відокремленим - і, отже, більш перевіреним - дизайн. Ось не вичерпний список, чому ви можете піти цим шляхом:

  • Це дозволяє легко ввести контейнер Інверсії управління (IoC)
  • Це робить ваш код більш випробуваним, оскільки ви можете знущатися над інтерфейсами
  • Це дає вам набагато більше гнучкості, коли настає час змінити додаток (тобто ви можете створювати нові реалізації без зміни залежного коду)

Розглянемо цю ситуацію.

Збірка A (-> засоби залежить від):

Class A -> Class B
Class A -> Class C
Class B -> Class D

Я хочу перенести клас B на збірку B, яка залежить від збірки A. За допомогою цих конкретних залежностей я повинен перемістити більшу частину всієї своєї ієрархії класів поперек. Якщо я використовую інтерфейси, я можу уникнути великого болю.

Асамблея A:

Class A -> Interface IB
Class A -> Interface IC
Class B -> Interface IB
Class C -> Interface IC
Class B -> Interface ID
Class D -> Interface ID

Тепер я можу перенести клас В на збірку В, не маючи болю. Це все ще залежить від інтерфейсів у збірці A.

Використання контейнера IoC для вирішення залежностей дозволяє вам ще більше гнучкості. Немає необхідності оновлювати кожен виклик конструктору щоразу, коли ви змінюєте залежності класу.

Дотримуючись принципу сегрегації інтерфейсу та принципу інверсії залежностей, ми можемо будувати дуже гнучкі, роз'єднані програми. Після роботи над одним із цих типів додатків ви більше не захочете повернутися до використання newключового слова.


40
Фабрики ІМО - найменш важлива річ для SOLID. Ви можете зробити SOLID просто чудово без фабрик.
Ейфорія

26
Одна річ, яка ніколи не мала для мене сенсу, якщо ви використовуєте фабрики для виготовлення нових предметів, то вам потрібно зробити завод в першу чергу. То що саме це отримує? Чи передбачається, що хтось інший передасть вам фабрику замість того, щоб ви створили її самостійно, чи щось інше? Це слід зазначити у відповіді, інакше незрозуміло, як фабрики насправді вирішують будь-яку проблему.
Мехрдад

8
@ BЈовић - За винятком того, що кожного разу, коли ви додаєте нову реалізацію, тепер ви маєте зламати фабрику та змінити її для врахування нової реалізації - явно порушуючи OCP.
Теластин

6
@Telastyn Так, але код за допомогою створених об'єктів не змінюється. Це важливіше, ніж зміни на заводі.
BЈович

8
Фабрики корисні в певних сферах, на які було звернуто відповідь. Їх недоцільно використовувати скрізь . Наприклад, використання фабрик для створення рядків або списків буде занадто далеко. Навіть для УДТ вони не завжди потрібні. Ключовим моментом є використання фабрики, коли точну реалізацію інтерфейсу потрібно роз’єднати.

126

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

Справа в тому, що фабрики (і одинаки) надзвичайно прості у здійсненні, і тому люди їх використовують багато, навіть у місцях, де вони не потрібні. Отже, коли програміст думає: "Які шаблони дизайну я повинен використовувати в цьому коді?" Фабрика - це перше, що спадає йому на думку.

Багато заводів створюються, тому що "Можливо, одного разу мені потрібно буде створити ці класи по-іншому" на увазі. Що є явним порушенням YAGNI .

А заводи застарівають, коли ви впроваджуєте основу IoC, адже IoC - це лише своєрідна фабрика. І багато фреймворків IoC здатні створювати реалізацію конкретних заводів.

Крім того, не існує структури дизайну, яка б говорить про створення величезних статичних класів CreateXXXметодами, які викликають лише конструктори. І його особливо не називають Фабрикою (ні абстрактною Фабрикою).


6
Я згоден з більшістю ваших пунктів, окрім "IoC - це фабрика" : контейнери IoC не є фабриками . Вони є зручним методом досягнення ін'єкції залежності. Так, деякі здатні будувати автоматичні заводи, але вони самі не є фабриками і не повинні розглядатися як такі. Я також заперечував би точку YAGNI. Корисно мати можливість замінити тестовий подвійний у вашому тесті на одиницю. Переробляти все, щоб забезпечити це після факту - це біль у попі . Плануйте заздалегідь і не потрапляйте на виправдання "YAGNI"
AlexFoxGill

11
@AlexG - так ... на практиці майже всі контейнери IoC працюють як фабрики.
Теластин

8
@AlexG Основний фокус IoC - побудова конкретних об'єктів для переходу в інші об'єкти на основі конфігурації / конвенції. Це те саме, що і фабричне використання. І вам не потрібна фабрика, щоб мати змогу створити макет для тесту. Ви просто інстанціонуєте та передаєте макет безпосередньо. Як я вже сказав у першому абзаці. Фабрики корисні лише тоді, коли ви хочете передати створення екземпляра користувачеві модуля, який викликає завод.
Ейфорія

5
Існує відмінність. Модель заводу використовується споживачем для створення об'єктів під час виконання програми. Контейнер IoC , використовується для створення графа об'єкта програми під час запуску. Зробити це можна вручну, контейнер - це просто зручність. Споживач фабрики не повинен знати про контейнер IoC.
AlexFoxGill

5
Re: "І вам не потрібна фабрика, щоб можна було створити макет для тесту. Ви просто інстанціюєте та передаєте макет безпосередньо" - знову ж таки, різні обставини. Ви використовуєте фабрику, щоб запитувати екземпляр - споживач контролює цю взаємодію. Поставлення екземпляра через конструктор або метод не працює, це інший сценарій.
AlexFoxGill

79

Фабрика модельних моделей випливає з майже догматичної віри серед кодерів в мовах "стилю С" (C / C ++, C #, Java), що використання "нового" ключового слова є поганим, і його слід уникати будь-якою ціною (або за будь-яку ціну найменш централізовані). Це, у свою чергу, випливає з надто суворої інтерпретації Принципу єдиної відповідальності ("S" SOLID), а також принципу інверсії залежності ("D"). Простіше кажучи, SRP говорить, що в ідеалі кодовий об'єкт повинен мати одну "причину для зміни", і лише одну; що "причина зміни" є основною метою цього об'єкта, його "відповідальністю" в кодовій базі, і будь-що інше, що вимагає зміни коду, не повинно вимагати відкриття файлу класу. DIP ще простіший; об'єкт коду ніколи не повинен залежати від іншого конкретного об'єкта,

Справа в конкретному випадку, використовуючи "новий" і загальнодоступний конструктор, ви з'єднуєте код виклику з конкретним методом побудови конкретного класу конкретних. Тепер ваш код повинен знати, що клас MyFooObject існує і має конструктор, який приймає рядок і int. Якщо цьому конструктору коли-небудь потрібна додаткова інформація, усі звички конструктора повинні бути оновлені, щоб передати цю інформацію, включаючи ту, яку ви зараз пишете, і тому вони повинні мати щось дійсне для передачі, і тому вони повинні або мати його або бути зміненим, щоб отримати його (додавши більше обов'язків до споживаючих об'єктів). Крім того, якщо MyFooObject коли-небудь замінюється в кодовій базі на BetterFooObject, всі звички старого класу повинні змінитися, щоб сконструювати новий об'єкт замість старого.

Отже, натомість усі споживачі MyFooObject повинні бути безпосередньо залежні від "IFooObject", який визначає поведінку впроваджуючих класів, включаючи MyFooObject. Тепер споживачі IFooObjects не можуть просто побудувати IFooObject (не маючи знання про те, що конкретний конкретний клас є IFooObject, який їм не потрібен), тому натомість їм слід надати примірник класу чи методу, що реалізує IFooObject. ззовні, іншим об'єктом, який несе відповідальність за знання того, як створити правильний IFooObject для тієї обставини, яка на нашому мові зазвичай називається Фабрикою.

Тепер ось теорія відповідає дійсності; об'єкт ніколи не може бути закритий для всіх типів змін. Справа в тому, IFooObject тепер є додатковим кодовим об'єктом у кодовій базі, який повинен змінюватися щоразу, коли інтерфейс, необхідний споживачам або реалізація IFooObjects, змінюється. Це вводить новий рівень складності, пов'язаний із зміною способу взаємодії об'єктів один з одним у цій абстракції. Крім того, споживачам доведеться все-таки змінитись і ще глибше, якщо сам інтерфейс буде замінений на новий.

Хороший кодер знає, як збалансувати YAGNI ("Вам це не знадобиться") з SOLID, проаналізувавши дизайн та знайди місця, які, ймовірно, повинні змінитися певним чином, і переробляти їх на більш толерантність до такий тип змін, тому що в такому випадку "вам це знадобиться".


21
Любіть цю відповідь, особливо, прочитавши всі інші. Я можу додати майже всі (хороші) нові кодери занадто догматично щодо принципів простим фактом, який вони дійсно хочуть бути хорошими, але не дізнаються ще значення простоти речей і не перенавантажувати.
Жан

1
Інша проблема громадських конструкторів полягає в тому, що для класу немає приємного способу Fooвказати публічний конструктор, який повинен бути придатним для створення Fooекземплярів або створення інших типів у межах одного пакета / складання , але не повинен використовуватись для створення типи, похідні в іншому місці. Я не знаю жодної особливо переконливої ​​причини, що мова / фреймворк не міг визначити окремі конструктори для використання в newвиразах, а не виклики з конструкторів підтипу, але я не знаю жодної мови, яка робить таке розрізнення.
supercat

1
Такий сигнал буде захищеним та / або внутрішнім конструктором; цей конструктор буде доступний лише для споживання коду, будь то в підкласі або в межах однієї збірки. C # не має комбінації ключових слів для "захищеного та внутрішнього", тобто лише підтипи в складі можуть використовувати його, але MSIL має ідентифікатор сфери для цього типу видимості, тому можливо, що специфікація C # може бути розширена, щоб забезпечити спосіб її використання . Але це насправді не має великого відношення до використання заводів (якщо ви не використовуєте обмеження видимості для забезпечення використання фабрики).
KeithS

2
Ідеальна відповідь. Безпосередньо з частиною "теорія відповідає дійсності". Подумайте, скільки тисяч розробників і людського часу витратили вантажний культ, вихваляючи, обґрунтовуючи, впроваджуючи, використовуючи їх, щоб потім потрапити в ту саму ситуацію, яку ви описали, просто зводить з розуму. Слідом за YAGNI, Іве ніколи не визначив необхідності впровадження фабрики
Брено Салгадо

Я програмую на кодовій базі, де, оскільки реалізація OOP не дозволяє перевантажувати конструктор, використання публічних конструкторів ефективно заборонено. Це вже незначна проблема в мовах, які це дозволяють. Як створити температуру. І Фаренгейт, і Цельсій - поплавці. Тим не менш, ти можеш поставити їх у вікні, тоді проблема вирішена.
jgmjgm

33

Конструктори добре, коли містять короткий простий код.

Коли ініціалізація стає більше, ніж призначення кількох змінних полям, фабрика має сенс. Ось деякі переваги:

  • Довгий складний код має більше сенсу в спеціальному класі (на заводі). Якщо той самий код буде введено в конструктор, який викликає купу статичних методів, це забруднить основний клас.

  • У деяких мовах та деяких випадках кидання винятків у конструкторах - це дійсно погана ідея , оскільки може вводити помилки.

  • Коли ви викликаєте конструктор, вам, абоненту, потрібно знати точний тип екземпляра, який ви бажаєте створити. Це не завжди так (як-от Feeder, мені просто потрібно сконструювати Animal, щоб годувати його; мені все одно, чи це Dogа Cat).


2
Вибір - це не лише "фабрика" чи "конструктор". Архів Feederможе не використовувати жодного, а замість цього називати метод Kennelоб'єкта getHungryAnimal.
DougM

4
+1 Я вважаю, що дотримання правила абсолютно ніякої логіки в конструкторі не є поганою ідеєю. Конструктор слід використовувати лише для встановлення початкового стану об'єкта, присвоюючи його значення аргументів змінним екземпляра. Якщо потрібно щось складніше, принаймні зробіть фабричний (класний) метод для створення екземпляра.
KaptajnKold

Це єдина задовольняюча відповідь, яку я бачив тут, врятував мене від необхідності писати свою. Інші відповіді стосуються лише абстрактних понять.
TheCatWhisperer

Але цей аргумент може бути справедливим і для Builder Pattern. Чи не так?
soufrk

Правило конструкторів
Брено Салгадо

10

Якщо ви працюєте з інтерфейсами, ви можете залишатися незалежними від фактичної реалізації. Фабрику можна налаштувати (за допомогою властивостей, параметрів або якогось іншого методу) для отримання екземпляра однієї з кількох різних реалізацій.

Один простий приклад: ви хочете спілкуватися з пристроєм, але не знаєте, чи буде це через Ethernet, COM або USB. Ви визначаєте один інтерфейс і 3 реалізації. Після цього ви можете вибрати метод, який ви хочете, і фабрика дасть вам відповідну реалізацію.

Використовуйте його часто ...


5
Я додам, що це чудово використовувати, коли існує декілька реалізацій інтерфейсу, а викличний код не знає або не повинен знати, яку вибрати. Але коли заводський метод - це просто статична обгортка навколо одного конструктора, як у питанні, то це анти-шаблон. Там повинно бути кілька реалізацій , з яких , щоб вибрати або завод , що потрапляють в дорозі і додавання зайвої складності.

Тепер у вас є додаткова гнучкість, і теоретично ваша програма може використовувати Ethernet, COM, USB та серійні програми, теоретично вона готова до fireloop. Насправді ваш додаток спілкуватиметься лише через Ethernet .... коли-небудь.
Пітер Б

7

Це симптом обмеження в модульних системах Java / C #.

В принципі, немає жодної причини, щоб ви не могли мати можливість замінювати одну реалізацію класу на іншу тим самим підписом конструктора та методу. Там є мови , які дозволяють це. Однак Java та C # наполягають на тому, що кожен клас має унікальний ідентифікатор (повністю кваліфіковане ім'я), і клієнтський код закінчується жорсткою кодовою залежністю від нього.

Ви можете роду обійти це возилися з файлової системою і опцією компілятора , так що com.example.Fooкарти до іншого файлу, але це дивно і неінтуітівнимі. Навіть якщо ви це зробите, ваш код все одно пов'язаний лише з однією реалізацією класу. Тобто, якщо ви пишете клас, Fooякий залежить від класу MySet, ви можете вибрати реалізацію MySetв час компіляції, але ви все ще не можете інстанціювати Foo, використовуючи дві різні реалізації MySet.

Це невдале дизайнерське рішення змушує людей використовувати interfaces без необхідності для підтвердження свого коду в майбутньому проти можливості того, що їм пізніше буде потрібно щось реалізувати чи полегшити тестування одиниць. Це не завжди можливо; якщо у вас є якісь методи, які розглядають приватні поля двох екземплярів класу, не вдасться реалізувати їх в інтерфейсі. Ось чому, наприклад, ви не бачите unionв Setінтерфейсі Java . Тим не менше, за винятком числових типів і колекцій, бінарні методи не є звичайними, тому зазвичай ви можете піти з нею.

Звичайно, якщо ви телефонуєте, new Foo(...)ви все ще маєте залежність від класу , тож вам потрібна фабрика, якщо ви хочете, щоб клас мав можливість інстанціювати інтерфейс безпосередньо. Однак, як правило, краще прийняти екземпляр у конструкторі та дозволити комусь іншому вирішити, яку реалізацію використовувати.

Ви вирішуєте, чи варто роздувати вашу кодову базу інтерфейсами та фабриками. З одного боку, якщо відповідний клас є внутрішнім у вашій кодовій базі, рефакторинг коду таким чином, щоб він використовував інший клас чи інтерфейс, в майбутньому є тривіальним; ви можете викликати YAGNI та рефактор пізніше, якщо виникла ситуація. Але якщо клас є частиною публічного API бібліотеки, яку ви опублікували, у вас немає можливості виправити клієнтський код. Якщо ви не скористаєтеся interfaceі не потребуєте декількох реалізацій, то ви застряжете між скелею і важким місцем.


2
Я хотів би, щоб Java та похідні, такі як .NET, мали спеціальний синтаксис для класу, що створює самі екземпляри, і в іншому випадку newпросто був би синтаксичним цукром для виклику спеціально названого статичного методу (який би створювався автоматично, якби тип мав "загальнодоступний конструктор" ", але метод не включив явно). IMHO, якщо код просто хоче нудну річ за замовчуванням, що реалізується List, інтерфейс повинен бути можливим, щоб він міг надати його без того, щоб клієнт мав знати про якусь конкретну реалізацію (наприклад ArrayList).
supercat

2

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

А оскільки вони створили "величезний клас тканини з тисячами статичних методів CreateXXX", це звучить як анти-візерунок (можливо, клас Бога?).

Я думаю, що методи Simple Factory і статичні творці (які не потребують зовнішнього класу) можуть бути корисними в деяких випадках. Наприклад, коли побудова об'єкта вимагає різних кроків, таких як інстанція інших об'єктів (наприклад, надання переваги композиції).

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


1
Проста Фабрика має своє місце. Уявіть, що підприємство приймає два конструкторські параметри int xта IFooService fooService. Ви не хочете проходити fooServiceскрізь, тому ви створюєте фабрику методом Create(int x)і вводите службу всередині фабрики.
AlexFoxGill

4
@AlexG І тоді вам доведеться проходити IFactoryскрізь, а не скрізь IFooService.
Ейфорія

3
Я згоден з Ейфоріком; з мого досвіду, об'єкти, які вводяться у графік зверху, як правило, такого типу, який потребують усі об'єкти нижчого рівня, і тому передача IFooServices навколо не є великою проблемою. Заміна однієї абстракції іншою не приносить нічого іншого, окрім додаткового придушення бази даних коду.
KeithS

це виглядає абсолютно неважливо в питанні, яке задається "Чому я повинен використовувати заводський клас замість прямої конструкції об'єкта? Коли я повинен використовувати громадські конструктори і коли я повинен використовувати фабрики?" Дивіться, як відповісти
gnat

1
Ось лише назва питання, я думаю, ви пропустили решту. Дивіться останню точку наданого посилання;).
FranMowinckel

1

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

Як творець бібліотеки, ви б використовували заводські методи, якщо хочете використовувати цю гнучкість самостійно.

У випадку, який ви описуєте, у вас складається враження, що замінити конструктори фабричними методами було просто безглуздо. Безумовно, це був біль для всіх, хто бере участь; бібліотека не повинна нічого видаляти зі свого API без дуже вагомих причин. Тож якби я додав фабричні методи, я залишив би наявні конструктори доступними, можливо, застарілими, поки заводський метод вже не просто викличе цей конструктор і не використовує код, використовуючи звичайний конструктор, менш добре, ніж слід. Ваше враження цілком може бути правильним.


Також зверніть увагу; Якщо розробнику необхідно надати похідному класу додаткову функціональність (додаткові властивості, ініціалізація), розробник може це зробити. (якщо припустити, що заводські методи переборні). Автор API може також надати розробки для спеціальних потреб клієнта.
Ерік Шнайдер

-4

Це здається застарілим у епоху Scala та функціонального програмування. Міцна основа функцій замінює газильйонні класи.

Також слід зазначити, що подвійний Java {{більше не працює при використанні фабрики, тобто

someFunction(new someObject() {{
    setSomeParam(...);
    etc..
}})

Що дозволяє вам створити анонімний клас та налаштувати його.

У дилемі часового простору часовий фактор настільки зменшився завдяки швидким процесорам, що функціональне програмування, що дозволяє скоротити простір, тобто розмір коду, тепер практичне.


2
це більше схоже на дотичний коментар (див. Як відповісти ), і, здається, не пропонує нічого суттєвого щодо питань, викладених та пояснених у попередніх 9 відповідях
gnat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.