The four pillars of object-oriented programming - part 2 - inheritance
This post was published on July 11, 2022In this blog post series, I’ll dive deeper into the four pillars (fundamental principles) of object-oriented programming:
- Encapsulation
- Inheritance (this post)
- Polymorphism
- Abstraction
Why? Because I think they are essential knowledge not just for developers, but definitely also for testers working with, reading or writing code. Understanding these principles helps you better understand application code, make recommendations on how to improve the structure of that code and, of course, write better automation code, too.
The examples I give will be mostly written in Java, but throughout these blog posts, I’ll mention how to implement these concepts, where possible, in C# and Python, too.
What is inheritance?
Inheritance is the practice of deriving classes from other classes, or, to put it in other words, creating parent-child relationships between classes.
Inheritance thus allows programmers to create a hierarchy of classes, which gives them the opportunity to better structure their code. Applying inheritance also removes the need for defining properties shared between classes more than once, which improves the maintainability of our code.
Inheritance: an example
To better understand what inheritance can do for you, let’s again take a look at the Account
class we defined in the previous post. In the withdraw()
method defined in that class, we made a distinction between the business rules for savings and checking accounts.
Now, consider a situation where we want to extend the functionality of this Account
class, but only for savings accounts. For example, we want to be able to add interest to an account, but only if the type of the account is AccountType.SAVINGS
.
We could solve this by using another if-then-else construct, just as we did in the withdraw()
method, but wouldn’t it be nicer if we could somehow find a way to make sure that adding interest is possible only on savings accounts?
In other words, is there a way in which we can create a new type of account, that shares the properties of our existing Account
class, but that has additional features that are specific to that type of account only? One of the ways to do this is by using inheritance.
Implementing inheritance
In the code snippet below, we create a new class SavingsAccount
that extends our existing Account
class and adds the option to calculate interest:
This SavingsAccount
class is created as a child class of the Account
class, as indicated by the extends
keyword. This means that SavingsAccount
inherits all the non-private properties and methods defined in Account
, so calling methods defined in Account
on an object of type SavingsAccount
is now possible:
When instantiating a new object that inherits from another class, Java requires you to call an appropriate constructor of the parent class first, to make sure that all properties are initialized correctly.
This is exactly what the super()
call does: it calls the constructor of the parent Account
class, which in turn sets the account type and balance (see the previous post for the Account
constructor implementation).
In addition to all properties and methods defined in the parent class, an object of type SavingsAccount
also has the addInterest()
method at its disposal.
Please note here that inheriting properties is a one way process: SavingsAccount
inherits the non-private properties and methods from Account
, but Account
does not inherit properties and methods from SavingsAccount
. As a result, it is not possible to call the addInterest()
method on an object of type Account
.
Inheritance and access modifiers
The final aspect of inheritance we need to address here is the access modifiers used on the properties defined in the parent Account
class. As you can see in the implementation of the addInterest()
method, there’s a direct reference to the balance
property defined in the parent class.
However, as this property was defined as private
, even the SavingsAccount
does not have access to it. Making it public
again would expose access to the balance property to all code again, which is not what we want, because that breaks the encapsulation we so carefully applied in the previous article… How can we deal with this?
Lucky for us, Java offers a way out by means of the protected
access modifier:
This access modifier enables access to a property (or a method, or even an entire class) to the class itself, as well as to all child classes of that class, but not to other classes. Exactly what we are looking for! We can now directly access and modify the balance
property from our Account
class as well as from our SavingsAccount
class, but not from anywhere else.
Inheritance in other languages
All of what you’ve seen above in Java can be done in C# as well. Defining a parent-child relationship between classes looks like this:
C# also offers the protected
keyword to give access to properties and methods to child classes as well as the implementing class itself.
Python, too, enables us to use inheritance:
Unlike in Java and C#, with Python you can even do multiple inheritance, i.e., create a class that inherits properties and methods from multiple different classes.
Python does not provide an access modifier similar to protected
in Java and C#. You can, however, still simulate this behaviour through property decorators.
Inheritance in automation
I have seen two applications of the inheritance principle in automation that are used fairly often.
The first, building on the Page Object pattern that we briefly discussed in the previous post, is the use of base pages that contain common methods that apply to multiple page objects. These can be wrappers around the Selenium API, for example, introducing an explicit wait before calling sendKeys()
or click()
:
These can then be inherited by and used in a Page Object class:
Another common application of inheritance in tests (in general, not just with user interface-driven tests) is specifying setup and teardown methods that apply to tests in several test classes in a base test class, and having all test classes inherit from this base test class:
The output generated when running these tests looks like this:
Test setup goes here...
Running the first test...
Test teardown goes here...
Test setup goes here...
Running the second test...
Test teardown goes here...
As you can see, the test setup and teardown methods defined in the base class are run before and after each of the tests in the test class, as we intended.
In the next blog post in this series, we’ll take a closer look at the third of the four fundamental principles of object-oriented programming: polymorphism.
"