How to maintain encapsulation with composition in C++?
- by iFreilicht
I am designing a class Master that is composed from multiple other classes, A, Base, C and D. These four classes have absolutely no use outside of Master and are meant to split up its functionality into manageable and logically divided packages. They also provide extensible functionality as in the case of Base, which can be inherited from by clients.
But, how do I maintain encapsulation of Master with this design?
So far, I've got two approaches, which are both far from perfect:
1. Replicate all accessors:
Just write accessor-methods for all accessor-methods of all classes that Master is composed of.
This leads to perfect encapsulation, because no implementation detail of Master is visible, but is extremely tedious and makes the class definition monstrous, which is exactly what the composition should prevent. Also, adding functionality to one of the composees (is that even a word?) would require to re-write all those methods in Master. An additional problem is that inheritors of Base could only alter, but not add functionality.
2. Use non-assignable, non-copyable member-accessors:
Having a class accessor<T> that can not be copied, moved or assigned to, but overrides the operator-> to access an underlying shared_ptr, so that calls like
Master->A()->niceFunction();
are made possible. My problem with this is that it kind of breaks encapsulation as I would now be unable to change my implementation of Master to use a different class for the functionality of niceFunction(). Still, it is the closest I've gotten without using the ugly first approach. It also fixes the inheritance issue quite nicely. A small side question would be if such a class already existed in std or boost.
EDIT: Wall of code
I will now post the code of the header files of the classes discussed. It may be a bit hard to understand, but I'll give my best in explaining all of it.
1. GameTree.h
The foundation of it all. This basically is a doubly-linked tree, holding GameObject-instances, which we'll later get to. It also has it's own custom iterator GTIterator, but I left that out for brevity. WResult is an enum with the values SUCCESS and FAILED, but it's not really important.
class GameTree
{
public:
//Static methods for the root. Only one root is allowed to exist at a time!
static void ConstructRoot(seed_type seed, unsigned int depth);
inline static bool rootExists(){ return static_cast<bool>(rootObject_); }
inline static weak_ptr<GameTree> root(){ return rootObject_; }
//delta is in ms, this is used for velocity, collision and such
void tick(unsigned int delta);
//Interaction with the tree
inline weak_ptr<GameTree> parent() const { return parent_; }
inline unsigned int numChildren() const{ return static_cast<unsigned int>(children_.size()); }
weak_ptr<GameTree> getChild(unsigned int index) const;
template<typename GOType>
weak_ptr<GameTree> addChild(seed_type seed, unsigned int depth = 9001){
GOType object{ new GOType(seed) };
return addChildObject(unique_ptr<GameTree>(new GameTree(std::move(object), depth)));
}
WResult moveTo(weak_ptr<GameTree> newParent);
WResult erase();
//Iterators for for( : ) loop
GTIterator& begin(){
return *(beginIter_ = std::move(make_unique<GTIterator>(children_.begin())));
}
GTIterator& end(){
return *(endIter_ = std::move(make_unique<GTIterator>(children_.end())));
}
//unloading should be used when objects are far away
WResult unloadChildren(unsigned int newDepth = 0);
WResult loadChildren(unsigned int newDepth = 1);
inline const RenderObject& renderObject() const{ return gameObject_->renderObject(); }
//Getter for the underlying GameObject (I have not tested the template version)
weak_ptr<GameObject> gameObject(){
return gameObject_;
}
template<typename GOType>
weak_ptr<GOType> gameObject(){
return dynamic_cast<weak_ptr<GOType>>(gameObject_);
}
weak_ptr<PhysicsObject> physicsObject() {
return gameObject_->physicsObject();
}
private:
GameTree(const GameTree&); //copying is only allowed internally
GameTree(shared_ptr<GameObject> object, unsigned int depth = 9001);
//pointer to root
static shared_ptr<GameTree> rootObject_;
//internal management of a child
weak_ptr<GameTree> addChildObject(shared_ptr<GameTree>);
WResult removeChild(unsigned int index);
//private members
shared_ptr<GameObject> gameObject_;
shared_ptr<GTIterator> beginIter_;
shared_ptr<GTIterator> endIter_;
//tree stuff
vector<shared_ptr<GameTree>> children_;
weak_ptr<GameTree> parent_;
unsigned int selfIndex_; //used for deletion, this isn't necessary
void initChildren(unsigned int depth); //constructs children
};
2. GameObject.h
This is a bit hard to grasp, but GameObject basically works like this:
When constructing a GameObject, you construct its basic attributes and a CResult-instance, which contains a vector<unique_ptr<Construction>>. The Construction-struct contains all information that is needed to construct a GameObject, which is a seed and a function-object that is applied at construction by a factory. This enables dynamic loading and unloading of GameObjects as done by GameTree. It also means that you have to define that factory if you inherit GameObject. This inheritance is also the reason why GameTree has a template-function gameObject<GOType>.
GameObject can contain a RenderObject and a PhysicsObject, which we'll later get to.
Anyway, here's the code.
class GameObject;
typedef unsigned long seed_type;
//this declaration magic means that all GameObjectFactorys inherit from GameObjectFactory<GameObject>
template<typename GOType>
struct GameObjectFactory;
template<>
struct GameObjectFactory<GameObject>{
virtual unique_ptr<GameObject> construct(seed_type seed) const = 0;
};
template<typename GOType>
struct GameObjectFactory : GameObjectFactory<GameObject>{
GameObjectFactory() : GameObjectFactory<GameObject>(){}
unique_ptr<GameObject> construct(seed_type seed) const{
return unique_ptr<GOType>(new GOType(seed));
}
};
//same as with the factories. this is important for storing them in vectors
template<typename GOType>
struct Construction;
template<>
struct Construction<GameObject>{
virtual unique_ptr<GameObject> construct() const = 0;
};
template<typename GOType>
struct Construction : Construction<GameObject>{
Construction(seed_type seed, function<void(GOType*)> func = [](GOType* null){}) :
Construction<GameObject>(),
seed_(seed),
func_(func)
{}
unique_ptr<GameObject> construct() const{
unique_ptr<GameObject> gameObject{ GOType::factory.construct(seed_) };
func_(dynamic_cast<GOType*>(gameObject.get()));
return std::move(gameObject);
}
seed_type seed_;
function<void(GOType*)> func_;
};
typedef struct CResult
{
CResult() :
constructions{}
{}
CResult(CResult && o) :
constructions(std::move(o.constructions))
{}
CResult& operator= (CResult& other){
if (this != &other){
for (unique_ptr<Construction<GameObject>>& child : other.constructions){
constructions.push_back(std::move(child));
}
}
return *this;
}
template<typename GOType>
void push_back(seed_type seed, function<void(GOType*)> func = [](GOType* null){}){
constructions.push_back(make_unique<Construction<GOType>>(seed, func));
}
vector<unique_ptr<Construction<GameObject>>> constructions;
} CResult;
//finally, the GameObject
class GameObject
{
public:
GameObject(seed_type seed);
GameObject(const GameObject&);
virtual void tick(unsigned int delta);
inline Matrix4f trafoMatrix(){ return physicsObject_->transformationMatrix(); }
//getter
inline seed_type seed() const{ return seed_; }
inline CResult& properties(){ return properties_; }
inline const RenderObject& renderObject() const{ return *renderObject_; }
inline weak_ptr<PhysicsObject> physicsObject() { return physicsObject_; }
protected:
virtual CResult construct_(seed_type seed) = 0;
CResult properties_;
shared_ptr<RenderObject> renderObject_;
shared_ptr<PhysicsObject> physicsObject_;
seed_type seed_;
};
3. PhysicsObject
That's a bit easier. It is responsible for position, velocity and acceleration. It will also handle collisions in the future. It contains three Transformation objects, two of which are optional. I'm not going to include the accessors on the PhysicsObject class because I tried my first approach on it and it's just pure madness (way over 30 functions). Also missing: the named constructors that construct PhysicsObjects with different behaviour.
class Transformation{
Vector3f translation_;
Vector3f rotation_;
Vector3f scaling_;
public:
Transformation() :
translation_{ 0, 0, 0 },
rotation_{ 0, 0, 0 },
scaling_{ 1, 1, 1 }
{};
Transformation(Vector3f translation, Vector3f rotation, Vector3f scaling);
inline Vector3f translation(){ return translation_; }
inline void translation(float x, float y, float z){ translation(Vector3f(x, y, z)); }
inline void translation(Vector3f newTranslation){
translation_ = newTranslation;
}
inline void translate(float x, float y, float z){ translate(Vector3f(x, y, z)); }
inline void translate(Vector3f summand){
translation_ += summand;
}
inline Vector3f rotation(){ return rotation_; }
inline void rotation(float pitch, float yaw, float roll){ rotation(Vector3f(pitch, yaw, roll)); }
inline void rotation(Vector3f newRotation){
rotation_ = newRotation;
}
inline void rotate(float pitch, float yaw, float roll){ rotate(Vector3f(pitch, yaw, roll)); }
inline void rotate(Vector3f summand){
rotation_ += summand;
}
inline Vector3f scaling(){ return scaling_; }
inline void scaling(float x, float y, float z){ scaling(Vector3f(x, y, z)); }
inline void scaling(Vector3f newScaling){
scaling_ = newScaling;
}
inline void scale(float x, float y, float z){ scale(Vector3f(x, y, z)); }
void scale(Vector3f factor){
scaling_(0) *= factor(0);
scaling_(1) *= factor(1);
scaling_(2) *= factor(2);
}
Matrix4f matrix(){
return WMatrix::Translation(translation_) * WMatrix::Rotation(rotation_) * WMatrix::Scale(scaling_);
}
};
class PhysicsObject;
typedef void tickFunction(PhysicsObject& self, unsigned int delta);
class PhysicsObject{
PhysicsObject(const Transformation& trafo) :
transformation_(trafo),
transformationVelocity_(nullptr),
transformationAcceleration_(nullptr),
tick_(nullptr)
{}
PhysicsObject(PhysicsObject&& other) :
transformation_(other.transformation_),
transformationVelocity_(std::move(other.transformationVelocity_)),
transformationAcceleration_(std::move(other.transformationAcceleration_)),
tick_(other.tick_)
{}
Transformation transformation_;
unique_ptr<Transformation> transformationVelocity_;
unique_ptr<Transformation> transformationAcceleration_;
tickFunction* tick_;
public:
void tick(unsigned int delta){ tick_ ? tick_(*this, delta) : 0; }
inline Matrix4f transformationMatrix(){ return transformation_.matrix(); }
}
4. RenderObject
RenderObject is a base class for different types of things that could be rendered, i.e. Meshes, Light Sources or Sprites. DISCLAIMER: I did not write this code, I'm working on this project with someone else.
class RenderObject
{
public:
RenderObject(float renderDistance);
virtual ~RenderObject();
float renderDistance() const { return renderDistance_; }
void setRenderDistance(float rD) { renderDistance_ = rD; }
protected:
float renderDistance_;
};
struct NullRenderObject : public RenderObject{
NullRenderObject() : RenderObject(0.f){};
};
class Light : public RenderObject{
public:
Light() : RenderObject(30.f){};
};
class Mesh : public RenderObject{
public:
Mesh(unsigned int seed) :
RenderObject(20.f)
{
meshID_ = 0;
textureID_ = 0;
if (seed == 1)
meshID_ = Model::getMeshID("EM-208_heavy");
else
meshID_ = Model::getMeshID("cube");
};
unsigned int getMeshID() const { return meshID_; }
unsigned int getTextureID() const { return textureID_; }
private:
unsigned int meshID_;
unsigned int textureID_;
};
I guess this shows my issue quite nicely: You see a few accessors in GameObject which return weak_ptrs to access members of members, but that is not really what I want.
Also please keep in mind that this is NOT, by any means, finished or production code! It is merely a prototype and there may be inconsistencies, unnecessary public parts of classes and such.