Какова цель такого сложного дизайна класса C++?

Я наткнулся на код с открытым исходным кодом C++ и мне стало любопытно, почему люди проектируют классы таким образом?

Итак, обо всем по порядку, вот класс Abstract:

class BaseMapServer
    {
    public:
        virtual ~BaseMapServer(){}

        virtual void LoadMapInfoFromFile(const std::string &file_name) = 0;
        virtual void LoadMapFromFile(const std::string &map_name) = 0;
        virtual void PublishMap() = 0;
        virtual void SetMap() = 0;
        virtual void ConnectROS() = 0;
    };

Здесь нет ничего особенного и наличие абстрактного класса может иметь несколько хорошо понятных причин. Таким образом, с этого момента я подумал, что, возможно, автор хочет поделиться общими чертами с другими классами. Итак, вот следующий класс, который является отдельным классом, но на самом деле содержит указатель типа абстрактного класса, упомянутого выше (фактический файл cpp, два других класса являются заголовочными файлами):

class MapFactory
{
    BaseMapServer *CreateMap(
            const std::string &map_type,
            rclcpp::Node::SharedPtr node, const std::string &file_name)
        {
            if (map_type == "occupancy") return new OccGridServer(node, file_name);
            else
            {
                RCLCPP_ERROR(node->get_logger(), "map_factory.cpp 15: Cannot load map %s of type %s", file_name.c_str(), map_type.c_str());
                throw std::runtime_error("Map type not supported")
            }
        }
};

А теперь самое интересное, вот дочерний класс абстрактного класса:

class OccGridServer : public BaseMapServer
    {
    public:
        explicit OccGridServer(rclcpp::Node::SharedPtr node) : node_(node) {}
        OccGridServer(rclcpp::Node::SharedPtr node, std::string file_name);
        OccGridServer(){}
        ~OccGridServer(){}

        virtual void LoadMapInfoFromFile(const std::string &file_name);
        virtual void LoadMapFromFile(const std::string &map_name);
        virtual void PublishMap();
        virtual void SetMap();
        virtual void ConnectROS();

    protected:
        enum MapMode { TRINARY, SCALE, RAW };

        // Info got from the YAML file
        double origin_[3];
        int negate_;
        double occ_th_;
        double free_th_;
        double res_;
        MapMode mode_ = TRINARY;
        std::string frame_id_ = "map";
        std::string map_name_;

        // In order to do ROS2 stuff like creating a service we need a node:
        rclcpp::Node::SharedPtr node_;

        // A service to provide the occupancy grid map and the message with response:
        rclcpp::Service<nav_msgs::srv::GetMap>::SharedPtr occ_service_;
        nav_msgs::msg::OccupancyGrid map_msg_;

        // Publish map periodically for the ROS1 via bridge:
        rclcpp::TimerBase::SharedPtr timer_;
    };

Так, какова цель класса MapFactory?

Чтобы быть более конкретным - в чем преимущество создания класса, который содержит указатель типа Abstract class BaseMapServer который является конструктором (я считаю), и этот странный конструктор создает память для нового объекта с именем OccGridServer и возвращает его? Я так запутался, только написав это. Я действительно хочу стать лучшим C++ кодером, и я отчаянно желаю узнать секрет этих конструкций кода.

Ответ 1

Класс MapFactory используется для создания правильного экземпляра подкласса BaseMapServer на основе переданных ему параметров.

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

BaseMapServer *CreateMap(
        const std::string &map_type,
        rclcpp::Node::SharedPtr node, const std::string &file_name)
    {
        if (map_type == "occupancy") return new OccGridServer(node, file_name);
        // create Type2Server
        else if (map_type == "type2") return new Type2Server(node, file_name);   
        // create Type3Server
        else if (map_type == "type3") return new Type3Server(node, file_name);
        else
        {
            RCLCPP_ERROR(node->get_logger(), 
                         "map_factory.cpp 15: Cannot load map %s of type %s", 
                         file_name.c_str(), map_type.c_str());
            throw std::runtime_error("Map type not supported")
        }
    }

Преимущество этого заключается в том, что вызывающей стороне не нужно знать точный используемый подкласс, и фактически базовый подкласс может потенциально измениться или даже быть заменен под капотом без необходимости изменения вызывающего кода. Фабричный метод усваивает эту логику для вас.

Ответ 2

Это фабричный образец. Смотрите https://en.wikipedia.org/wiki/Factory_method_pattern. Похоже, что текущий код поддерживает только одну реализацию (OccGridServer), но в будущем может быть добавлено еще больше. И наоборот, если когда-либо существует только одна конкретная реализация, то это чрезмерный дизайн.

Ответ 3

Это пример шаблона дизайна фабрики. Вариант использования такой: есть несколько типов очень похожих классов, которые будут использоваться в коде. В этом случае OccGridServer является единственным показанным на самом деле, но общее объяснение может ссылаться на гипотетические классы Dog, Cat, Otter и т.д. Из-за их сходства желателен некоторый полиморфизм: если все они наследуются от базового класса Animal они могут совместно использовать методы виртуального класса, такие как ::genus, ::species и т.д., И на производные классы можно указывать или ссылаться с помощью base указатели класса/ссылки. В вашем случае OccGridServer наследуется от BaseMapServer; предположительно есть и другие производные классы, а также указатели/ссылки.

Если вы знаете, какой производный класс нужен во время компиляции, вы обычно вызываете его конструктор. Смысл шаблона проектирования фабрики состоит в том, чтобы упростить выбор производного класса, когда конкретный производный класс неизвестен до времени выполнения. Представьте, что пользователь выбирает свое любимое животное, выбирая кнопку или вводя имя. Как правило, это означает, что где-то существует большой блок if/else, который отображается из определенного типа неоднозначности ввода/вывода (string, enum и т.д.) В определенный тип производного класса, вызывая его конструктор. Полезно инкапсулировать это в шаблоне фабрики, который может действовать как именованный конструктор, который принимает этот неоднозначитель как параметр "конструктор" и находит правильный производный класс для конструирования.

Обычно, кстати, CreateMap будет статическим методом BaseMapServer. Я не понимаю, почему в этом случае нужен отдельный класс для функции фабрики.