Skip to content

Class design

Learning outcomes

  • Understand what an invariant is
  • Write a class that protects an invariant
For teachers

Prior:

  • What are classes?
  • When to use classes, or when not?

Why?

You are modeling something in the real world as code. You want to use the same world in your code as in the real world and you want it to be natural to use in your code.

This is a goal of class design.

Example

Here we see some code where a user constantly checks if his/her stays positive:

positive_number = 42
assert positive_number >= 0

positive_number = do_something_with_it(positive_number)
assert positive_number >= 0

positive_number = do_something_else_with_it(positive_number)
assert positive_number >= 0

Wouldn't it be great if positive_number itself could check if it is positive, instead of us asserting this at every step?

For that, we could write a class for exactly that, with a name such as PositiveNumber.

Benefits from object-oriented development

Benefits from object-oriented development (from [Booch, 2008]):

  • Appeals to the working of human cognition
  • Leads to systems that are more resilient to change
  • Encourages the reuse of software components
  • Reduces development risk
  • Exploits the expressive power of object-oriented programming languages

An invariant

An invariant is something that must always be. Some examples:

  • a persons' age must always be positive
  • a persons' total height must be longer than a persons' arms' length

Use a class if the class has an invariant [CppCore C.2].

For example, here we have a class with an invariant:

classDiagram
  class Range{
    -lowest
    -highest
  }
What is the invariant in the Range class?

The invariant is that highest must be bigger or equal to lowest.

Writing a good class

A good class guarantees that its stored data is valid. For example, the class DnaSequence is probably a string of one or more A, C, G and T

  • the quality requirements for a function, among others a good interface
  • writing a design, documentation and tests all help

General class anatomy

  • A constructor: all data needed to create it
  • Private member variables
  • Public member functions
Prefer R?

Class anatomy in R:

  • R has four class types (S3, S4, R5, R6)
  • S3 classes are closest to structures
  • R6 classes are real classes

A DnaSequence class

Here is how to implement a class for a DNA sequence:

class DnaSequence:
    def __init__(self, sequence):
        assert is_dna_string(sequence)
        self._sequence = sequence

    def get_str(self):
        return self._sequence

a = DnaSequence("ACGT")
assert a.get_str() == "ACGT"

The init method (also known as a constructor) checks if the input is indeed a valid DNA string, using an assert. After that, the sequences is stored inside of the class, in a member variable called _sequence. The underscore signals (by social convention) that the value must be treated as 'do not touch' and that the only class itself will keep it valid.

However, nothing stops you from doing this:

a._sequence = "XXX" # No! Breaks the invariant!
assert a.get_str() == "XXX"

On the other hand, a Python developer can at least see that this convention was broken.

Note that some other programming languages completely disallows you from modifying a so-called 'private' member variable.

Exercise

Exercise: write a class with an invariant

  • Pick a class at your skill level:
Easiest: a class for a positive number

Here is an example how to use it:

x = PositiveNumber(3)
assert x.get_value() == 3
PositiveNumber(-1) # Must raise an exception

Work in src/learners.

Medium: a class for a range, e.g 'a range from 3 to 10'

Here is an example how to use it:

x = Range(3, 10)
assert x.get_lowest() == 3
assert x.get_highest() == 10
Range(100, 10) # Must raise an exception

Work in src/learners

Hard: your own class

Come up with a class you may need yourself and try to write it.

Work in src/learners

  • Write the class that protects its invariant
Answer for a positive number
class PositiveNumber:
    def __init__(self, any_positive_number):
        assert any_positive_number >= 0
        self._value = any_positive_number
    def get_value(self):
        return self._value
Answer for range
class Range:
    def __init__(self, any_lowest, any_highest):
        assert any_lowest <= any_highest
        self._lowest = any_lowest
        self._highest = any_highest
    def get_lowest(self):
        return self._lowest
    def get_highest(self):
        return self._highest

References