Как бороться с конструкторами разной длины с шаблоном дизайна фабрики?

Я хочу иметь класс, который создает различные виды объектов на основе строки, которую я передаю. Из моих исследований, это лучше всего описывает шаблон проектирования фабрики. У меня есть успех в его реализации, но я столкнулся с проблемой проектирования: я не знаю, как создавать объекты с конструкторами разной длины.

Давайте возьмем для примера абстрактный родительский класс с именем Pet. Из него 3 детей: Рыба, Кошка и Собака. Все они наследуют вес и цвет от Pet, так что это входит в их конструкторы. Но рыбе может потребоваться множество плавников и логическое значение относительно того, является ли это морская рыба. Это конструктор с 4 параметрами. Кошка хотела бы количество ног. Это 3 параметра. У собаки могут быть параметры для ног, породы и того, хорошо ли он играет с другими собаками, по 5 параметрам.

В C++ я понимаю, что никакого отражения нет, поэтому наиболее распространенной практикой, по-видимому, является просто объявление карты строки для указателей на функции, где указатель на функцию указывает на функцию, которая выглядит примерно так:

    template<typename T> Pet* createObject(int weight, std::string color) {return new T(weight, color);}

Опять же, я не уверен, как бы я добавил больше параметров в вызов, не влияя на вызовы конструкторов других объектов.

Я могу придумать два обходных пути: создать новые функции для приема различного количества параметров или сделать параметры по умолчанию для конструкторов выше определенного размера.

Обходной путь 1 кажется чрезмерным в зависимости от того, сколько у меня разных размеров параметров.

Обходной путь 2, кажется, игнорирует всю точку конструктора, так как я буду вынужден назначать данные после вызова конструктора.

Есть ли другие лучшие способы обхода?

5 ответов

Решение

Вы можете использовать различные шаблоны и идеальную пересылку.

template<typename T, typename... Args>
Pet* createObject(Args&&... args) {
    return new T(std::forward<Args>(args)...);
}

Однако, поскольку любой указатель может быть преобразован в его базовый класс, вероятно, было бы лучше, если бы эта функция возвращала T*, Более того, использование голых указателей нецелесообразно, так как вам придется удалять их вручную. Лучше использовать shared_ptr или же unique_ptr, Для этих классов уже существуют похожие фабричные методы: make_shared а также make_unique (последняя только в C++14). Или, если ваш компилятор не поддерживает C++11, вы можете использовать shared_ptr а также make_shared от Boost.

Конечно, это решение работает, когда вы знаете во время компиляции, какой тип вам нужно создать. Если вы должны решить это во время выполнения, тогда вся проблема должна рассматриваться с другой стороны, как если бы вы не знали, какой тип вы собираетесь создать, то нет способа узнать, какие параметры им дать, за исключением параметров, общих для всех типов. В этом случае вам нужен абстрактный фабричный шаблон. К счастью, C++ (по крайней мере, из C++11) предоставляет способ реализовать этот шаблон без создания большого количества классов. Например, допустим, вы должны создать экземпляры некоторого класса, производного от Pet, Фактический вид домашнего животного, его размер и другие атрибуты определяются где-то еще, в то время как имя домашнего животного определяется во время создания. Тогда вам понадобится такая фабрика:

typedef std::function<std::shared_ptr<Pet>(const std::string& name)> PetFactory;

В какой-то момент вы решаете, что хотите создать Dog (Я оставляю значение фактических параметров создания для вашего воображения).

PetFactory petFactory =
        [](const std::string& name) {
            return std::make_shared<Dog>(name, 12, "brown", 23.5);
        }

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

std::shared_ptr<Pet> pet = petFactory("Pet Name");

Если, когда вы создаете объект, вы уже знаете его параметры и что это будет Рыба, вам вообще не нужна фабрика: просто создайте Рыбу, и все готово.

Вы можете разумно использовать фабрику, если вы не знаете, какой объект получится из нее, от вызывающей стороны. Например, вы вводите в качестве входных данных метода фабрики строку, возможно, считываемую из файла: фабрика создает и возвращает правильный тип объекта путем анализа строки.

Звонящий не знает, будет ли это рыба или собака: вот цель фабричного метода.

Более того, вы используете фабрику, когда можете расширить ее, добавив больше "конструируемых" объектов путем наследования и переопределив метод виртуального креациониста. Этого не происходит, если методы имеют разные подписи - на самом деле это разные методы

Ну, это было весело! Я прибег к использованию фабричных подфункций, которые анализируют поток на наличие аргументов. Я добавил шаблон регистрации для простоты использования и, конечно, хорошую дозу TMP.

Вот сокращенный код:

/* Pet and derived classes omitted */
/*    Registrar pattern omitted    */

struct PetFactory {
    using PetCtr = std::unique_ptr<Pet> (*)(std::istream &);

    static auto make(std::istream &stream) {
        std::string str;
        stream >> str;
        return ctrMap().at(str)(stream);
    }

    using PetCtrMap = std::map<std::string, PetCtr>;
    static PetCtrMap &ctrMap();
};

template <class T, class... Args, std::size_t... Idx>
auto streamCtr_(std::istream &stream, std::index_sequence<Idx...> const &) {

    std::tuple<Args...> args;

    using unpack = int[];
    unpack{0, (void(stream >> std::get<Idx>(args)), 0)...};

    return std::make_unique<T>(std::move(std::get<Idx>(args))...);
}

template <class T, class... Args>
auto streamCtr(std::istream &stream) {
    return std::unique_ptr<Pet>(streamCtr_<T, Args...>(
        stream,
        std::index_sequence_for<Args...>{}
    ));
}

int main() {
    PetFactory::make("fish 1 silver 5 true");
    PetFactory::make("cat 4 tabby 9");
    PetFactory::make("dog 17 white husky playful");
}

Выход:

I'm a 1kg silver fish with 5 fins, living in salty water.
I'm a 4kg tabby cat with 9 lives.
I'm a 17kg white husky and I'm playful.

Полный закомментированный код доступен здесь на Coliru. Спасибо за вызов!

Это то, что вам нужно (простите за утечки памяти и тому подобное)

#include <map>
#include <string>

// definition of pet hierarcy
class pet_t
{
public:
    virtual ~pet_t(void) {}
};

class frog_t : public pet_t
{
public:
    frog_t(int) {}
    static frog_t *builder(int n) { return new frog_t(n); }
};

class dog_t : public pet_t
{
public:
    dog_t(const char *, int) {}
    static dog_t *builder(const char *n, int p) { return new dog_t(n, p); }
};
// the per builder function type
typedef pet_t *(*pet_builder_t)(...);
// the map containing per builders: it's indexed by per type name
std::map<std::string, pet_builder_t> registry;
void build_factory(void)
{
    registry["frog"] = reinterpret_cast<pet_builder_t>(&frog_t::builder);
    registry["dog"] = reinterpret_cast<pet_builder_t>(&dog_t::builder);
}
// the actual factory function
template <class ...Ts>
pet_t *factory(std::string name, Ts&&...ts)
{
    pet_builder_t builder = registry[name];
    // assume there is something in the map
    return builder(std::forward<Ts>(ts)...);
}

int main(int argc, char *argv[])
{
    build_factory();
    dog_t  *dog  = dynamic_cast<dog_t  *>(factory(std::string("dog"), std::string("pluto"), 3));
    frog_t *frog = dynamic_cast<frog_t *>(factory(std::string("frog"), 7));
}

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

Есть много подходов к этой проблеме; тот, который я предпочитаю, может быть не самым элегантным, но это очень явно. По сути, вы создаете карту boost::any указатели, и все конструкторы теперь просто берут такую ​​карту.

using MyArgs = unordered_map<string, boost::any>;

class Fish {
  Fish(MyArgs args) {
    int num_fins = boost::any_cast<int>(args.at("num_fins"));
  }

Теперь все ваши конструкторы имеют одинаковую подпись, поэтому ваша фабрика может выглядеть так:

unique_ptr<Pet> factory(string animal_name, MyArgs args) {
  auto func = factory_map.at(animal_name);
  return func(args);
}

Изменить: Я также должен отметить, что если вы допустите ошибку, то есть ваши MyArgs пропускает аргумент или имеет неправильный тип, будет сгенерировано исключение. Таким образом, вы можете получить хорошую ясную ошибку и даже обработать ее, в отличие от получения UB.

Другие вопросы по тегам