Designing object-oriented programs

Let’s talk about some principles of designing classes.

Is-A vs. Has-A

Let’s say we’re designing an ecology simulator. We have a collection of classes:

  • Animal
  • Tiger
  • Mongoose
  • Habitat

What might the relationships among these classes be?

We know that tigers and mongooses are animals, so it would make sense for them to both be subclasses of Animal. A tiger isn’t a mongoose and a mongoose isn’t a tiger; so, neither should be a subclass of the other.

Should Animal be a subclass of Habitat? Many animals live inside a Habitat. But, an animal is not in itself a habitat (except, perhaps, in cases like remoras living on a whale). This case illustrates an important distinction:

Is-A
If every A is a B, then perhaps A should be a subclass of B. For instance, every convertible is a car, so Convertible could be a subclass of Car.
Has-A
Is an A has a B or contains a collection of B, then A should have a field of type B (or a list of B, etc.). For instance, a car has an engine, so a Car class could have a field engine whose type is Engine.

Another way to think about this is to think about what inheritance does: inheritance lets us share functionality between classes. An Animal shouldn’t have the functionality of a Habitat.

A Library class

We can add a Library class to our little library system:

class Library:
    def __init__(self):
        self.shelves = []

    def add(self, item: LibraryItem):
        self.shelves.append(item)

    def search(self, query: str) -> list:
        items = []
        for item in self.shelves:
            if item.matches(query):
                items.append(item)
        return items

Should Library inherit from LibraryItem? How about vice versa?

Neither class should be a subclass of the other: a LibraryItem is not a Library and a Library is not a LibraryItem.

Exercise: astronomy simulation

Imagine we were writing an astronomy simulation. What might the relationships be among these classes? Consider both has-A and is-A relationships.

  • Star
  • Planet
  • Gas giant
  • Galaxy
  • Spiral Galaxy
  • Earth

See the lecture capture for details.

Inheritance vs. instantiation

There’s a last, really important distinction to be made here. Let’s say we have something like this:

class Planet:
    pass #other methods elided

class GasGiant(Planet):
    pass

earth = Planet() # maybe need to pass size, climate type, etc...

GasGiant is a subclass of Planet. Both are classes. earth is an instance of Planet–an object whose class is Planet.

When to use dataclasses

At the beginning of the class, we used dataclasses in order to represent complex data types. We’ve also seen regular Python classes, which can also be used for the same thing. When would we use each?

Dataclasses are very useful to represent data that won’t change during the program’s execution. For instance, if we wanted to represent people in our library system (authors of books or directors of movies, say) we might have a dataclass with fields for name and birth year:

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    birth_year: int

Using dataclasses for data like this has some advantages. You get a nice display for free, and you get equality checking that works the way you want:

>>> Person("Doug", 1989) == Person("Doug", 1989)
True