В C ++ есть одна вещь, которая заставляет меня чувствовать себя некомфортно в течение достаточно долгого времени, потому что я, честно говоря, не знаю, как это сделать, хотя это звучит просто:
Как правильно реализовать Factory Method в C ++?
Цель: позволить клиенту создавать экземпляры некоторого объекта, используя фабричные методы вместо конструкторов объекта, без неприемлемых последствий и снижения производительности.
Под «фабричным методом» я подразумеваю как статические фабричные методы внутри объекта или методы, определенные в другом классе, так и глобальные функции. Как правило, «концепция перенаправления обычного способа создания экземпляров класса X куда-либо еще, кроме конструктора».
Позвольте мне просмотреть некоторые возможные ответы, о которых я подумал.
0) Не делайте фабрики, делайте конструкторов.
Это звучит неплохо (и зачастую является лучшим решением), но не является общим средством. Прежде всего, есть случаи, когда построение объекта является достаточно сложной задачей, чтобы оправдать его выделение в другой класс. Но даже если оставить этот факт в стороне, даже для простых объектов, использующих только конструкторы, часто не получится.
Самый простой пример, который я знаю, это 2-D класс Vector. Так просто, но сложно. Я хочу иметь возможность построить его как из декартовых, так и из полярных координат. Очевидно, я не могу сделать:
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);
// ...
};
Что вместо конструкторов приводит меня к использованию статических фабричных методов ... что по сути означает, что я каким-то образом реализую фабричный шаблон ("класс становится своей собственной фабрикой"). Это выглядит красиво (и подойдет в данном конкретном случае), но в некоторых случаях дает сбой, который я собираюсь описать в пункте 2. Продолжайте читать дальше.
другой случай: попытка перегрузки двумя непрозрачными определениями типов некоторого API (такими как GUID несвязанных доменов, или GUID и битовое поле), типы семантически совершенно разные (так - в теории - допустимые перегрузки), но которые на самом деле оказываются то же самое - например, неподписанные целые или пустые указатели.
1) Путь Явы
В Java все просто, поскольку у нас есть только динамически размещенные объекты. Создание фабрики так же тривиально, как:
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);
}
}
В C ++ это означает:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Прохладно? Часто так и есть. Но тогда - это заставляет пользователя использовать только динамическое распределение. Статическое распределение - это то, что делает C ++ сложным, но также и то, что часто делает его мощным. Кроме того, я считаю, что существуют некоторые цели (ключевое слово: внедренные), которые не позволяют динамическое размещение. И это не означает, что пользователям этих платформ нравится писать чистый ООП.
В любом случае, оставим философию: в общем случае я не хочу заставлять пользователей фабрики ограничиваться динамическим распределением.
2) Возврат по стоимости
Итак, мы знаем, что 1) здорово, когда мы хотим динамическое распределение. Почему мы не добавим статическое размещение поверх этого?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Какой? Мы не можем перегрузить типом возвращаемого значения? О, конечно, мы не можем. Итак, давайте изменим имена методов, чтобы отразить это. И да, я написал приведенный выше пример неверного кода, чтобы подчеркнуть, насколько мне не нравится необходимость менять имя метода, например, потому что мы не можем сейчас правильно реализовать независимый от языка дизайн фабрики, так как нам приходится менять имена - и каждый пользователь этого кода должен помнить об этом отличии реализации от спецификации.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
ОК ... там у нас это есть. Это ужасно, так как нам нужно изменить имя метода. Это несовершенно, поскольку нам нужно писать один и тот же код дважды. Но как только это будет сделано, это работает Правильно?
Ну обычно. Но иногда это не так. При создании Foo мы фактически полагаемся на то, что компилятор выполнит для нас оптимизацию возвращаемого значения, поскольку стандарт C ++ достаточно доброжелателен, чтобы поставщики компилятора не указывали, когда объект будет создан на месте и когда он будет скопирован при возврате временный объект по значению в C ++. Поэтому, если копировать Foo дорого, такой подход рискован.
А что если Foo вообще не копируется? Ну, дох ( Обратите внимание, что в C ++ 17 с гарантированным разрешением копирования, отсутствие возможности копирования больше не является проблемой для кода выше )
Вывод: создание фабрики путем возврата объекта действительно является решением для некоторых случаев (например, упомянутый ранее двумерный вектор), но все же не является общей заменой конструкторам.
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());
// ...
}
Можно подумать, что это работает как шарм. Единственная цена, которую мы платим в нашем коде ...
Поскольку я написал все это и оставил это как последнее, мне тоже это не понравится. :) Зачем?
Прежде всего ... Мне искренне не нравится концепция двухфазного строительства, и я чувствую вину, когда использую ее. Если я создаю свои объекты с утверждением, что «если он существует, он находится в допустимом состоянии», я чувствую, что мой код безопаснее и менее подвержен ошибкам. Мне так нравится.
Необходимость отказаться от этого соглашения И изменить дизайн моего объекта только для того, чтобы сделать из него фабрику ... ну, громоздко.
Я знаю, что вышесказанное не убедит многих, поэтому позвольте мне привести более веские аргументы. Используя двухфазную конструкцию, вы не можете:
- инициализировать
const
или ссылочные переменные-члены, - передать аргументы конструкторам базовых классов и конструкторам объектов-членов.
И, возможно, могут быть еще некоторые недостатки, о которых я не могу думать прямо сейчас, и я даже не чувствую особой необходимости, так как вышеупомянутые пункты пули уже убеждают меня.
Итак: даже близко к хорошему общему решению для реализации фабрики.
Выводы:
Мы хотим иметь способ создания объектов, который бы:
- разрешить единообразную реализацию независимо от распределения,
- дать разные, значимые имена методам конструирования (таким образом, не полагаясь на перегрузку по аргументам),
- не приводить к значительному падению производительности и, предпочтительно, значительному взлому кода, особенно на стороне клиента,
- быть общим, как в: можно ввести для любого класса.
Я считаю, что доказал, что упомянутые мной способы не соответствуют этим требованиям.
Есть намеки? Пожалуйста, предоставьте мне решение, я не хочу думать, что этот язык не позволит мне правильно реализовать такую тривиальную концепцию.
delete
. Методы такого типа прекрасно подходят, если они «документированы» (исходный код - документация ;-)), что вызывающий объект становится владельцем указателя (читай: отвечает за его удаление, когда это необходимо).
unique_ptr<T>
вместо T*
.