Статическая фабрика против фабрики как синглтон

24

В некоторых моих кодах у меня есть статическая фабрика, похожая на эту:

public class SomeFactory {

    // Static class
    private SomeFactory() {...}

    public static Foo createFoo() {...}

    public static Foo createFooerFoo() {...}
}

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

public class SomeFactory {

    public SomeFactory() {}

    public Foo createFoo() {...}

    public Foo createFooerFoo() {...}
}

Несколько вещей, чтобы выделить:

  • Обе фабрики не имеют гражданства.
  • Единственное различие между методами - это их область действия (экземпляр против статического). Реализации одинаковы.
  • Foo - это боб, у которого нет интерфейса.

Аргументы в пользу того, что я стою, были:

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

Аргументы для фабрики как синглтона были:

  • Хорошо впрыскивать все
  • Несмотря на заводское безгражданство, тестирование легче с помощью инъекций (легко издеваться)
  • Надо издеваться при тестировании потребителя

У меня есть некоторые серьезные проблемы с одноэлементным подходом, поскольку кажется, что никакие методы никогда не должны быть статичными. Также кажется, что такие утилиты, как, StringUtilsследует обернуть и ввести, что кажется глупым. Наконец, это означает, что мне нужно будет издеваться над фабрикой в ​​какой-то момент, что кажется неправильным. Я не могу вспомнить, когда мне нужно издеваться над фабрикой.

Что думает сообщество? Хотя мне не нравится одноэлементный подход, у меня, похоже, нет слишком сильного аргумента против этого.

bstempi
источник
2
Фабрика чем- то занята . Чтобы протестировать эту вещь изолированно, вы должны предоставить ее макет вашей фабрики. Если вы не издеваетесь над своей фабрикой, то ошибка в ней может привести к неудачному модульному тесту, если это не так.
Майк
Итак, вы выступаете против статических утилит в целом или просто статических фабрик?
bstempi
1
Я выступаю против них в целом. Например, в C # классы DateTimeand Fileсложно тестировать по тем же причинам. Если у вас есть класс, например, который устанавливает Createdдату DateTime.Nowв конструкторе, как вы собираетесь создавать модульный тест с двумя из этих объектов, которые были созданы с интервалом в 5 минут? А как насчет лет? Вы действительно не можете сделать это (без большой работы).
Майк
2
Разве на вашей фабрике-синглете не должно быть privateконструктора и getInstance()метода? Извините, неисправимая гнилька!
TMN
1
@Mike - Согласен - я часто использовал класс DateTimeProvider, который тривиально возвращает DateTime.Now. Затем вы можете поменять его на макет, который возвращает заданное время в ваших тестах. Это дополнительная работа, передаваемая всем конструкторам - самая большая головная боль - это когда коллеги не покупают ее на 100% и проскальзывают явные ссылки на DateTime.Now из лени ... :)
Джулия Хейворд,

Ответы:

15

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

public class Foo {

    // Private constructor
    private Foo(...) {...}

    public static Foo of(...) { return new Foo(...); }
}

Это то, что Джошуа Блох описывает как свой пункт 1 на странице 5 своей книги «Эффективная Ява». Java достаточно многословен без добавления дополнительных классов и одиночных игр и еще много чего. Есть некоторые случаи, когда отдельный класс фабрики имеет смысл, но они немногочисленны.

Перефразируя пункт 1 Джошуа Блоха, в отличие от конструкторов, статические фабричные методы могут:

  • Имена, которые могут описывать конкретные виды создания объектов (в качестве примера Bloch использует probablePrime (int, int, Random))
  • Вернуть существующий объект (думаю: вес в полете)
  • Возвратите подтип исходного класса или интерфейса, который он реализует
  • Уменьшить детализацию (указав параметры типа - см. Блох, например)

Недостатки:

  • Классы без открытого или защищенного конструктора не могут быть разделены на подклассы (но вы можете использовать защищенные конструкторы и / или элемент 16: отдавать предпочтение композиции над наследованием)
  • Статические фабричные методы не отличаются от других статических методов (используйте стандартное имя вроде of () или valueOf ())

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

GlenPeterson
источник
Я тоже об этом думал и думаю, мне это нравится. Я не помню, почему я разделил их в первую очередь.
bstempi
Вообще говоря, мы согласны со стилем Блоха. В конечном счете, мы собираемся переместить наши фабричные методы в класс, который они создают. Единственное различие между нашим и вашим решением состоит в том, что конструктор останется общедоступным, чтобы люди могли по-прежнему непосредственно создавать экземпляр класса. Спасибо за Ваш ответ!
bstempi
Но ... это ужасно. Скорее всего, вам нужен класс, реализующий IFoo, и передающий его. Используя этот шаблон, это больше невозможно; Вы должны жестко закодировать IFoo, чтобы обозначить Foo, чтобы получить экземпляры. Если у вас есть IFooFactory, который содержит IFoo create (), клиентскому коду не нужно знать детали реализации того, какой конкретный Foo был выбран в качестве IFoo.
Уилберт
@ Уилберт В public static IFoo of(...) { return new Foo(...); } чем проблема? Bloch-списки удовлетворяют вашу озабоченность как одно из преимуществ этого дизайна (второй пункт выше). Я не должен понимать ваш комментарий.
ГленПетерсон
Для создания экземпляра этот дизайн ожидает: Foo.of (...). Тем не менее, существование Foo никогда не должно быть разоблачено, должен быть известен только IFoo (поскольку Foo - это конкретный смысл IFoo). Наличие статического метода фабрики на конкретном импле нарушает это и приводит к связи между клиентским кодом и конкретной реализацией.
Уилберт
5

Вот только некоторые проблемы со статикой в ​​Java:

  • статические методы не играют по "правилам" ООП. Вы не можете заставить класс реализовать определенные статические методы (насколько я знаю). Таким образом, вы не можете иметь несколько классов, реализующих один и тот же набор статических методов с одинаковыми сигнатурами. Таким образом, вы застряли с одним из всего, что вы делаете. Вы также не можете создать подкласс статического класса. Так что нет полиморфизма и т. Д.

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

  • статические поля очень похожи на глобальные


Ты упомянул:

У меня есть некоторые серьезные проблемы с одноэлементным подходом, поскольку кажется, что никакие методы никогда не должны быть статичными. Также кажется, что такие утилиты, как StringUtils, нужно оборачивать и вставлять, что выглядит глупо. Наконец, это означает, что мне нужно будет издеваться над фабрикой в ​​какой-то момент, что кажется неправильным. Я не могу вспомнить, когда мне нужно издеваться над фабрикой.

Что заставляет меня любопытно спросить вас:

  • В чем, на ваш взгляд, преимущества статических методов?
  • Считаете ли вы, что StringUtilsправильно разработан и реализован?
  • почему вы думаете, что вам никогда не придется издеваться над фабрикой?

источник
Эй, спасибо за ответ! В том порядке, в котором вы спрашивали: (1) Преимущества статических методов - синтаксический сахар и отсутствие ненужного экземпляра. Приятно читать код со статическим импортом и говорить что-то вроде, Foo f = createFoo();а не иметь именованный экземпляр. Сам экземпляр не предоставляет никакой полезности. (2) Я так думаю. Он был разработан, чтобы преодолеть тот факт, что многие из этих методов не существуют в Stringклассе. Замена чего-то столь же фундаментального, как Stringне вариант, так что утилиты - это путь. (продолжение)
bstempi
Они просты в использовании и не требуют, чтобы вы осматривали все случаи жизни. Они чувствуют себя естественно. (3) Потому что моя фабрика очень похожа на фабричные методы внутри Integer. Они очень просты, имеют очень мало логики и предназначены только для обеспечения удобства (например, нет другой ОО-причины иметь их). Основная часть моего аргумента заключается в том, что они не полагаются на состояние - нет причин изолировать их во время тестов. Люди не издеваются StringUtilsпри тестировании - зачем им издеваться над этой простой фабрикой удобства?
bstempi
3
Они не высмеивают, StringUtilsпотому что есть разумная уверенность в том, что методы не имеют побочных эффектов.
Роберт Харви
@RobertHarvey Полагаю, я делаю то же самое в отношении своей фабрики - она ​​ничего не меняет. Так же, как StringUtilsвозвращает некоторый результат без изменения входных данных, моя фабрика также ничего не меняет.
bstempi
@bstempi Я собирался немного сократовского метода там. Я думаю, что я сосу на это.
0

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

Представьте, что ваше приложение развивается, и теперь в некоторых случаях вам нужно создать FooBar(подкласс Foo). Теперь вам нужно пройтись по всем местам в вашем коде, которые знают о вас, SomeFactoryи написать некую логику, которая просматривает someConditionи вызывает SomeFactoryили SomeOtherFactory. На самом деле, глядя на ваш пример кода, это хуже, чем это. Вы планируете просто добавить метод за методом для создания различных Foos, и весь ваш клиентский код должен проверить условие, прежде чем выяснить, какой Foo сделать. Это означает, что все клиенты должны иметь глубокие знания о том, как работает ваша Фабрика, и все они должны меняться всякий раз, когда новый Foo необходим (или удален!).

Теперь представьте, если вы только что создали, IFooFactoryчто делает IFoo. Теперь создание другого подкласса или даже совершенно другой реализации - это детская игра - вы просто вводите правильный IFooFactory выше по цепочке, а затем, когда клиент получает его, он вызывает gimmeAFoo()и получает правильный foo, потому что он получил правильную Factory ,

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

Вопросы могут быть представлены либо в Инструкции, либо в Упражнениях (которые оба реализуют IContentBlock), поэтому каждый из них InstructionsFactoryили ExerciseFactoryполучит копию QuestionFactoryMap. Затем различные фабрики с инструкциями и упражнениями передаются на тот, ContentBlockBuilderкоторый снова содержит хэш, который нужно использовать, когда. И затем, когда XML-файл загружен, ContentBlockBuilder вызывает фабрику упражнений или инструкций из хэша и запрашивает его createContent(), а затем Factory делегирует построение вопросов тому, что он извлекает из своей внутренней QuestionFactoryMap, на основе того, что находится в каждом узле XML против хеша. К сожалению , эта вещь BaseQuestionвместоIQuestion, но это просто говорит о том, что даже когда вы осторожны с дизайном, вы можете совершать ошибки, которые могут преследовать вас.

Ваш инстинкт говорит вам, что эти статические травмы причинят вам боль, и в литературе , безусловно , есть много того, что поддерживает вашу интуицию. Вы никогда не потеряете, используя Dependency Injection, которое вы используете только для своих модульных тестов (не то, чтобы я полагал, что если вы создадите хороший код, вы не найдете его более), но статика может полностью разрушить ваши способности быстро и легко изменить ваш код. Так зачем вообще туда ехать?

Эми Бланкеншип
источник
Задумайтесь на минуту о фабричных методах в Integerклассе - простых вещах, таких как преобразование из строки. Вот что Fooделает. Мой Fooне предназначен для расширения (моя вина в том, что я этого не сказал), поэтому у меня нет ваших полиморфных проблем. Я понимаю ценность наличия полиморфных фабрик и использовал их в прошлом ... Я просто не думаю, что они здесь уместны. Можете ли вы представить себе, нужно ли создавать экземпляр фабрики для выполнения простых операций Integer, которые в настоящее время запекаются через статические фабрики?
bstempi
Кроме того, ваши предположения о приложении довольно не верны. Приложение довольно вовлечено. Это Foo, однако, намного проще, чем многие классы. Это просто простой POJO.
bstempi
Если Foo предназначен для расширения, то именно поэтому есть причина, по которой Factory должна быть экземпляром - вы предоставляете правильный экземпляр Factory, чтобы дать вам правильный подкласс, а не вызывать if (this) then {makeThisFoo()} else {makeThatFoo()}весь код. Одна вещь, которую я заметил в людях с хронически плохой архитектурой, это то, что они похожи на людей с хронической болью. Они на самом деле не знают, каково это - не чувствовать боли, поэтому боль, в которой они находятся, не кажется такой уж плохой.
Эми Бланкеншип
Кроме того, вы можете проверить эту ссылку о проблемах со статическими методами (даже математическими!)
Эми Бланкеншип
Я обновил вопрос. Вы и еще один человек упомянули расширение Foo. По праву так - я никогда не объявлял это окончательным. Fooэто боб, который не предназначен для расширения.
bstempi