A Short Overview of Object Oriented Software Design

A Short Overview of Object Oriented Software Design

By Stanislav Kozlovski

Overload, 26(145):9-15, June 2018


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 a Zone 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 and Item .

    A HeroInventory can have many Items and an Item can belong to any HeroInventory (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 and HeroAttributes .

    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






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.