Є одна річ у С ++, яка змушує мене відчувати себе незручно досить довго, тому що я, чесно кажучи, не знаю, як це зробити, хоча це звучить просто:
Як правильно реалізувати заводський метод у C ++?
Мета: зробити можливим дозволити клієнтові інстанціювати який-небудь об’єкт за допомогою заводських методів замість конструкторів об'єкта без неприпустимих наслідків та досягнень ефективності.
Під "Заводським методом" я маю на увазі як статичні заводські методи всередині об'єкта, або методи, визначені в іншому класі, або глобальні функції. Просто загалом "концепція перенаправлення нормального способу інстанції класу X на інше місце, ніж конструктор".
Дозвольте мені проглянути кілька можливих відповідей, які я придумав.
0) Не робіть фабрик, не робіть конструкторів.
Це звучить приємно (і дійсно часто найкраще рішення), але не є загальним засобом. Перш за все, бувають випадки, коли побудова об'єкта є завданням досить складним, щоб виправдати його вилучення до іншого класу. Але навіть відкладати цей факт убік, навіть для простих об'єктів, що використовують лише конструктори, часто не обійдеться.
Найпростіший приклад, який я знаю, - це 2-D клас вектора. Так просто, але все ж хитро. Я хочу мати можливість побудувати це як з декартових, так і з полярних координат. Очевидно, я не можу:
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. Читай далі.
інший випадок: намагаючись перевантажити двома непрозорими typedefs деякого 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);
}
};
Класно? Часто, справді. Але потім - це змушує користувача використовувати тільки динамічне розподілення. Статичний розподіл - це те, що робить С ++ складним, але також є тим, що часто робить його потужним. Також я вважаю, що існують деякі цілі (ключове слово: вбудовані), які не дозволяють динамічно розподіляти. І це не означає, що користувачі цих платформ люблять писати чистий OOP.
У будь-якому випадку, філософія вбік: У загальному випадку я не хочу змушувати користувачів фабрики стримуватися до динамічного розподілу.
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 із гарантованим зменшенням копіювання копія, яка не підлягає копіюванню, вже не є проблемою для коду вище )
Висновок: Створення фабрики шляхом повернення об'єкта справді є рішенням для деяких випадків (наприклад, 2-D вектора, який вже згадувався), але все ще не є загальною заміною для конструкторів.
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*
.