Вот эта одна вещь в С++, которая заставляла меня чувствовать себя некомфортно в течение довольно долгого времени, потому что я честно не знаю, как это сделать, хотя это звучит просто:
Как реализовать метод Factory в С++ правильно?
Цель: предоставить клиенту возможность создавать экземпляр какого-либо объекта с использованием методов Factory вместо конструкторов объектов без неприемлемых последствий и производительности.
В "шаблоне метода Factory" я имею в виду как статические методы Factory внутри объекта, так и методы, определенные в другом классе, или глобальные функции. Как правило, "концепция перенаправления нормального способа создания класса X в любом месте, кроме конструктора".
Позвольте мне просмотреть некоторые возможные ответы, о которых я думал.
0) Не создавайте фабрики, не создавайте конструкторы.
Это звучит неплохо (и действительно часто лучшее решение), но не является общим средством. Прежде всего, бывают случаи, когда построение объекта является сложным комплексом задач, чтобы оправдать его извлечение в другой класс. Но даже оставляя этот факт в стороне, даже для простых объектов, использующих только конструкторы, часто не будет.
Самый простой пример, который я знаю, - это двухмерный векторный класс. Так просто, но сложнее. Я хочу иметь возможность построить его как из декартовой, так и из полярной координат. Очевидно, я не могу:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Мой естественный образ мышления:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
Что, вместо конструкторов, приводит меня к использованию статических методов Factory... что по существу означает, что я каким-то образом реализую шаблон Factory ( "класс становится его собственным factory" )). Это выглядит красиво (и подходит для этого конкретного случая), но в некоторых случаях это не удается, о чем я расскажу в пункте 2. Продолжайте читать.
другой случай: попытка перегрузить два непрозрачных typedefs некоторого API (например, GUID не связанных между собой доменов или GUID и бит-поле), типы семантически совершенно разные (так что в теории - допустимые перегрузки), но которые фактически оказываются быть одним и тем же - как беззнаковые int или указатели void.
1) Путь Java
У Java это просто, поскольку у нас есть только объекты с динамическим распределением. Создание Factory столь же тривиально, как:
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
В С++ это означает:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Охладить? Часто, действительно. Но тогда это заставляет пользователя использовать только динамическое распределение. Статическое распределение - это то, что делает сложный С++, но также и то, что часто делает его мощным. Кроме того, я считаю, что существуют определенные цели (ключевое слово: embedded), которые не позволяют динамического выделения. И это не означает, что пользователям этих платформ нравится писать чистый ООП.
В любом случае, философия в стороне: в общем случае я не хочу принуждать пользователей Factory к ограничениям динамического размещения.
2) Возвращаемое значение
ОК, поэтому мы знаем, что 1) классно, когда мы хотим динамического выделения. Почему мы не добавим статическое распределение поверх этого?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Что? Мы не можем перегружать возвращаемый тип? О, конечно, мы не можем. Поэтому позвольте изменить имена методов, чтобы это отразить. И да, я написал неверный пример кода выше, просто чтобы подчеркнуть, насколько мне не нравится необходимость изменить имя метода, например, потому что мы не можем сейчас правильно разработать язык-агностик Factory, так как нам нужно изменить имена - и каждый пользователь этого кода должен будет помнить эту разницу в реализации из спецификации.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
ОК... там у нас это есть. Это уродливо, так как нам нужно изменить имя метода. Это несовершенно, так как нам нужно написать один и тот же код дважды. Но после этого он работает. Правильно?
Ну, обычно. Но иногда это не так. При создании Foo мы фактически полагаемся на компилятор, чтобы сделать оптимизацию возвращаемого значения для нас, потому что стандарт С++ достаточно доброжелателен, чтобы поставщики компилятора не указывали, когда будет создан объект на месте, и когда он будет скопирован при возврате временный объект по значению в С++. Поэтому, если Foo дорого копировать, этот подход является рискованным.
А что, если Foo вообще не скопируется? Ну, да. (Обратите внимание, что в С++ 17 с гарантированным копированием elision, not-being-copiable больше не проблема для кода выше)
Заключение: Создание Factory путем возврата объекта действительно является решением для некоторых случаев (например, ранее упомянутым двухмерным вектором), но все же не является общей заменой для конструкторов.
3) Двухфазная конструкция
Еще одна вещь, которую кто-то, вероятно, придумал, - это разделение проблемы выделения объекта и его инициализации. Обычно это приводит к следующему коду:
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
Можно подумать, что это работает как шарм. Единственная цена, которую мы платим в нашем коде...
Поскольку я написал все это и оставил это последним, мне тоже это не понравится.:) Почему?
Прежде всего... Я искренне не люблю концепцию двухфазной конструкции, и я чувствую себя виноватой, когда я ее использую. Если я создаю свои объекты с утверждением, что "если он существует, он находится в правильном состоянии", я считаю, что мой код более безопасен и менее подвержен ошибкам. Мне это нравится.
Необходимо отказаться от этого соглашения И изменить дизайн моего объекта только с целью сделать его Factory.. ну, громоздким.
Я знаю, что вышеупомянутое не убедит многих людей, поэтому позвольте мне дать более веские аргументы. Используя двухфазную конструкцию, вы не можете:
- initialize
const
или переменные элемента ссылки, - передать аргументы конструкторам базового класса и конструкторам объектов-членов.
И, вероятно, может быть еще несколько недостатков, о которых я не могу сейчас думать, и я даже не чувствую себя особенно обязанными, так как вышеупомянутые пулевые моменты уже меня убеждают.
Итак: даже близко к хорошему общему решению для реализации factory.
Выводы:
Мы хотим иметь способ создания объекта, который:
- допускает единообразный экземпляр независимо от его распределения,
- предоставляют разные, значимые имена для методов построения (таким образом, не полагаясь на перегрузку по аргументам),
- не вводит существенный удар производительности и, предпочтительно, значительный взлет разрастания кода, особенно на стороне клиента,
- быть общим, как в: возможно быть введенным для любого класса.
Я считаю, что доказал, что описанные мной способы не соответствуют этим требованиям.
Любые подсказки? Пожалуйста, предоставьте мне решение, я не хочу думать, что этот язык не позволит мне правильно реализовать такую тривиальную концепцию.