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

162

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

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

Почему такое изменение будет полезным? Какой смысл рефакторинга таким образом? Каковы преимущества замены вызовов конструкторов открытых классов статическими вызовами методов фабрики?

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

rufanov
источник
1
Что такое класс / метод ткани?
Довал

Ответы:

56

Фабричные классы часто реализуются, потому что они позволяют проекту более точно следовать принципам SOLID . В частности, принципы разделения интерфейсов и инверсии зависимостей.

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

  • Это позволяет легко вводить контейнер Inversion of Control (IoC)
  • Это делает ваш код более тестируемым, так как вы можете макетировать интерфейсы
  • Это дает вам гораздо больше гибкости, когда приходит время изменить приложение (т.е. вы можете создавать новые реализации без изменения зависимого кода)

Рассмотрим эту ситуацию.

Сборка А (-> означает, зависит от):

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

Я хочу переместить класс B в сборку B, которая зависит от сборки 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

Теперь я могу переместить класс B в сборку B безо всякой боли. Это все еще зависит от интерфейсов в сборке А.

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

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

Стивен
источник
40
Фабрики IMO - наименее важная вещь для SOLID. Вы можете сделать SOLID просто отлично без фабрик.
Эйфорическая
26
Одна вещь, которая никогда не имела смысла для меня, это то, что если вы используете фабрики для создания новых объектов, то вы должны создать фабрику в первую очередь. Так что именно это дает вам? Предполагается ли, что кто-то другой даст вам фабрику, а не вы ее создаете сами или что-то еще? Это должно быть упомянуто в ответе, в противном случае неясно, как фабрики действительно решают любую проблему.
Мердад
8
@ BЈовић - За исключением того, что каждый раз, когда вы добавляете новую реализацию, вы теперь можете взломать фабрику и изменить ее, чтобы учесть новую реализацию - явно нарушая OCP.
Теластин
6
@Telastyn Да, но код, использующий созданные объекты, не меняется. Это важнее, чем изменения на заводе.
BЈовић
8
Фабрики полезны в определенных областях, к которым относится ответ. Они не подходят для использования везде . Например, использование фабрик для создания строк или списков было бы слишком далеко. Даже для UDT они не всегда необходимы. Ключ должен использовать фабрику, когда точная реализация интерфейса должна быть отделена.
126

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

Дело в том, что фабрики (и синглеты) чрезвычайно легко внедрить, и поэтому люди часто их используют, даже там, где они не нужны. Поэтому, когда программист думает: «Какие шаблоны проектирования я должен использовать в этом коде?» Фабрика - первое, что приходит ему в голову.

Многие фабрики создаются потому, что «возможно, однажды мне нужно будет создать эти классы по-другому». Что является явным нарушением ЯГНИ .

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

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

Euphoric
источник
6
Я согласен с большинством ваших пунктов, за исключением того, что «IoC - это своего рода фабрика» : контейнеры IoC не являются фабриками . Они являются удобным способом достижения внедрения зависимости. Да, некоторые способны строить автоматические фабрики, но сами они не являются фабриками и не должны рассматриваться как таковые. Я бы также поспорил с ЯГНИ. Полезно иметь возможность заменить двойной тест в вашем модульном тесте. Рефакторинг всего, чтобы обеспечить это после факта, является болью в заднице . Планируйте заранее и не
поддавайтесь
11
@AlexG - да ... практически все контейнеры IoC работают как фабрики.
Теластин
8
@AlexG Основной задачей IoC является создание конкретных объектов для передачи в другие объекты на основе конфигурации / соглашения. Это то же самое, что использование фабрики. И вам не нужна фабрика, чтобы иметь возможность создавать макет для теста. Вы просто создаете экземпляр и передаете макет напрямую. Как я сказал в первом абзаце. Фабрики полезны только тогда, когда вы хотите передать создание экземпляра пользователю модуля, который вызывает фабрику.
Эйфорическая
5
Есть различие, которое будет сделано. Модель завода используется потребителем для создания объектов во время выполнения программы. Контейнер IoC , используется для создания графа объекта программы во время запуска. Вы можете сделать это вручную, контейнер просто удобство. Потребитель фабрики не должен знать о контейнере IoC.
AlexFoxGill
5
Re: «И вам не нужна фабрика, чтобы иметь возможность создавать макет для теста. Вы просто создаете экземпляр и передаете макет напрямую» - опять же, разные обстоятельства. Вы используете фабрику для запроса экземпляра - потребитель контролирует это взаимодействие. Предоставление экземпляра через конструктор или метод не работает, это другой сценарий.
AlexFoxGill
79

Модельный паттерн «Фабрика» проистекает из почти догматического убеждения программистов на языках «C-стиля» (C / C ++, C #, Java), что использование «нового» ключевого слова плохо, и его следует избегать любой ценой (или по крайней мере). наименее централизованный). Это, в свою очередь, исходит из ультра-строгой интерпретации принципа единой ответственности («S» SOLID), а также принципа обращения зависимостей («D»). Проще говоря, SRP говорит, что в идеале у объекта кода должна быть одна «причина для изменения» и только одна; эта «причина изменения» является центральной целью этого объекта, его «ответственностью» в кодовой базе и всем остальным, что требует изменения кода, не должно требовать открытия этого файла класса. DIP еще проще; объект кода никогда не должен зависеть от другого конкретного объекта,

Например, используя «new» и открытый конструктор, вы связываете вызывающий код с конкретным методом конструирования конкретного конкретного класса. Теперь ваш код должен знать, что класс MyFooObject существует и имеет конструктор, который принимает строку и int. Если этому конструктору когда-либо понадобится дополнительная информация, все виды использования конструктора должны быть обновлены, чтобы передать эту информацию, включая ту, которую вы пишете сейчас, и поэтому они должны иметь что-то допустимое для передачи, и поэтому они должны либо иметь это или быть изменено, чтобы получить это (добавление большего количества обязанностей к потребляющим объектам). Кроме того, если MyFooObject когда-либо будет заменен в кодовой базе на BetterFooObject, все виды использования старого класса должны измениться, чтобы создать новый объект вместо старого.

Таким образом, вместо этого все потребители MyFooObject должны напрямую зависеть от «IFooObject», который определяет поведение реализации классов, включая MyFooObject. Теперь потребители IFooObjects не могут просто создать IFooObject (не зная, что конкретный конкретный класс является IFooObject, который им не нужен), поэтому вместо этого им должен быть предоставлен экземпляр класса или метода, реализующего IFooObject. извне, другим объектом, который несет ответственность за знание того, как создать правильный IFooObject для обстоятельств, которые, на нашем языке, обычно называют Factory.

Теперь вот где теория встречается с реальностью; объект никогда не может быть закрыт для всех типов изменений все время. Например, IFooObject теперь является дополнительным объектом кода в базе кода, который должен меняться всякий раз, когда изменяется интерфейс, требуемый потребителями или реализациями IFooObjects. Это вводит новый уровень сложности, связанный с изменением способа взаимодействия объектов друг с другом в этой абстракции. Кроме того, потребители все равно должны будут измениться, и более глубоко, если сам интерфейс будет заменен на новый.

Хороший кодер знает, как сбалансировать YAGNI («Вам это не нужно») с SOLID, анализируя дизайн и находя места, которые с большой вероятностью могут измениться определенным образом, и реорганизуя их, чтобы они были более терпимыми к что тип изменения, потому что в этом случае «вы это собираетесь это нужно».

Keiths
источник
21
Люблю этот ответ, особенно после прочтения всех остальных. Я могу добавить, что почти все (хорошие) новые программисты слишком догматичны в отношении принципов из-за того простого факта, что они действительно хотят быть хорошими, но еще не научились ценить простоту и не переусердствовать.
Жан
1
Другая проблема с общедоступными конструкторами заключается в том, что у класса нет хорошего способа Fooуказать, что общедоступный конструктор должен использоваться для создания Fooэкземпляров или создания других типов в том же пакете / сборке , но не должен использоваться для создания типы получены в другом месте. Я не знаю какой-либо особенно убедительной причины, по которой язык / среда не могли определить отдельные конструкторы для использования в newвыражениях, в отличие от вызова из конструкторов подтипов, но я не знаю ни одного языка, который бы делал это различие.
суперкат
1
Защищенный и / или внутренний конструктор будет таким сигналом; этот конструктор будет доступен только для потребляющего кода в подклассе или в той же сборке. В C # нет комбинации ключевых слов для «защищенного и внутреннего», что означает, что ее могут использовать только подтипы в сборке, но MSIL имеет идентификатор области видимости для этого типа видимости, поэтому вполне возможно, что спецификацию C # можно расширить, чтобы обеспечить способ ее использования. , Но это на самом деле не имеет большого отношения к использованию фабрик (если только вы не используете ограничение видимости для принудительного использования фабрик).
KeithS
2
Идеальный ответ. Точно с точки зрения «теория встречает реальность». Подумайте только, сколько тысяч разработчиков и людей потратили время на похвалу культового культа, оправдывая, применяя их, чтобы потом попасть в ту же ситуацию, которую вы описали, просто сводит с ума. Вслед за YAGNI, я никогда не определял необходимость создания фабрики
Брено Сальгадо
Я программирую на базе кода, где, поскольку реализация ООП не позволяет перегружать конструктор, использование открытых конструкторов фактически запрещено. Это уже незначительная проблема в языках, которые это позволяют. Как создать температуру. И по Фаренгейту, и по Цельсию являются поплавками. Вы можете, однако, боксировать их, тогда проблема решена.
jgmjgm
33

Конструкторы хороши, когда они содержат короткий, простой код.

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

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

  • В некоторых языках и в некоторых случаях создание исключений в конструкторах - это действительно плохая идея , так как это может привести к ошибкам.

  • Когда вы вызываете конструктор, вы, вызывающая сторона, должны знать точный тип экземпляра, который вы хотите создать. Это не всегда так (как Feeder, мне просто нужно построить Animal, чтобы кормить его; мне все равно, если это Dogили Cat).

Арсений Мурзенко
источник
2
Выбор не только «фабрика» или «конструктор». A 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а позже вам нужно несколько реализаций, вы застрянете между молотом и наковальней.

Doval
источник
2
Я хотел бы, чтобы Java и производные, такие как .NET, имели специальный синтаксис для класса, создающего экземпляры самого себя, и в противном случае newпросто были бы синтаксическим сахаром для вызова специально названного статического метода (который генерировался бы автоматически, если бы у типа был открытый конструктор) "но явно не включал метод). ИМХО, если код просто хочет реализовать скучную вещь по умолчанию, которая Listдолжна быть реализована, интерфейс должен иметь возможность дать ему такую ​​информацию без необходимости знать клиенту какую-либо конкретную реализацию (например ArrayList).
суперкат
2

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

И так как они создали «огромный класс фабрики с тысячами статических методов CreateXXX», это звучит как анти-паттерн (возможно, Бог-класс?).

Я думаю, что в некоторых случаях могут быть полезны методы Simple Factory и статические создатели (для которых не требуется внешний класс). Например, когда построение объекта требует различных шагов, таких как создание экземпляров других объектов (например, создание композиции).

Я бы даже не назвал это Фабрикой, а просто набором методов, инкапсулированных в некоторый случайный класс с суффиксом «Фабрика».

FranMowinckel
источник
1
Простая фабрика имеет свое место. Представьте, что объект принимает два параметра конструктора, int xи IFooService fooService. Вы не хотите распространяться fooServiceвезде, поэтому вы создаете фабрику с помощью метода Create(int x)и внедряете сервис внутри фабрики.
AlexFoxGill
4
@AlexG И потом, IFactoryвместо того, чтобы ходить, вы должны проходить везде IFooService.
Эйфорическая
3
Я согласен с Euphoric; По моему опыту, объекты, которые вставляются в граф сверху, имеют тип, который нужен всем объектам более низкого уровня, и поэтому передача IFooServices не представляет особой проблемы. Замена одной абстракции другой не дает ничего, кроме дальнейшего запутывания кодовой базы.
KeithS
это выглядит совершенно неуместно для вопроса: «Почему я должен использовать фабричный класс вместо прямого конструирования объекта? Когда я должен использовать публичные конструкторы, а когда я должен использовать фабрики?» Смотрите, как ответить
gnat
1
Это только название вопроса, я думаю, что вы пропустили все остальное. Смотрите последнюю точку ссылки при условии;).
FranMowinckel
1

Как пользователь библиотеки, если в библиотеке есть фабричные методы, вы должны их использовать. Можно предположить, что фабричный метод дает автору библиотеки возможность вносить определенные изменения, не влияя на ваш код. Например, они могут возвращать экземпляр некоторого подкласса в методе фабрики, который не будет работать с простым конструктором.

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

В описываемом вами случае создается впечатление, что замена конструкторов фабричными методами была просто бессмысленной. Это было, конечно, боль для всех участников; библиотека не должна ничего удалять из своего API без веской причины. Поэтому, если бы я добавил фабричные методы, я бы оставил существующие конструкторы доступными, возможно, не рекомендуется, пока фабричный метод больше не просто вызывает этот конструктор, а код, использующий простой конструктор, работает не так хорошо, как следовало бы. Ваше впечатление вполне может быть правильным.

gnasher729
источник
Также обратите внимание; Если разработчику необходимо предоставить производному классу дополнительные функциональные возможности (дополнительные свойства, инициализация), разработчик может сделать это. (при условии, что фабричные методы могут быть переопределены). Автор API также может предоставить решение для особых потребностей клиентов.
Эрик Шнайдер
-4

Это кажется устаревшим в эпоху Scala и функционального программирования. Прочная основа функций заменяет газиллион классов.

Также следует отметить, что Java double {{больше не работает при использовании фабрики, т.е.

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

Который может позволить вам создать анонимный класс и настроить его.

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

Марк
источник
2
это больше похоже на тангенциальный комментарий (см. Как ответить ), и, похоже, он не предлагает ничего существенного по сравнению с замечаниями, сделанными и объясненными в предыдущих 9 ответах
комнат