Please have a look at the two simplified examples of designing a class aggregation below.
Solution 1
Header
// need include, forward declaration is not enough
#include "door.h"
class CGarage
{
public:
CGarage(const std::string &val);
private:
CDoor m_door;
};
Source
#include "garage.h"
CGarage::CGarage(const std::string &val)
:m_door(val)
{
}
Solution 2
Header
#include "smart_ptr.hpp"
// forward declaration
class CDoor;
class CGarage
{
public:
CGarage(const std::string &val);
private:
scoped_ptr<CDoor> m_door;
};
Source
#include "garage.h"
#include "door.h"
CGarage::CGarage(const std::string &val)
:m_door(new CDoor(val))
{
}
Questions concerning the creation of the CDoor member
What advantages/disadvantages do you see in the design of the examples (dynamic allocation of CDoor vs automatic allocation)?
This is what I came up with:
Solution 1:
+ no issues with memory handling or lifetime
+ no need for expensive memory allocation at runtime
- need additional include in header (compilation speed slower?, closer coupling to CDoor) -> many includes in header files are considered bad...
Solution 2:
+ loose coupling with CDoor in header (only forward declaration needed)
- memory needs to be handled by programmer
Which design do you usually prefer for what reason?
It is rare that we get question design (I mean, interesting ones).
Let's forget for a moment the (obviously) contrived example and concentrate on the notion.
We have 2 solutions:
I'll voluntarily discard all "performances" argument for the moment. Performance doesn't matter 97% of the time (says Knuth) so unless we measure a noticeable difference, since the functionality is identical, we thus need not worry about it at the moment.
We therefore have two orthogonal concepts attempting to sway our decision:
Some answers here have rightly spoken about polymorphism, but the exact implementation of Door
is a detail that is Door
's concern, not Garage
's. If Door
wishes to offer several implementations, that's fine, as long as its clients need not be concerned by this detail.
I am quite a fanboy, myself, of the KISS and YAGNI principles. So I would argue in favor of Hard containment... with one caveat.
When designing an interface that will be exposed, an interface therefore that stands at the frontier of the library, then this interface should expose a minimum of dependencies and internals. Ideally, this should be a Facade
or a Proxy
, an object whose only purpose is to hide the internals of the library, and this object should have minimal dependencies in its header and have maximal layout compatibility, which means:
For all internal classes, simplicity wins hands off.
Solution 1 is superior at both run and compile-time in every conceivable case, unless you're having extreme issues with include dependencies and must act to reduce them. Solution 2 has more issues than you've mentioned - you'll need to write and maintain additional copy constructor/assignment operator, just to begin with.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With