This question deals with class design and coherent interfaces (I guess).
Say you have a small class to represent the "Geometry" of a road... It could contain many properties and methods like this...
class RoadMap
{
private:
struct RoadPiece
{
float x1, y1, x2, y2;
};
std::string name;
float area_width;
float area_height;
std::vector<RoadPiece> pieces;
public:
const std::string& get_name() const {return name;}
float get_width() const {return area_width;}
float get_height() const {return area_height;}
float get_area() const {return area_width * area_height;}
void set_width(float v) {area_width=v;}
void set_height(float v) {area_height=v;}
void set_name(const std::string v) {name=v;}
void add_road_piece(float x1, float y1, float x2, float y2)
{
//...
}
}
As you can see, we're mixing const and non const methods. No big deal: we can pretty much write client code like this
RoadMap m;
m.set_width(100.0);
m.set_height(150.0);
m.set_name("Northern Hills");
//Tedious code here...
std::cout<<"The area of the map is "<<m.get_area()<<std::endl;
Now, let's figure we want to add "another layer" of information to the map that does not neccesarily belong with the map, rather... complements it in client code... Say, traffic signs
class TrafficSignsMap
{
private:
struct Sign
{
enum class types {STOP, YIELD, STEP_ON_IT};
types type;
float x;
float y;
}
std::vector<Sign> signs;
public:
void add_stop_sign(float x, float y) {/*Blah blah*/}
void add_yield_sign(float x, float y) {/*Blah blah*/}
void add_step_on_it_sign(float x, float y) {/*Blah blah*/}
const std::vector<Sign>& get_all_signs() {return signs;}
const std::vector<const Sign const *> get_signs_in_area(float x1, float y1, float x2, float y2)
{
//Do some calculations, populate a vector with pointers to signs, return it...
}
}
Again, we can write all sorts of client code and mix roads and signs up. At this point, please, notice I am not really doing this application, just took it as an example...
Anyway, after writing some more code I come with a third layer of data... This time it is "PlacesToEat". I won't describe them here, but you get the drift: it exists on its own but can share a certain space with roads or signs (more like roads, but well...). With this third and final layer we find a place where we can get files with the info for roads, signs and places to eat. We think that we could write one class that we would feed the file and it would store the info for us. Like this:
class MapData
{
private:
RoadMap roads;
TrafficSignsMap signs;
PlacesToEatMap places_to_eat;
public:
MapData(const std::string& filename)
{
std::ifstream(filename);
//Read the file... populate our properties...
}
const RoadMap& get_road_map() const {return roads;}
const TrafficSignsMap& get_signs_map() const {return signs;}
const PlacesToEatMap& get_places_to_eat_map() const {return places_to_eat;}
};
And here's the thing... Once all data is grouped inside a big container we should provide both const and non const access, right?. I would like to get all const data but I should also be able to add new places to eat, something I shouldn't be able to do with the current interface.
Now I know I can use the MapData class as a proxy (increasing its responsability in the application) so I would go:
MapData MD;
MD.add_stop_sign(10.0, 20.0); //This, in time, proxies to the inner property.
Or I could add const and non const getters (increasing my headache) like so:
MapData MD;
float area=MD.get_road_map().get_area();
MD.get_non_const_road_map().add_road(/*blah blah*/);
Or I could just screw it and make these properties public:
public:
RoadMap roads;
TrafficSignsMap signs;
PlacesToEatMap places_to_eat;
Or I could just make the getters non const since I am meant to modify the data and be done with it (there's not really a disadvantage here... I guess, say I get a const MapData object, I shouldn't be able to change it anyway):
RoadMap& get_road_map() {return roads;}
TrafficSignsMap& get_signs_map() {return signs;}
PlacesToEatMap& get_places_to_eat_map() {return places_to_eat;}
Again, please, note that the scenario has been made up as this question was redacted (why else would the road map store dimensions??). Considering this, how would you go with this kind of situation?. I am looking for a way that I can have the MapData class as extensible as possible in case I want more layers added (that should discard the proxy option) and also one that is as correct as possible. Thanks a lot.
There are of course many way to do this. But from a design perspective, it's important to remain consistent (see here: "consistency coincides with conceptual integrity").
Your approach for the three container classes RoadMap
, TrafficSignsMap
and PlacesToEatMap
all share the following principle:
If you want to be consistent, you should then adopt the same approach for MapData
: use the proxy approach (your first alternative).
Personally (but here we are leaving the objective facts and enter in subjective opinions) I think that this design does not take advantage of object oriented design. I don't say it's bad: there could be good reasons for doing so. But not optimal. Why ? The user of your classes can't manipulate the application objects your class is designed around: he doesn't work with road segments, but only with segment coordinates. For example: if later you'd decide that float
is not precise enough and double
should be use instead every piece of code would have to be reviewed. If you'd realise that there might be tunels and your coordinates need to be 3D it would be a real maintenance catastrophee