Object oriented design has many principles. Stanislav Kozlovski demonstrates good design through a role playing game.
Most modern programming languages support and encourage object-oriented programming (OOP). Even though lately we seem to be seeing a slight shift away from this, as people start using languages which are not heavily influenced by OOP (such as Go, Rust, Elixir, Elm, Scala), most still have objects. The design principles we are going to outline here apply to non-OOP languages as well.
To succeed in writing clear, high-quality, maintainable and extendable code you will need to know about design principles that have proven themselves effective over decades of experience.
Disclosure: The example we are going to be going through will be in Python. Examples are there to prove a point and may be sloppy in other, obvious, ways.
Object types
Since we are going to be modelling our code around objects, it would be useful to differentiate between their different responsibilities and variations.
There are three type of objects:
1. Entity object
This object generally corresponds to some real-world entity in the problem space. Say we’re building a role-playing game (RPG), an entity object would be our simple
Hero
class (Listing 1).
class Hero: def __init__(self, health, mana): self._health = health self._mana = mana def attack(self) -> int: """ Returns the attack damage of the Hero """ return 1 def take_damage(self, damage: int): self._health -= damage def is_alive(self): return self._health > 0 |
Listing 1 |
These objects generally contain properties about themselves (such as
health
or
mana
) and are modifiable through certain rules.
2. Control object
Control objects (sometimes also called
Manager objects
) are responsible for the coordination of other objects. These are objects that
control
and make use of other objects. A great example in our RPG analogy would be the
Fight
class, which controls two heroes and makes them fight (Listing 2).
class Fight: class FightOver(Exception): def __init__(self, winner, *args, **kwargs): self.winner = winner super(*args, **kwargs) def __init__(self, hero_a: Hero, hero_b: Hero): self._hero_a = hero_a self._hero_b = hero_b self.fight_ongoing = True self.winner = None def fight(self): while self.fight_ongoing: self._run_round() print( 'The fight has ended! Winner is #{}'.\ format(self.winner)) def _run_round(self): try: self._run_attack(self._hero_a, self._hero_b) self._run_attack(self._hero_b, self._hero_a) except self.FightOver as e: self._finish_round(e.winner) def _run_attack(self, attacker: Hero, victim: Hero): damage = attacker.attack() victim.take_damage(damage) if not victim.is_alive(): raise self.FightOver(winner=attacker) def _finish_round(self, winner: Hero): self.winner = winner self.fight_ongoing = False |
Listing 2 |
Encapsulating the logic for a fight in such a class provides you with multiple benefits: one of which is the easy extensibility of the action. You can very easily pass in a non-player character (NPC) type for the hero to fight, provided it exposes the same API. You can also very easily inherit the class and override some of the functionality to meet your needs.
3. Boundary object
These are objects which sit at the boundary of your system. Any object which takes input from or produces output to another system – regardless if that system is a User, the internet or a database – can be classified as a boundary object (Listing 3).
class UserInput: def __init__(self, input_parser): self.input_parser = input_parser def take_command(self): """ Takes the user's input, parses it into a recognizable command and returns it """ command = self._parse_input( self._take_input()) return command def _parse_input(self, input): return self.input_parser.parse(input) def _take_input(self): raise NotImplementedError() class UserMouseInput(UserInput): pass class UserKeyboardInput(UserInput): pass class UserJoystickInput(UserInput): pass |
Listing 3 |
These boundary objects are responsible for translating information into and out of our system. In an example where we take User commands, we would need the boundary object to translate a keyboard input (like a spacebar) into a recognizable domain event (such as a character jump).
Bonus: Value object
Value objects [ Wikipedia-1 ] represent a simple value in your domain. They are immutable and have no identity.
If we were to incorporate them into our game, a
Money
or
Damage
class would be a great fit. Said objects let us easily distinguish, find and debug related functionality, while the naive approach of using a primitive type – an array of integers or one integer – does not (Listing 4).
class Money: def __init__(self, gold, silver, copper): self.gold = gold self.silver = silver self.copper = copper def __eq__(self, other): return self.gold == other.gold and \ self.silver == other.silver and \ self.copper == other.copper def __gt__(self, other): if self.gold == other.gold and \ self.silver == other.silver: return self.copper > other.copper if self.gold == other.gold: return self.silver > other.silver return self.gold > other.gold def __add__(self, other): return Money( gold=self.gold + other.gold, silver=self.silver + other.silver, copper=self.copper + other.copper) def __str__(self): return 'Money Object(' + \ 'Gold: {}; Silver: {}; Copper: {})'.\ format(self.gold, self.silver, self.copper) def __repr__(self): return self.__str__() print(Money(1, 1, 1) == Money(1, 1, 1)) # => True print(Money(1, 1, 1) > Money(1, 2, 1)) # => False print(Money(1, 1, 0) + Money(1, 1, 1)) # => Money Object(Gold: 2; Silver: 2; Copper: 1) |
Listing 4 |
They can be classified as a subcategory of
Entity
objects.
Key design principles
Design principles are rules in software design that have proven themselves valuable over the years. Following them strictly will help you ensure your software is of top-notch quality.
Abstraction
Abstraction is the idea of simplifying a concept to its bare essentials in some context. It allows you to better understand the concept by stripping it down to a simplified version.
The examples above illustrate abstraction – look at how the
Fight
class is structured. The way you use it is as simple as possible – you give it two heroes as arguments in instantiation and call the
fight()
method. Nothing more, nothing less.
Abstraction in your code should follow the rule of least surprise [ Wikipedia-2 ]. Your abstraction should not surprise anybody with needless and unrelated behavior/properties. In other words – it should be intuitive.
Note that our
Hero#take_damage()
function does not do something unexpected, like delete our character upon death. But we can expect it to kill our character if his health goes below zero.
Encapsulation
Encapsulation can be thought of as putting something inside a capsule – you limit its exposure to the outside world. In software, restricting access to inner objects and properties helps with data integrity.
Encapsulation black-boxes inner logic and makes your classes easier to manage, because you know what part is used by other systems and what isn’t. This means that you can easily rework the inner logic while retaining the public parts and be sure that you have not broken anything. As a side-effect, working with the encapsulated functionality from the outside becomes simpler as you have less things to think about.
In most languages, this is done through the so-called access modifiers (private, protected, and so on) [
Wikipedia-3
]. Python is not the best example of this, as it lacks such explicit modifiers built into the runtime, but we use conventions to work around this. The
_
prefix to the variables/methods denote them as being private.
For example, imagine we change our
Fight#_run_attack
method to return a boolean variable that indicates if the fight is over rather than raise an exception. We will know that the only code we might have broken is inside the
Fight
class, because we made the method private.
Remember, code is more frequently changed than written anew. Being able to change your code with as clear and little repercussions as possible is flexibility you want as a developer.
Decomposition
Decomposition is the action of splitting an object into multiple separate smaller parts. Said parts are easier to understand, maintain and program.
Imagine we wanted to incorporate more RPG features like buffs, inventory, equipment and character attributes on top of our
Hero
(see Listing 5).
class Hero: def __init__(self, health, mana): self._health = health self._mana = mana self._strength = 0 self._agility = 0 self._stamina = 0 self.level = 0 self._items = {} self._equipment = {} self._item_capacity = 30 self.stamina_buff = None self.agility_buff = None self.strength_buff = None self.buff_duration = -1 def level_up(self): self.level += 1 self._stamina += 1 self._agility += 1 self._strength += 1 self._health += 5 def take_buff(self, stamina_increase, strength_increase, agility_increase): self.stamina_buff = stamina_increase self.agility_buff = agility_increase self.strength_buff = strength_increase self._stamina += stamina_increase self._strength += strength_increase self._agility += agility_increase self.buff_duration = 10 # rounds def pass_round(self): if self.buff_duration > 0: self.buff_duration -= 1 if self.buff_duration == 0: # Remove buff self._stamina -= self.stamina_buff self._strength -= self.strength_buff self._agility -= self.agility_buff self._health -= self.stamina_buff * 5 self.buff_duration = -1 self.stamina_buff = None self.agility_buff = None self.strength_buff = None def attack(self) -> int: """ Returns the attack damage of the Hero """ return 1 + (self._agility * 0.2) + ( self._strength * 0.2) def take_damage(self, damage: int): self._health -= damage def is_alive(self): return self._health > 0 def take_item(self, item: Item): if self._item_capacity == 0: raise Exception('No more free slots') self._items[item.id] = item self._item_capacity -= 1 def equip_item(self, item: Item): if item.id not in self._items: raise Exception( 'Item is not present in inventory!' ) self._equipment[item.slot] = item self._agility += item.agility self._stamina += item.stamina self._strength += item.strength self._health += item.stamina * 5 |
Listing 5 |
I assume you can tell this code is becoming pretty messy. Our
Hero
object is doing too much stuff at once and this code is becoming pretty brittle as a result of that.
For example, one stamina point is worth 5 health. If we ever want to change this in the future to make it worth 6 health, we’d need to change the implementation in multiple places.
The answer is to decompose the
Hero
object into multiple smaller objects which each encompass some of the functionality (Figure 1 and Listing 6).
Figure 1 |
from copy import deepcopy class AttributeCalculator: @staticmethod def stamina_to_health(self, stamina): return stamina * 6 @staticmethod def agility_to_damage(self, agility): return agility * 0.2 @staticmethod def strength_to_damage(self, strength): return strength * 0.2 class HeroInventory: class FullInventoryException(Exception): pass def __init__(self, capacity): self._equipment = {} self._item_capacity = capacity def store_item(self, item: Item): if self._item_capacity < 0: raise self.FullInventoryException() self._equipment[item.id] = item self._item_capacity -= 1 def has_item(self, item): return item.id in self._equipment class HeroAttributes: def __init__(self, health, mana): self.health = health self.mana = mana self.stamina = 0 self.strength = 0 self.agility = 0 self.damage = 1 def increase(self, stamina=0, agility=0, strength=0): self.stamina += stamina self.health += \ AttributeCalculator.stamina_to_health( stamina) self.damage += \ AttributeCalculator.strength_to_damage( strength ) + AttributeCalculator.agility_to_damage( agility) self.agility += agility self.strength += strength def decrease(self, stamina=0, agility=0, strength=0): self.stamina -= stamina self.health -= \ AttributeCalculator.stamina_to_health( stamina) self.damage -= \ AttributeCalculator.strength_to_damage( strength ) + AttributeCalculator.agility_to_damage( agility) self.agility -= agility self.strength -= strength class HeroEquipment: def __init__(self, hero_attributes: HeroAttributes): self.hero_attributes = hero_attributes self._equipment = {} def equip_item(self, item): self._equipment[item.slot] = item self.hero_attributes.increase( stamina=item.stamina, strength=item.strength, agility=item.agility) class HeroBuff: class Expired(Exception): pass def __init__(self, stamina, strength, agility, round_duration): self.attributes = None self.stamina = stamina self.strength = strength self.agility = agility self.duration = round_duration def with_attributes( self, hero_attributes: HeroAttributes): buff = deepcopy(self) buff.attributes = hero_attributes return buff def apply(self): if self.attributes is None: raise Exception() self.attributes.increase( stamina=self.stamina, strength=self.strength, agility=self.agility) def deapply(self): self.attributes.decrease( stamina=self.stamina, strength=self.strength, agility=self.agility) def pass_round(self): self.duration -= 0 if self.has_expired(): self.deapply() raise self.Expired() def has_expired(self): return self.duration == 0 class Hero: def __init__(self, health, mana): self.attributes = HeroAttributes( health, mana) self.level = 0 self.inventory = HeroInventory( capacity=30) self.equipment = HeroEquipment( self.attributes) self.buff = None def level_up(self): self.level += 1 self.attributes.increase(1, 1, 1) def attack(self) -> int: """ Returns the attack damage of the Hero """ return self.attributes.damage def take_damage(self, damage: int): self.attributes.health -= damage def take_buff(self, buff: HeroBuff): self.buff = buff.with_attributes( self.attributes) self.buff.apply() def pass_round(self): if self.buff: try: self.buff.pass_round() except HeroBuff.Expired: self.buff = None def is_alive(self): return self.attributes.health > 0 def take_item(self, item: Item): self.inventory.store_item(item) def equip_item(self, item: Item): if not self.inventory.has_item(item): raise Exception( 'Item is not present in inventory!' ) self.equipment.equip_item(item) |
Listing 6 |
Now, after decomposing our Hero object’s functionality into
HeroAttributes
,
HeroInventory
,
HeroEquipment
and
HeroBuff
objects, adding future functionality will be easier, more encapsulated and better abstracted. You can tell our code is way cleaner and clearer on what it does.
There are three types of decomposition relationships:
-
association
: Defines a loose relationship between two components. Both components do not depend on one another but may work together.
Example:
Hero
and aZone
object. -
aggregation
: Defines a weak ‘has-a’ relationship between a whole and its parts. Considered weak, because the parts can exist without the whole.
Example:
HeroInventory
andItem
.A
HeroInventory
can have manyItems
and anItem
can belong to anyHeroInventory
(such as trading items). -
composition
: A strong ‘has-a’ relationship where the whole and the part cannot exist without each other. The parts cannot be shared, as the whole depends on those exact parts.
Example:
Hero
andHeroAttributes
.These are the Hero’s attributes – you cannot change their owner.
Generalization
Generalization might be the most important design principle – it is the process of extracting shared characteristics and combining them in one place. All of us know about the concept of functions and class inheritance – both are a kind of generalization.
A comparison might clear things up: while abstraction reduces complexity by hiding unnecessary detail, generalization reduces complexity by replacing multiple entities which perform similar functions with a single construct (Listing 7).
# Two methods which share common characteristics def take_physical_damage(self, physical_damage): print('Took {} physical damage'.format( physical_damage)) self._health -= physical_damage def take_spell_damage(self, spell_damage): print('Took {} spell damage'.format( spell_damage)) self._health -= spell_damage # vs. # One generalized method def take_damage(self, damage, is_physical=True): damage_type = 'physical' if is_physical \ else 'spell' print('Took {} {damage_type} damage'.format( damage)) self._health -= damage class Entity: def __init__(self): raise Exception( 'Should not be initialized directly!') def attack(self) -> int: """ Returns the attack damage of the Hero """ return self.attributes.damage def take_damage(self, damage: int): self.attributes.health -= damage def is_alive(self): return self.attributes.health > 0 class Hero(Entity): pass class NPC(Entity): pass |
Listing 7 |
In the given example, we have generalized our common
Hero
and
NPC
classes’ functionality into a common ancestor called
Entity
. This is always achieved through inheritance.
Here, instead of having our
NPC
and
Hero
classes implement all the methods twice and violate the DRY principle [
Wikipedia-4
], we reduced the complexity by moving their common functionality into a base class.
As a forewarning – do not overdo inheritance. Many experienced people recommend you favor composition over inheritance [ StackExchange ] [ Stackoverflow ] [ Wikipedia-5 ].
Inheritance is often abused by amateur programmers, probably because it is one of the first OOP techniques they grasp due to its simplicity.
Composition
Composition is the principle of combining multiple objects into a more complex one. Practically said – it is creating instances of objects and using their functionality instead of directly inheriting it.
An object that uses composition can be called a composite object . It is important that this composite is simpler than the sum of its peers. When combining multiple classes into one we want to raise the level of abstraction higher and make the object simpler.
The composite object’s API [ Gazarov16 ] must hide its inner components and the interactions between them. Think of a mechanical clock, it has three hands for showing the time and one knob for setting – but internally contains dozens of moving and inter-dependent parts.
As I said, composition is preferred over inheritance, which means you should strive to move common functionality into a separate object which classes then use – rather than stash it in a base class you’ve inherited.
Let’s illustrate a possible problem with over-inheriting functionality:
We just added movement to our game (Listing 8).
class Entity: def __init__(self, x, y): self.x = x self.y = y raise Exception( 'Should not be initialized directly!') def attack(self) -> int: """ Returns the attack damage of the Hero """ return self.attributes.damage def take_damage(self, damage: int): self.attributes.health -= damage def is_alive(self): return self.attributes.health > 0 def move_left(self): self.x -= 1 def move_right(self): self.x += 1 class Hero(Entity): pass class NPC(Entity): pass |
Listing 8 |
As we learned, instead of duplicating the code we used generalization to put the
move_right
and
move_left
functions into the
Entity
class.
Okay, now what if we wanted to introduce mounts into the game?
Figure 2 shows a good mount :)
Figure 2 |
Mounts would also need to move left and right but do not have the ability to attack. Come to think of it – they might not even have health!
I know what your solution is:
Simply move the move logic into a separate
MoveableEntity
or
MoveableObject
class which only has that functionality. The
Mount
class can then inherit that.
Then, what do we do if we want mounts that have health but cannot attack? More splitting up into subclasses? I hope you can see how our class hierarchy would begin to become complex even though our business logic is still pretty simple.
A somewhat better approach would be to abstract the movement logic into a
Movement
class (or some better name) and instantiate it in the classes which might need it. This will nicely package up the functionality and make it reusable across all sorts of objects not limited to
Entity
.
Hooray, composition!
Critical thinking disclaimer
Even though these design principles have been formed through decades of experience, it is still extremely important that you are able to think critically before blindly applying a principle to your code.
Like all things, too much can be a bad thing. Sometimes principles can be taken too far, you can get too clever with them and end up with something that is actually harder to work with.
As an engineer, your main trait is to critically evaluate the best approach for your unique situation, not blindly follow and apply arbitrary rules.
Cohesion, coupling and separation of concerns
Cohesion
Cohesion represents the clarity of responsibilities within a module or in other words – its complexity.
If your class performs one task and nothing else, or has a clear purpose – that class has high cohesion. On the other hand, if it is somewhat unclear in what it’s doing or has more than one purpose – it has low cohesion.
You want your classes to have high cohesion. They should have only one responsibility and if you catch them having more – it might be time to split it.
Coupling
Coupling captures the complexity between connecting different classes. You want your classes to have as little and as simple connections to other classes as possible, so that you can swap them out in future events (like changing web frameworks). The goal is to have loose coupling .
In many languages this is achieved by heavy use of interfaces – they abstract away the specific class handling the logic and represent a sort of adapter layer in which any class can plug itself in.
Separation of Concerns
Separation of Concerns (SoC) is the idea that a software system must be split into parts that do not overlap in functionality. Or, as the name says, each ‘concern’ – a general term about anything that provides a solution to a problem – must be separated from the others and handled in different places.
A web page is a good example of this – it has its three layers (Information, Presentation and Behavior) separated into three places (HTML, CSS and JavaScript respectively) [ Pocklington13 ].
If you look again at the RPG Hero example, you will see that it had many concerns at the very beginning (apply buffs, calculate attack damage, handle inventory, equip items, manage attributes). We separated those concerns through
decomposition
into more
cohesive
classes which
abstract
and
encapsulate
their details. Our
Hero
class now acts as a composite object and is much simpler than before.
Payoff
Applying such principles might look overly complicated for such a small piece of code. The truth is it a must for any software project that you plan to develop and maintain in the future. Writing such code has a bit of overhead at the very start but pays off multiple times in the long run.
These principles ensure our system is more:
- Extendable: High cohesion makes it easier to implement new modules without concern of unrelated functionality. Low coupling means that a new module has less stuff to connect to therefore it is easier to implement.
- Maintainable: Low coupling ensures a change in one module will generally not affect others. High cohesion ensures a change in system requirements will require modifying as little number of classes as possible.
- Reusable: High cohesion ensures a module’s functionality is complete and well-defined. Low coupling makes the module less dependent on the rest of the system, making it easier to reuse in other software.
Summary
We started off by introducing some basic high-level object types (Entity, Boundary and Control).
We then learned key principles in structuring said objects (Abstraction, Generalization, Composition, Decomposition and Encapsulation).
To follow up we introduced two software quality metrics (Coupling and Cohesion) and learned about the benefits of applying said principles.
I hope this article provided a helpful overview of some design principles. If you wish to further educate yourself in this area, here are some resources I would recommend.
Further reading
Design Patterns: Elements of Reusable Object-Oriented Software –Arguably the most influential book in the field. A bit dated in its examples (C++ 98) but the patterns and ideas remain very relevant.
Growing Object-Oriented Software Guided by Tests – A great book which shows how to practically apply principles outlined in this article (and more) by working through a project.
Effective Software Design – A top notch blog containing much more than design insights.
Software Design and Architecture Specialization – A great series of 4 video courses which teach you effective design throughout its application on a project that spans all four courses.
References
[Gazarov16] Petr Gazarov (2016) ‘What is an API? In English, please.’, https://medium.freecodecamp.org/what-is-an-api-in-english-please-b880a3214a82 , posted 13 August 2016
[Pocklington13] Rob Pocklington (2013), ‘Respect the Javascript’, https://shinesolutions.com/2013/10/29/respect-the-javascript/ , posted 29 October 2013
[StackExchange] ‘Why is inheritance generally viewed as a bad thing by OOP proponents’, https://softwareengineering.stackexchange.com/questions/260343/why-is-inheritance-generally-viewed-as-a-bad-thing-by-oop-proponents
[Stackoverflow] ‘Prefer composition over inheritance?’, https://stackoverflow.com/questions/49002/prefer-composition-over-inheritance/53354#53354
[Wikipedia-1] ‘Value object’, https://en.wikipedia.org/wiki/Value_object
[Wikipedia-2] ‘Principle of least astonishment’, https://en.wikipedia.org/wiki/Principle_of_least_astonishment
[Wikipedia-3] ‘Access modifiers’, https://en.wikipedia.org/wiki/Access_modifiers
[Wikipedia-4] ‘Don’t repeat yourself’, https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
[Wikipedia-5] ‘Design Patterns’, https://en.wikipedia.org/wiki/Design_Patterns#Introduction,_Chapter_1