Объектно-ориентированный дизайн системы бронирования отелей

Я практикую объектно-ориентированный дизайн для предстоящего интервью. Мой вопрос касается дизайна системы бронирования отелей: - Система должна иметь возможность возвращать открытый номер определенного типа или возвращать все открытые номера в отеле. - В отеле есть много типов номеров, таких как обычные, роскошные, знаменитости и так далее.

До сих пор я придумал следующие классы:

Class Room{
//Information about room
virtual string getSpecifications(Room *room){};
}

Class regularRoom: public Room{
//get specifications for regular room
}

Class luxuryRoom: public Room{
//get specifications for regular room
}
//Similarly create as many specialized rooms as you want

Class hotel{
vector<Room *>openRooms; //These are all the open rooms (type casted to Room type pointer)

Public:
Room search(Room *aRoom){ //Search room of a specific type
        for(int i=0;i<openRooms.size();i++){
            if(typeid(*aRoom)==typeid(*openRooms[i])) return *openRooms[i];
        }
}

vector<Room> allOpenRooms(){//Return all open rooms
...
}

}

Я запутался в реализации метода hotel.search(), где я проверяю тип (который, я считаю, должен каким-то образом обрабатываться полиморфизмом). Есть ли лучший способ спроектировать эту систему, чтобы поиск и все методы OpenRooms можно было реализовать без явной проверки типа объектов?


person Mishra    schedule 16.06.2013    source источник
comment
Тест typeid(aRoom)==typeid(openRooms[i]) всегда будет успешным, потому что aRoom является Room*, а openRooms содержит элементы типа Room*. Поэтому вы можете просто заменить if(typeid(aRoom)==typeid(openRooms[i])) на if(true).   -  person Oswald    schedule 16.06.2013
comment
@ Освальд: Правда? Я думал, что tyepid разрешит значение, соответствующее АКТУАЛЬНОМУ типу аргумента.   -  person Mats Petersson    schedule 16.06.2013
comment
Да. Room* является фактическим типом. Если вы хотите проверить, имеют ли комнаты, на которые указывают ваши указатели, разные типы среды выполнения, вы должны проверить, typeid(*aRoom)==typeid(*openRooms[i])   -  person Oswald    schedule 16.06.2013
comment
Я думаю, что typeid возвращает информацию о динамическом типе.   -  person Mishra    schedule 16.06.2013
comment
Обновил код. Спасибо Освальду.   -  person Mishra    schedule 16.06.2013
comment
Трудно ответить лучше, не имея более подробного контекста спецификаций вопроса об отеле. Использование метода поиска typeid в порядке, учитывая другие проблемы здесь. Вы должны использовать list вместо vector, так как комнаты должны открываться и закрываться, что означает, что ваш openRooms участник изменится. Поскольку при бронировании отеля будет выполняться много поисков, вам также следует подумать о том, чтобы класс вашего отеля был оптимизирован для поиска, например, используя hash_map с type_info в качестве ключа и list открытых номеров для этого типа в качестве значения. .   -  person Alan    schedule 16.06.2013
comment
@Alan Поиск с использованием typeid - это худшее решение, которое только можно себе представить. Он маскирует атрибут в типе, что является плохим дизайном и, мягко говоря, сбивает с толку.   -  person James Kanze    schedule 16.06.2013
comment
@JamesKanze: Не могли бы вы уточнить, почему это худшее? Я могу признать, что это сбивает с толку, ему, конечно, задан синтаксис, но в данном случае атрибутом является тип: Luxury, Standard, Executive и т. д. Из вашего ответа ниже, если номера просто различаются в зависимости от типа, я согласен.   -  person Alan    schedule 16.06.2013
comment
Поиск метода берет экземпляр комнаты и возвращает первую открытую комнату, которая имеет тот же тип комнаты, что и аргумент. Разве это не проблема курицы и яйца? Как я собираюсь создать экземпляр комнаты для передачи методу в первую очередь?   -  person prasopes    schedule 16.06.2013
comment
Кроме того, не лучше ли возложить ответственность за знание статуса номера (занят/свободен) на сам номер, а не на отель? Системе бронирования отеля, вероятно, потребуется отслеживать больше статусов номеров, чем просто «свободно/занято» (например, когда клиент выписывается из номера, номер не может быть немедленно переведен в состояние «свободен», персонал отеля должен сначала убрать его). ). Будете ли вы иметь одну коллекцию для каждого статуса комнаты? Что, если у комнаты может быть сразу несколько статусов? (Пример: нуждается в очистке и обслуживании...)   -  person prasopes    schedule 16.06.2013
comment
@Alan Алан Именно по той причине, о которой я говорю. Это маскирует атрибут в типе. Это было бы оправдано, если бы разные типы комнат имели разное поведение (но даже тогда я бы использовал виртуальную функцию matches( Criteria const& other ) const).   -  person James Kanze    schedule 17.06.2013


Ответы (6)


Вы всегда можете позволить комнате нести свой реальный тип вместо того, чтобы сравнивать тип объекта:

enum RoomType
{
  RegularRoom,
  luxuryRoom
};

class Room{
public:
  explicit Room(RoomType room_type) : room_type_(room_type) { }
  virtual ~Room(){}

  RoomType getRoomType() const { return room_type_; }
private:
  RoomType room_type_;     // carries room type
};

class regularRoom: public Room{
public:
  regularRoom() : Room(RegularRoom){ }
};


Room search(Room *aRoom)
{
   //Search room of a specific type
   for(size_t i=0;i<openRooms.size();i++)
   {
     if (aRoom->getRoomType() == RegularRoom)  // <<-- compare room type
     {
         // do something
      }
    }
};
person billz    schedule 16.06.2013
comment
Это определенно возможно, но это не сильно отличается от использования typeid(). Здесь тип маскируется под член данных класса. Мне интересно знать альтернативные шаблоны дизайна, которые могут полностью исключить сравнение типов комнат, если это возможно. - person Mishra; 16.06.2013
comment
Не обязательно, так как наличие подклассов позволяет добавлять атрибуты определенных комнат. Например, в роскошном номере одним из атрибутов может быть бильярдный стол. - person Saurabh Goyal; 10.11.2015

Просмотр объектов подкласса с вопросом, к какому типу они относятся, на самом деле не является хорошей иллюстрацией дизайна o-o. Вам действительно нужно что-то сделать со всеми комнатами, не зная, к какому типу относится каждая из них. Например, распечатайте ежедневное меню для комнаты (которое может быть разным для разных типов). Преднамеренный поиск типа объекта подкласса, хотя и не является неправильным, не является хорошим стилем o-o. Если вы просто хотите сделать это, как сказали другие респонденты, просто создайте «комнаты» с набором свойств.

person Spalteer    schedule 16.06.2013

У разных типов комнат разное поведение? Судя по вашему описанию, это не тот случай, когда следует использовать наследование. У каждой комнаты просто есть атрибут type, который в своей простейшей форме представляет собой просто перечисление.

person James Kanze    schedule 16.06.2013

Если вы действительно хотите проверить, что комната имеет тот же тип, что и другая комната, то typeid() так же хорош, как и любой другой метод, и это, безусловно, "лучше" (по крайней мере, с точки зрения производительности) для вызова виртуального метода.

Другой вариант — вообще не иметь отдельных классов и хранить тип комнаты в качестве переменной-члена (и, конечно, именно так я бы его и разработал, но это не очень хороший дизайн для изучения объектной ориентации и наследования — вы не получите для наследования, когда базовый класс выполняет все ваши потребности).

person Mats Petersson    schedule 16.06.2013
comment
Я рассматривал возможность сохранения переменной-члена для типа комнаты, но исключил это, потому что меня могут попросить написать новые методы, такие как raisePrice(), которые сильно зависят от типа комнаты. Выполнение этого в базовом классе поощряло бы процедурный код. - person Mishra; 16.06.2013
comment
@user2490334 user2490334 Даже в таких случаях, вероятно, предпочтительнее хранить тип в переменной-члене. Однако вы можете захотеть сделать переменную-член полиморфной. Хотя даже здесь, я думаю, было бы чище оставить тип простой переменной, а затем использовать какую-то полиморфную стратегию для различных специальных функций. - person James Kanze; 16.06.2013

Самый простой способ - иметь перечисление типа комнаты, как предлагает вам @billz. Проблема с этим способом заключается в том, что вы не должны забывать добавлять значение в перечисление и использовать его один раз каждый раз, когда вы добавляете новый тип комнаты в систему. Вы должны быть уверены, что используете значения перечисления только один раз, один раз для каждого класса.

Но, с другой стороны, проекты, основанные на наследовании, имеют смысл только в том случае, если типы иерархии имеют общее поведение. Другими словами, вы хотите использовать их одинаково, независимо от их типа. ИМПО, дизайн OO/наследования — не лучший способ сделать это.

Причудливый и масштабируемый способ, которым я делаю такие вещи, — это списки типов.

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

Для этого в системе есть три списка типов: один, содержащий типы данных, один, содержащий типы критериев поиска, и один, содержащий типы результатов поиска:

using system_data_types     = type_list<NormalRoom,LuxuryRoom>;
using search_criteria_types = type_list<NormalRoomsCriteria,LuxuryRoommsCriteria>;
using search_results_types  = type_list<NormalRoomSearchResults,LuxuryRoomSearchResults>;

Обратите внимание, что списки типов сортируются таким же образом. Это важно, как показано ниже.

Итак, реализация поисковой системы:

class SearchEngine
{
private:
    std::vector<VectorWrapper*> _data_lists; //A vector containing each system data type in its own vector. (One vector for NormalRoom, one for LuxuryRoom, etc)

    //This function returns the vector that stores the system data type passed.
    template<typename T>
    std::vector<T>& _get_vector() {...} //Implementation explained below.
public:
     SearchEngine() {...}//Explanation below.
    ~SearchEngine() {...}//Explanation below.

    //This function adds an instance of a system data type to the "database".
    template<typename T>
    void addData(const T& data) { _get_vector<T>().push_back( data ); }

    //The magic starts here:
    template<typename SEARCH_CRITERIA_TYPE>//This template parameter is deduced by the compiler through the function parameter, so you can ommit it.
    typename search_results_types::type_at<search_criteria_types::index_of<SEARCH_CRITERIA_TYPE>> //Return value (The search result that corresponds to the passed criteria. THIS IS THE REASON BECAUSE THE TYPELISTS MUST BE SORTED IN THE SAME ORDER.
    search( const SEARCH_CRITERIA_TYPE& criteria)
    {
        using system_data_type = system_data_types::type_at<search_criteria_types::index_of<SEARCH_CRITERIA_TYPE>>; //The type of the data to be searched.
        std::vector<system_data_type>& data = _get_vector<system_data_type>(); //A reference to the vector where that type of data is stored.

        //blah, blah, blah (Search along the vector using the criteria parameter....)
    }
};

А поисковик можно использовать следующим образом:

int main()
{
    SearchEngine engine;

    engine.addData(LuxuryRoom());
    engine.addData(NormalRoom());

    auto luxury_search_results = engine.search(LuxuryRoomCriteria()); //Search LuxuryRooms with the specific criteria and returns a LuxuryRoomSearchResults instance with the results of the search.
    auto normal_search_results = engine.search(NormalRoomCriteria()); //Search NormalRooms with the specific criteria and returns a NormalRoomSearchResults instance with the results of the search.
}

Движок основан на хранении одного вектора для каждого типа системных данных. И движок использует вектор, который хранит эти векторы. У нас не может быть полиморфной ссылки/указателя на векторы разных типов, поэтому мы используем оболочку std::vector:

struct VectorWrapper
{
    virtual ~VectorWrapper() = 0;
};

template<typename T>
struct GenericVectorWrapper : public VectorWrapper
{
    std::vector<T> vector;
    ~GenericVectorWrapper() {};
};

//This template class "builds" the search engine set (vector) of system data types vectors:
template<int type_index>
struct VectorBuilder
{
    static void append_data_type_vector(std::vector<VectorWrapper*>& data)
    {
        data.push_back( new GenericVectorWrapper< system_data_types::type_at<type_index> >() ); //Pushes back a vector that stores the indexth type of system data.

        VectorBuilder<type_index+1>::append_data_type_vector(data); //Recursive call
    }
};

//Base case (End of the list of system data types)
template<>
struct VectorBuilder<system_data_types::size>
{
    static void append_data_type_vector(std::vector<VectorWrapper*>& data) {}
};

Итак, реализация SearchEngine::_get_vector<T> выглядит следующим образом:

template<typename T>
std::vector<T>& get_vector()
{
    GenericVectorWrapper<T>* data; //Pointer to the corresponing vector
    data = dynamic_cast<GenericVectorWrapper<T>*>(_data_lists[system_data_types::index_of<T>]); //We try a cast from pointer of wrapper-base-class to the expected type of vector wrapper

    if( data )//If cast success, return a reference to the std::vector<T>
        return data->vector;
    else
        throw; //Cast only fails if T is not a system data type. Note that if T is not a system data type, the cast result in a buffer overflow (index_of<T> returns -1)
}

Конструктор SearchEngine использует VectorBuilder только для построения списка векторов:

SearchEngine()
{
    VectorBuilder<0>::append_data_type_vector(_data_list);
}

И деструктор только перебирает список, удаляя векторы:

~SearchEngine()
{
    for(unsigned int i = 0 ; i < system_data_types::size ; ++i)
        delete _data_list[i];
}

Преимуществами данной конструкции являются:

  • Поисковая система использует один и тот же интерфейс для разных поисков (поиск с разными типами системных данных в качестве цели). И процесс «связывания» типа данных с соответствующими критериями поиска и результатами выполняется во время компиляции.

  • Этот интерфейс типобезопасен: вызов SearchEngine::search() возвращает тип результатов, основанный только на переданных критериях поиска. Ошибки результатов назначения обнаруживаются во время компиляции. Например: NormalRoomResults = engine.search(LuxuryRoomCriteria()) вызывает ошибку компиляции (engine.search<LuxuryRoomCriteria> возвращает LuxuryRoomResults).

  • Поисковая система является полностью масштабируемой: чтобы добавить новый тип данных в систему, вам нужно только перейти к добавлению типов в списки типов. Реализация поисковой системы не меняется.

person Manu343726    schedule 16.06.2013

Класс номера

class Room{
    public:
        enum Type {
            Regular,
            Luxury,
            Celebrity
        };

        Room(Type rt):roomType(rt), isOpen(true) { }

        Type getRoomType() { return roomType; }

        bool getRoomStatus() { return isOpen; }

        void setRoomStatus(bool isOpen) { this->isOpen = isOpen; }

    private:
        Type roomType;
        bool isOpen;
    };

Класс отеля

class Hotel{

    std::map<Room::Type, std::vector<Room*>> openRooms;
    //std::map<Room::Type, std::vector<Room*>> reservedRooms;

public:

    void addRooms(Room &room) { openRooms[room.getRoomType()].push_back(&room); }

    auto getOpenRooms() {
        std::vector<Room*> allOpenRooms;
        for(auto rt : openRooms)
            for(auto  r : rt.second)
                    allOpenRooms.push_back(r);
        return allOpenRooms;
    }

    auto getOpenRoomsOfType(Room::Type rt) {
        std::vector<Room*> OpenRooms;
        for(auto r : openRooms[rt])
            OpenRooms.push_back(r);
        return OpenRooms;
    }

    int totalOpenRooms() {
        int roomCount=0;
        for(auto rt : openRooms)
            roomCount += rt.second.size();
        return roomCount;
    }

};

Пример использования клиента:

Hotel Marigold;
Room RegularRoom1(Room::Regular);
Room RegularRoom2(Room::Regular);
Room LuxuryRoom(Room::Luxury);

Marigold.addRooms(RegularRoom1);
Marigold.addRooms(RegularRoom2);
Marigold.addRooms(LuxuryRoom);

auto allRooms = Marigold.getOpenRooms();
auto LRooms = Marigold.getOpenRoomsOfType(Room::Luxury);
auto RRooms = Marigold.getOpenRoomsOfType(Room::Regular);
auto CRooms = Marigold.getOpenRoomsOfType(Room::Celebrity);

cout << " TotalOpenRooms : " << allRooms.size()
                            << "\n Luxury : " << LRooms.size()
                            << "\n Regular : " << RRooms.size()
                            << "\n Celebrity : " << CRooms.size()
                            << endl;

Всего OpenRooms : 4
Люкс : 2
Обычные : 2
Знаменитости : 0

person Betran Jacob    schedule 20.03.2017