2.6 Inheritance: Thoughts on Design

Now that you understand the mechanics of inheritance, let’s go back and make some final comments.

Four things we can do with an inherited method

When a subclass inherits a method from its superclass, there are four things we can choose to do in the subclass.

1. Simply inherit an implemented method

If a method has been implemented in the superclass and its behaviour is appropriate for the subclass, then we can simply use this behaviour by choosing not to override the method. For example, HourlyEmployee does not define a pay method, so it simply inherits the pay method from Employee. Any time we call pay on an instance of HourlyEmployee, the Employee.pay method is called.

Of course, we should never do this when the method is abstract, because a subclass must override every abstract method to implement it properly.

2. Override an abstract method to implement it

When a method has not been implemented in the superclass (its body is just raise NotImplementedError), the method must be overridden in the subclass in order to provide an implementation. This is not a requirement of the Python language, but is a feature of the way we are using inheritance. In other uses of inheritance, implementation can be deferred to a subclass of the subclass or a class further down in the inheritance chain. For example, SalariedEmployee and HourlyEmployee must both implement the abstract get_monthly_payment method.

3. Override an implemented method to replace it

If a method has been implemented in the superclass, but the subclass requires a different behaviour, the subclass can override the method and provide a completely different implementation. This is something we haven’t yet seen, but is very simple. For example, we could override the pay method in SalariedEmployee:

class SalariedEmployee(Employee):
    def get_monthly_payment(self) -> float:
        # Assuming an annual salary of 60,000
        return round(60000 / 12, 2)

    def pay(self, pay_date: date) -> None:
        print('Payment rejected! Mwahahahaha.')

>>> fred = SalariedEmployee()
>>> fred.pay(date(2017, 9, 30))
Payment rejected! Mwahahahaha.

4. Override an implemented method to extend it

Sometimes we want the behaviour that was defined in the superclass, but we want to add some other behaviour. In other words, we want to extend the inherited behaviour. We have witnessed this in the initializers for our payroll system. The Employee initializer takes care of instance attributes that are common to all employees. Rather than repeat that code, each subclass initializer calls it as a helper and then has additional code to initialize additional instance attributes that are specific to that subclass.

We can extend any inherited method, not just an initializer. Here’s an example. Suppose at pay time we wanted to print out two messages, the original one from Employee, and also a SalariedEmployee-specific message. Since we already have a superclass method that does part of the work, we can call it as a helper method instead of repeating its code:

class SalariedEmployee(Employee):
    def pay(self, pay_date: date) -> None:
        Employee.pay(self, pay_date)  # Call the superclass method as a helper.
        print('Payment accepted! Have a nice day. :)')

>>> fred = SalariedEmployee()
>>> fred.pay(date(2017, 9, 30))
An employee was paid 3200 on September 30, 2017.
Payment accepted! Have a nice day. :)

Using inheritance to define a shared public interface

Our use of inheritance allows client code to do the same thing for all types of employee. Here’s an example where we iterate over a list of employees and call method pay on each, without regard to what kind of Employee each one is:

>>> employees = [
        SalariedEmployee(14, 'Fred Flintstone', 5200.0),
        HourlyEmployee(23, 'Barney Rubble', 1.25, 50.0),
        SalariedEmployee(99, 'Mr Slate', 120000.0)
    ]
for e in employees:
    # At this point, we don't know what kind of employee e is.
    # It doesn't matter, because they all share a common interface,
    # as defined by class Employee!
    # For example, they all have a pay method.
    e.pay(date(2018, 8, 31))

In other words, the client can write code to an interface defined once in the abstract class that will work for any of its subclasses—even ones that we haven’t thought of yet!

This is very powerful. If we couldn’t treat all kinds of employees the same way, we would need an if block that checks what kind of employee we have and does the specific thing that is appropriate for each kind. Much messier, especially if there are more than one or two subclasses!

We say that the Employee class represents the shared public interface of classes SalariedEmployee and HourlyEmployee. The public interface of a class is the way client code interacts with the methods and attributes of the class. It’s a “shared” public interface in the sense that it is held in common between SalariedEmployee and HourlyEmployee.

We say that class Employee is polymorphic, The roots of this word are poly, which means “many”, and morphe, which means “form”. So “polymorphic” literally means “taking many forms”., to signify that it can take different forms: as a SalariedEmployee or an HourlyEmployee.

Abstract classes are useful

The Employee class is abstract, and client code should never instantiate it. Is it therefore useless? No, quite the opposite! We’ve already seen that it defines a shared public interface that client code can count on, and as a result, supports polymorphism. Furthermore, polymorphic client code will continue to work even if new subclasses are written in the future!

Our abstract Employee class is useful in a second way. If and when someone does decide to write another subclass of Employee, for instance for employees who are paid a commission, the programmer knows that the abstract method get_monthly_payment must be implemented. In other words, they must support the shared public interface that the client code counts on. We can think of this as providing helpful guidance for the programmer writing the new subclass.

When to use inheritance

We’ve seen some benefits of inheritance. However, inheritance isn’t perfect for every situation. Don’t forget the other kind of relationship between classes that we’ve seen: composition. For example, to represent people who are car owners, a Person object might have an attribute car which stores a reference to a Car object. We wouldn’t use inheritance to represent the relationship between Person and Car!

Composition is commonly thought of as a “has a” relationship. For example, a person “has a” car. Inheritance is thought of as an “is a” relationship. For example, a salaried employee “is an” employee. Of course, the “has a” vs. “is a” categorization is rather simplistic, and not every real-world problem is so clearly defined.

When we use inheritance, any change in a superclass affects all of its subclasses, which can lead to unintended effects. To avoid this complexity, in this course we’ll stick to using inheritance in the traditional “shared public interface” sense. Moreover, we will often prefer that a subclass not change the public interface of a superclass at all:

As a general programming concept, inheritance has many other uses, and you’ll learn about some of them in CSC207, Software Design.