سؤال

I am practicing object oriented design for an upcoming interview. My question is about the design for a hotel reservation system: - The system should be able to return an open room of a specific type or return all the open rooms in the hotel. - There are many types of rooms in hotel like regular, luxury, celebrity and so on.

So far I have come up with following classes:

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
...
}

}

I am confused about the implementation of hotel.search() method where I am checking the type (which I believe should be handled by polymorphism in some way). Is there a better way of designing this system so that the search and allOpenRooms methods can be implemented without explicitly checking the type of the objects?

هل كانت مفيدة؟

المحلول 2

You could always let a room carry it's real type, instead of comparing the object type:

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
      }
    }
};

نصائح أخرى

Going through the sub-class objects asking what type they are isn't really a good illustration of o-o design. You really need something you want to do to all rooms without being aware of what type each one is. For example print out the daily room menu for the room (which might be different for different types). Deliberately looking for the sub-class object's type, while not being wrong, is not great o-o style. If you just want to do that, as the other respondents have said, just have "rooms" with a set of properties.

Do the different types of rooms have different behavior? From the description you give, this is not a case where inheritance should be used. Each room simply has an attribute, type, which is, in its simplest form, simply an enum.

The simplest way is to have a Room type enumeration as @billz suggest you. The problem with tis way is that you must not forget to add a value to the enumeration and use it once every time you add a new type of Room to the system. You have to be sure you use the enum values only once, one time per class.

But, on the other hand, inheritance bassed dessigns only have sense if the types of the hierarchy shares a common behaviour. In other words, you want to use them in the same way, regardless of its type. IMPO, an OO/inheritance dessign is not the better way to do this.

The freak and scalable way I do this type of things is through typelists.

Normally, you have different search criteria for every type in your system. And, in many cases, the results of this search are not the same for different types of your system (Is not the ssame to search a luxury room and to search a normal room, you could have different search criteria and/or want different search results data).

For this prupose, the system has three typelists: One containing the data types, one containing the search criteria types, and one containing the search results types:

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

Note that type_lists are sorted in the same manner. This is important, as I show below.

So the implementation of the search engine is:

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....)
    }
};

And the search engine can be used as follows:

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.
}

The engine is based on store one vector for every system data type. And the engine uses a vector that stores that vectors. We cannot have a polymorphic reference/pointer to vectors of different types, so we use a wrapper of a 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) {}
};

So the implementation of SearchEngine::_get_vector<T> is as follows:

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)
}

The constructor of SearchEngine only uses VectorBuilder to build the list of vectors:

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

And the destructor only iterates over the list deleting the vectors:

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

The advantages of this dessign are:

  • The search engine uses exactly the same interface for different searches (Searches with different system data types as target). And the process of "linking" a data type to its corresponding search criteria and results is done at compile-time.

  • That interface is type safe: A call to SearchEngine::search() returns a type of results based only on the search criteria passed. Assignament results errors are detected at compile-time. For example: NormalRoomResults = engine.search(LuxuryRoomCriteria()) generates a compilation error (engine.search<LuxuryRoomCriteria> returns LuxuryRoomResults).

  • The search engine is fully scalable: To add a new datatype to the system, you must only go to add the types to the typelists. The implementation of the search engine not changes.

Room Class

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;
    };

Hotel Class

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;
    }

};

Client UseCase:

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;

TotalOpenRooms : 4
Luxury : 2
Regular : 2
Celebrity : 0

If you really want to check that a room is of the same type as some other room, then typeid() is as good as any other method - and it's certainly "better" (from a performance perspective, at least) to calling a virtual method.

The other option is to not have separate classes at all, and store the roomtype as a member variable (and that is certainly how I would design it, but that's not a very good design for learning object orientation and inheritance - you don't get to inherit when the base class fulfils all your needs).

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top