1. Designing for inheritance and polymorphism
In this final chapter, we'll talk about some good practices of class design. We'll cover two main topics: efficient use of inheritance, and managing the levels of access to the data contained in your objects.
2. Polymorphism
Polymorphism means using a unified interface to operate on objects of different classes. We've already dealt with it in Chapter 2.
3. Account classes
We defined a bank account class, and two classes inherited from it: a checking account class and a savings account class. All of them had a withdraw method, but the checking account's method was executing different code.
4. All that matters is the interface
Let's say we defined a function to withdraw the same amount of money from a whole list of accounts at once.
This function doesn't know -- or care -- whether the objects passed to it are checking accounts, savings accounts or a mix -- all that matters is that they have a withdraw method that accepts one argument. That is enough to make the function work. It does not check which withdraw it should call -- the original or the modified. When the withdraw method is actually called, Python will dynamically pull the correct method: modified withdraw for whenever a checking account is being processed,and base withdraw for whenever a savings or generic bank account is processed.
So you, as a person writing this batch processing function, don't need to worry about what exactly is being passed to it, only what kind of interface it has.
To really make use of this idea, you have to design your classes with inheritance and polymorphism - the uniformity of interface - in mind
5. Liskov substitution principle
There is a fundamental object-oriented design principle of when and how to use inheritance properly, called "Liskov substitution principle" named after the computer scientist Barbara Liskov:
A base class should be interchangeable with any of its subclasses without altering any properties of the surrounding program.
Using the example of our Account hierarchy, that means that wherever in your application you use a bankaccount object instance, substituting a checking account instead should not affect anything in the surrounding program. For example, the batch withdraw function worked regardless of what kind of account was used.
6. Liskov substitution principle
This should be true both syntactically and semantically. On the one hand, the method in a subclass should have a signature with parameters and returned values compatible with the method in the parent class. On the other hand, the state of objects also must stay consistent; the subclass method shouldn't rely on stronger input conditions, should not provide weaker output conditions, it should not throw additional exceptions and so on.
7. Violating LSP
Let's illustrate some possible violations of LSP - Liskov substitution principle - on our account classes: for example, the parent's -- or base's -- withdraw method could require 1 parameter, but the subclass method could require 2. Then we couldn't use the subclass's withdraw in place of the parent's. But if the subclass method has a default value for the second parameter, then there is no problem.
If the subclass method only accepts certain amounts, unlike the base one, then sometimes the subclass could not be used in place of the base class, if those unsuitable amounts are used.
If the base withdraw had a check for whether the resulting balance is positive, and only performed the withdrawal in that case, but the subclass did not do that, we wouldn't be able to use subclass in place of the base class, because it's possible that ambient program depends on the fact that the balance is always positive after withdrawal.
8. Violating LSP
There are other ways to violate LSP like changing attributes that weren't changed in the base class, or throwing additional exceptions that the base class didn't throw (because those new exceptions are guaranteed to be unhandled).
9. No LSP - no inheritance
The ultimate rule is that if your class hierarchy violates the Liskov substitution principle, then you should not be using inheritance, because it is likely to cause the code to behave in unpredictable ways somewhere down the road.
10. Let's practice!
In the exercises, you'll explore the circle-ellipse problem - a famous software design problem that shows how our notions of inheritance are flawed. Have fun!