The Software Purist |

CAT | Beginner

Dec/09

10

Inheritance vs. Composition

If you’re familiar with Object-Oriented programming, and potentially, UML, these terms may be familiar to you. If not, I will present them below.

Inheritance is a relationship between two classes, where there is a parent-child relationship, in the sense that the parent appears higher on the tree than the children and a parent may have multiple children. However, the relationship follows than the children inherit all the parent’s state and behavior and may also additionally add their own. I’ll demonstrate an example with UML, done with SmartDraw, and some pseudo-code:

class Shape
{
public:
	virtual void draw() = 0;
};

class Circle : public Shape
{
public:
	Circle(const Pos2f& p_pos, float p_radius) :
		pos_(p_pos), radius_(p_radius)
	{
	}

	virtual void draw()
	{
		// perform draw operation based on position and radius
	}

private:
	Pos2f pos_;
	float radius_;
};

class Square : public Shape
{
public:
	Square(const Pos2f& p_topLeftPos, const Dim2f& p_dimensions) :
		topLeftPos_(p_topLeftPos), dimensions_(p_dimensions)
	{
	}

	virtual void draw()
	{
		// perform draw operation based on top-left position, width and height
	}

private:
	Pos2f topLeftPos_;
	Dim2f dimensions_;
};

This relationship makes sense as a class hierarchy because there’s a simple guideline to determine whether a class should be inherited from another. If you can justify that a class “is-a” more specific type of another type, then semantically, it makes sense to inherit, if not it doesn’t. Applying it to this case, a Circle is-a type of Shape. A Square is-a type of Shape. So, therefore, inheritance is appropriate.

This brings up the question, what do we do when inheritance isn’t appropriate? There is also object composition. Object composition basically means that a type or class is “composed” of other types or classes. If you make use of object composition it looks more like this:

Here’s the UML diagram, done with SmartDraw:

class Duck
{
public:
	void quack()
	{
		// logic to quack
	}

	void fly()
	{
		// logic to fly
	}

private:
	Bill duckBill_;
	std::vector<Feather> feathers_;
};

class Bill
{
...
};

class Feather
{
...
};

class Pond
{
...

private:
   Duck* pDuck_;
};

This example demonstrates what it might look like for code that involves a Duck. A duck has-a bill, and has-some feathers. When a relationship between two classes is a has-a relationship, then you should use composition. There are two types of composition, which are demonstrated in the code and diagram above. The more general form of composition implies full ownership, including responsibility for an object’s lifetime. This is true for the bill of a duck and its feathers, for without a instance of a duck object, neither the feathers nor bill exist. (Side note: This is assuming we can’t pluck the feathers of the duck or remove its bill, which would get into a concept called Transfer of Ownership, but that is beyond the scope of this article.) The second type of composition is called aggregation, which implies that while an object has another object, it doesn’t own it. In the example I showed above, the Pond has this sort of relationship with the Duck. An instance of a Pond object may keep a reference (or pointer) to the Duck object while the Duck is in the Pond, but isn’t responsible for managing the lifetime of the Duck object, so it is a aggregation relationship.

Now that I’ve clarified inheritance vs. composition a bit, it brings up some additional questions. In the case of the Duck, a common design problem I’ve seen before is that in the case of a one-to-one relationship, like there is between the Duck and the Bill, you could theoretically derive Duck from the Bill class. The argument I’ve heard is, “This violates the is-a rule, but really what is the big deal? I was going to promote the methods of Bill to be public on Duck anyway? This saves typing.” The problem is semantic understanding. This type of inheritance is generally meant to be a polymorphic relationship. Polymorphism requires a is-a relationship to make sense. Now, there are cases where it makes sense to inherit when there isn’t a is-a relationship, but generally you should avoid it, when it’s obviously wrong. A codebase that violates is-a/has-a rules is much more difficult to understand than one that follows it. Ultimately, it just flows more naturally, and you wind up avoiding some dead ends that code that violates this runs into.

In a future article I will discuss some of the more grey area cases. Hopefully, you found this to be a good introduction.

, , , , , , , , ,

Find it!