Inheritance Best Practices
00:00 You’ve now learned how to write what is fundamentally the same software project using both inheritance and composition. The rest of this course will be about learning when to use each one, and more importantly, when not to use them.
00:18 We’ll start with inheritance. First off, it’s a good idea to try to follow the Liskov substitution principle when you can. If you remember, this states that if some interface requires a specific type of object, you can substitute in a class that is a child of that type so long as it properly inherits the interface it needs.
00:46
We use this in our employee management program—both the productivity and payroll systems required a list of employees to operate on—but instead of just giving them Employee
objects we gave them subclasses of Employee
, like Manager
or Secretary
.
01:07
Additionally, the Python language requires that we make exception types inherit from BaseException
, but we inherited from Exception
, which is okay because Exception
itself inherits from BaseException
.
01:24 When you have two classes and you’re trying to determine whether or not you should inherit one from the other, think about the is a relationship that inheritance models. In general, you want to use inheritance only if the relationship works in one direction—that is, A is a B but B is not an A.
01:50 If you can justify the relationship in both directions, you should avoid making one inherit from the other. To understand why, let’s take a look at a code demo.
02:02
Let’s say we are writing a program to calculate the areas of various shapes. I’m writing this in a new module called rectangle_square_demo
.
02:14 There are two shapes we need to be able to handle: squares and rectangles. Squares and rectangles are pretty similar. A square is just a rectangle but with equal sides.
02:28 A rectangle, on the other hand, does not have to have sides of equal length. At the same time, you could argue that a rectangle is just a square with uneven lengths.
02:42
It sounds like this relationship works either way, so we probably shouldn’t use inheritance. To show you what would happen if we do, I’m going to create a Rectangle
class and then a Square
class that inherits from Rectangle
. I’ll make the Rectangle
have instance attributes for the length and the height.
03:05
I’m using a leading underscore (_
) to show that we should not access or modify these attributes from outside of the Rectangle
class.
03:15
In order to calculate the area, I’ll create a new method called .area()
. This will simply return the ._length
times the ._height
.
03:26
I’m going to add this @property
decorator to the method so that we can access it as if it were an attribute and not a method. That just means that we’ll call the method without parentheses.
03:41
Now, I’m going to create a new class called Square
, which will inherit from Rectangle
. This class will have a different interface for its requirements.
03:53
It will accept only a side_size
and then it will initialize its parent constructor, passing in that size for both the length
and the height
.
04:06
That looks like it should be good. Let’s try this out by instantiating the classes. I’ll create a rectangle
object that will store a 2
by 4
Rectangle
.
04:19
This should give us an area of 8
. To check that I’ll say assert rectangle.area == 8
. The assert
keyword acts almost as a conditional.
04:35
If the Boolean statement evaluates to True
, execution continues to the next line.
04:41
If it’s False
, execution stops and an AssertionError
is raised. Also, notice how we didn’t call the .area()
method with parentheses.
04:52
It almost looks like it’s an attribute. That’s because we marked that method as a property. We can use a property for this method because we aren’t passing any data to the .area()
method. We’re just calling it, so it calculates the area based on the instance attributes already set in the object. Next, I’ll create a square of length 2
.
05:18
Just like before, we want to assert that the area of this square is 4
.
05:25
If all this checks out I’ll print "OK :)"
.
05:31
It looks like everything is okay so far. Right now, Square
just acts as a specialized Rectangle
. But now what happens if we have to add support for resizing the rectangle after it’s been created? That doesn’t sound too hard.
05:50
All we have to do is create a method called .resize()
, which will change the instance attributes of the Rectangle
to some new values.
06:01 I’ll add this new method to our list of assertions.
06:08
All right, that seems to work fine. What about resizing a square? The Square
class inherited the .resize()
method from the Rectangle
class.
06:20 I’ll give it some dimensions that are not equal.
06:29
And now we see that it’s working, unfortunately. We’ve just created a Square
object and transformed it into what’s really a rectangle, but it’s still of type Square
; now it’s just an invalid square. There are ways that we can fix this, but it’s going to be awkward any way we do it.
06:50
We could override the .resize()
method in the Square
class, ignoring the height
parameter, but then we’d have a bunch of Rectangle
objects where only some of them are resized with two parameters—the rectangle—and others—the squares—are resized with one. The Square
and the Rectangle
require different interfaces because their .resize()
methods require a different number of arguments.
Bartosz Zaczyński RP Team on Jan. 3, 2022
@mojkol The rectangle-square inheritance is a classic example of the Liskov Substitution Principle (the “L” in SOLID) violation, leading to subtle bugs. In a nutshell, code that expects rectangles should work correctly with instances of rectangle’s subclasses without knowing the difference. Since squares exhibit different behaviors and properties than rectangles, you may be forced to check the type information at runtime, which breaks polymorphism. There’s a great lecture available for free on YouTube by Uncle Bob about the SOLID principles, which goes into more detail about the rectangle-square problem.
Become a Member to join the conversation.
mojkol on Dec. 18, 2021
First, thank you for the great tutorial! Let me ask a quick question. I just don’t understand why the Square class can’t inherit the Rectangle? You said, because resize() method requires two parameters for the Rectangle class and it requires one parameter for the Square class. What confuses me is that, the same goes for the init() method. init() of Rectangle accepts two parameters and the init() of Square accepts one parameter! So, why this fact is not a problem for init(), but it is a problem for resize()?! Thanks!