В C ++ есть одна вещь, которая довольно долгое время заставляла меня чувствовать себя некомфортно, потому что я, честно говоря, не знаю, как это сделать, хотя это звучит просто:
Как правильно реализовать фабричный метод в 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
В 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
или ссылочные переменные-члены, - передать аргументы конструкторам базового класса и конструкторам объектов-членов.
И, вероятно, могут быть еще какие-то недостатки, о которых я не могу вспомнить прямо сейчас, и я даже не чувствую себя особенно обязанным, поскольку вышеупомянутые пункты уже меня убеждают.
Итак: даже близко к хорошему общему решению для реализации фабрики.
Выводы:
Мы хотим иметь способ создания экземпляра объекта, который:
- разрешить единообразное создание экземпляров независимо от распределения,
- давать разные значимые имена методам построения (таким образом, не полагаясь на перегрузку по аргументам),
- не приводить к значительному снижению производительности и, желательно, значительному раздутию кода, особенно на стороне клиента,
- быть общим, например: можно ввести для любого класса.
Я считаю, что доказал, что упомянутые мной способы не соответствуют этим требованиям.
Есть подсказки? Пожалуйста, дайте мне решение, я не хочу думать, что этот язык не позволит мне должным образом реализовать такую тривиальную концепцию.
static
распределение, это распределение стека, то есть локальное, то естьauto
в C ++ - person ThomasMcLeod   schedule 07.06.2013delete
. Такого рода методы вполне подходят, если документировано (исходный код - это документация ;-)), что вызывающий объект становится владельцем указателя (читайте: отвечает за его удаление, когда это необходимо). - person Boris Dalstein   schedule 15.07.2013unique_ptr<T>
вместоT*
. - person Kos   schedule 30.01.2014