Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Object-oriented design principles

After this lesson: I can spot code that gives a class two jobs or is hard to extend.

55 min lesson10 commandsEveryday Linux commands lab: the thirty commands engineers type from memory

Design principles show when a class is wrong

Object-oriented design principles are guidelines that keep classes small, clear and easy to change. Two of the best known are the single responsibility principle and the open/closed principle. They help you see when a class does too much, and when a new feature forces you to edit code that already works.

By the end of this lesson

Note

I can spot code that gives a class two jobs or is hard to extend.

Key words

WordWhat it means
Responsibilityone job a class is there to do
Reason to changea kind of request that forces an edit
Single responsibilitya class has one reason to change
Open/closedopen to extension, closed to editing
Extensionnew behaviour added without editing old code

The cells students fill in are left blank in this preview.

A class should have one reason to change

Ask of any class: what kinds of request would force me to edit it? If the answer is two or more, the class has two jobs. This is the single responsibility principle.

Look at Summary. It counts errors and it also decides the wording. A change to the counting rule and a change to the wording both edit the same class.

A class with two jobs

Code: python
class Summary:
    def __init__(self, levels):
        self.levels = levels

    def count_errors(self):
        total = 0
        for level in self.levels:
            if level == "ERROR":
                total = total + 1
        return total

    def text(self):
        return "Errors: " + str(self.count_errors())

Make a folder for this lesson

Command
mkdir -p ~/oop4

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Go into it

Command
cd ~/oop4

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the class to smell.py

Command
printf 'class Summary:\n    def __init__(self, levels):\n        self.levels = levels\n\n    def count_errors(self):\n        total = 0\n        for level in self.levels:\n            if level == "ERROR":\n                total = total + 1\n        return total\n\n    def text(self):\n        return "Errors: " + str(self.count_errors())\n' > smell.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run the summary on three levels

Command
python3 -c "from smell import Summary; print(Summary(['INFO', 'ERROR', 'ERROR']).text())"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Split a class by its reasons to change

The Run printed Errors: 2, so the class works. It still has two reasons to change. Give each reason its own class.

ErrorCounter only counts. SummaryText only words the answer. A French summary or an HTML one now means a new text class, and the counter is never touched.

One job for each class

Code: python
class ErrorCounter:
    def __init__(self, levels):
        self.levels = levels

    def count(self):
        total = 0
        for level in self.levels:
            if level == "ERROR":
                total = total + 1
        return total

class SummaryText:
    def words(self, count):
        return "Errors: " + str(count)

Write the two classes to better.py

Command
printf 'class ErrorCounter:\n    def __init__(self, levels):\n        self.levels = levels\n\n    def count(self):\n        total = 0\n        for level in self.levels:\n            if level == "ERROR":\n                total = total + 1\n        return total\n\nclass SummaryText:\n    def words(self, count):\n        return "Errors: " + str(count)\n' > better.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Count with one class, word with the other

Command
python3 -c "from better import ErrorCounter, SummaryText; c = ErrorCounter(['INFO', 'ERROR', 'ERROR']); print(SummaryText().words(c.count()))"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Open for extension, closed for editing

The open/closed principle says you should add new behaviour by adding code, not by editing code that works. Each edit risks breaking something that was fine.

Look at score below. A new level, CRITICAL, means opening the function and adding another elif. The next level means doing it again.

A score edited for every level

Code: python
def score(level):
    if level == "ERROR":
        return 5
    elif level == "WARN":
        return 2
    return 0

Write it to chain.py

Command
printf 'def score(level):\n    if level == "ERROR":\n        return 5\n    elif level == "WARN":\n        return 2\n    return 0\n' > chain.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Score a level the function does not know

Command
python3 -c "from chain import score; print(score('ERROR'), score('CRITICAL'))"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Move what varies into data or a part

The Run printed 5 0. CRITICAL scores 0 because nobody has edited the function yet.

The fix is to move what changes out of the code. Scorer takes a dictionary of weights. A new level is one more entry in the data, and the class is never edited.

A scorer that is open to new levels

Code: python
class Scorer:
    def __init__(self, weights):
        self.weights = weights

    def score(self, level):
        if level in self.weights:
            return self.weights[level]
        return 0

Write it to scorer.py

Command
printf 'class Scorer:\n    def __init__(self, weights):\n        self.weights = weights\n\n    def score(self, level):\n        if level in self.weights:\n            return self.weights[level]\n        return 0\n' > scorer.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Add a level by changing data only

Command
python3 -c "from scorer import Scorer; a = Scorer({'ERROR': 5, 'WARN': 2}); print(a.score('ERROR'), a.score('WARN'), a.score('INFO')); b = Scorer({'ERROR': 5, 'WARN': 2, 'CRITICAL': 9}); print(b.score('CRITICAL'))"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Signs that a design needs work

What you seeThe signA way out
A class whose name has "and" in itTwo jobsSplit it into two classes
An if and elif chain on a kind or a typeEvery new kind edits old codeMove the kind into data or a part
A change in one place breaks a far away placeToo much shared in one classGive each reason its own class
Tests that need a lot of setupA class doing too muchSplit it and test the parts

The cells students fill in are left blank in this preview.

A common mistake

Warning

Many people think a principle is a law, so every class must be tiny and every if must go. A principle is a guide for the code you expect to change. Split a class when you can name its two reasons to change, and leave a short stable class alone.

About this lab and its source: Everyday Linux commands lab: the thirty commands engineers type from memory

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.