The four pillars of object-oriented programming - part 4 - abstraction
This post was published on October 6, 2022In this blog post series, I’ll dive deeper into the four pillars (fundamental principles) of object-oriented programming:
- Encapsulation
- Inheritance
- Polymorphism
- Abstraction (this post)
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 abstraction?
Abstraction is the creation of abstract representations of or blueprints for concrete concepts, most commonly classes.
Abstraction allows programmers to enforce a common structure for a group of related classes, either through use of interfaces or through abstract classes.
Abstraction: an example
To get a better understanding of what abstraction looks like and what the benefits of abstraction are in object-oriented programming, let’s look at an example. In the previous post in this series, we ended up with a class SavingsAccount
that inherited from a parent class Account
but had its own implementation of specific methods.
If we take a closer look, however, at the relationship between Account
and SavingsAccount
, the parent-child relationship is probably not the best. A better way of modeling different types of classes in our code would be to have each type of account (checking, savings, investment, …) be represented by its own class, without any parent-child relationships between them.
We do want to ensure, though, that all classes follow some sort of general structure, or rather, that they all contain specific properties and methods that are generic to all types of accounts. Applying abstraction can help us do exactly that.
First, we’re going to take a look at how to do that using interfaces.
Interfaces in action
Interfaces in Java can be seen as a form of contract that all classes that implement that interface should adhere to. It contains, at the minimum, a list of methods that should be present in all classes that follow (implement) the interface. Here’s what an Account
interface might look like:
This interface tells us that all types of accounts should, at a minimum, implement a withdraw()
method, as well as a deposit()
method. The classes can also contain other methods that are not defined in the interface, but they have to implement these.
A SavingsAccount
class can now be defined implementing the Account
interface like this:
Note the use of the implements
keyword here to denote that our SavingsAccount
class follows the structure defined in the Account
interface. Failing to implement the methods defined in the interface in the class results in a compile-time error.
Other classes, such as CheckingAccount
can now also implement the Account
interface, and we can even instantiate new objects using the interface data type:
So, interfaces are a way to establish a common structure for classes. Again, you could see this as a form of a contract. But what if the implementation for different methods is the same across many or even all classes that implement the interface? Wouldn’t that introduce a lot of duplicated code? Well, yes it would, but fear not, there are ways to address this.
One way to address this problem is by using default methods in your interface. A default method is defined at the interface level and will automatically be available in all classes that implement the interface:
However, this will only get you so far, as interfaces in Java do not have state, i.e., you can’t define, access or modify properties in interfaces. If, for example, we want to define a common implementation for the deposit()
method for all of our account types in the abstraction, we can’t achieve that using interfaces. Instead, we’ll have to use an abstract class.
Abstract classes in action
Like interfaces, abstract classes provide a way to enforce a common structure on a group of related classes. In contrast to interfaces, however, abstract classes can have state, and can have method implementations that access and modify the state of an object, i.e., its properties. Here’s what an abstract Account
class, defining a common implementation for the deposit()
method, could look like:
And here’s how our SavingsAccount
class now extends Account
(you extend an abstract class, you don’t implement it):
As you can see, the SavingsAccount
no longer needs to contain an implementation of the deposit()
method, as that is already supplied by the abstract class, but you can call the method on an object of type SavingsAccount
without problems:
Furthermore, as the balance
property is already defined in Account
, SavingsAccount
can access and use it without the need to explicitly define it once more. This all under the condition, of course, that your access modifiers allow you to do so.
Abstraction in other languages
In C#, abstraction works much the same way as in Java, with some slight differences. The main difference is that in C#, you can define even more in an interface than you can in Java. You can define interfaces, and interfaces can contain method implementations, as in Java, but in C# (or at least in recent versions of the language), interfaces can also define and access state (again, properties).
This makes the difference between interfaces and abstract methods in C# even smaller than in Java. The biggest remaining differences are, in my opinion:
- Interfaces cannot contain constructors, whereas abstract methods can
- A class can only extend one abstract class, but it can implement multiple interfaces
These differences apply to both Java and C#.
Python does know the concept of an abstract class, which can be used to implement abstraction. There is such thing as an interface in Python. You can, technically, construct something that in some way resembles an interface, as is shown in this article, but in my opinion, it look pretty contrived and not like a proper interface as you would see in Java or C#.
Abstraction in automation
Abstraction is the principle of object-oriented programming I find myself applying the least in my automation code, to the extent that it is hard for me to come up with an useful example of the use of it. I even think that if you find yourself using interfaces or abstract classes in your automation code, you might want to ask yourself the question whether you’re not overengineering things…
A common example of people using abstraction in their automation code (but not implementing it themselves) is the WebDriver
interface in Selenium. The fact that in your code, you can do
and
allowing you to create tests that run against different browsers without having to juggle different driver objects is a demonstration of the power of abstraction.
I would love to see some examples that prove me wrong and that show that defining and using interfaces or abstract classes in your automation code is a good idea. If you do have examples where abstraction was really useful in your automation code, please send them my way and I’ll happily change my mind.
"