Let's explore the relationship between concrete class, abstract class, and interfaces.
Suppose we would like to create a representation of a laptop.
class Laptop {
String screen;
int batteryHour;
Laptop(String screen, int batteryHour) {
this.screen = screen;
this.batteryHour = batteryHour;
}
void moveCursor(){
System.out.println("Using the trackpad to move the cursor");
}
}
The Laptop
class contains a constructor that takes in a string representing the resolution of its screen and an integer representing the hours that its battery can last. It also has a moveCursor
method for illustration purposes later on.
Laptop ex1 = new Laptop("1080P",5);
Laptop ex2 = new Laptop("4k", 8);
A typical issue we often face is that sometimes we need to create something related to the existing objects, like a desktop in this case.
Given that a desktop also has a screen, we can simply and intuitively make our laptop class the parent of the desktop class, like so:
class Desktop extends Laptop {
Desktop(String screen, int batteryHour){
super(screen, batteryHour);
}
}
If you read the book "Thinking Fast And Slow", you will know that system 1, or the lazy, quick response system of our brain, often make mistakes.
This is one such instance. So, DO NOT in any circumstances, subclass an object simply because they share some properties. There are many reasons why the above solution might violate the Liskov Substitution Principle (LSP). One obvious point is that the batteryHour
does not make sense to a Desktop
.
Both laptops and desktops are computers. They share a lot of similarities. However, you would agree with me that we do not typically say that a desktop is a laptop or vice versa. This is a tell-tale sign that they are not suitable for a parent-child relationship.
Concrete Class
Knowing that they are both computers leads us to the first valid design:
// Have Computer as a Concrete parent class
class Computer {
String screen;
void moveCursor(){
System.out.println("moving the cursor?");
}
}
class Laptop extends Computer {
int batteryHour;
Laptop(String screen, int batteryHour) {
super.screen = screen;
this.batteryHour = batteryHour;
}
@Override
void moveCursor(){
System.out.println("Using the trackpad to move the cursor");
}
}
class Desktop extends Computer {
Desktop(String screen){
super.screen = screen;
}
@Override
void moveCursor(){
System.out.println("Using the mouse to move the cursor");
}
}
Observations
- Define a
Computer
class as the parent of these two child class - For properties and methods that are common, they should be extracted into the parent class.
- Methods can be shared among the children, and by including them in the parent class, we make sure that all its children will have those methods. However, we may or may not be able to fill in the implementation of a method accurately at the parent class level. This means each child class will need to override this method to include its own implementation. Note that this behavior is not enforced by the compiler, meaning forgetting to override a method when you meant to do so will result in an unexpected runtime error. For example, desktop users typically do not have/use a trackpad to control their cursor. Therefore,
moveCursor
method has to be overridden at the child class level forDesktop
. Yet, to omit the overriding is perfectly acceptable behavior to the compiler. - It is also possible to instantiate the parent class since it is concrete. But, it may not make sense to do so. In our case, if we call
new Computer().moveCursor()
, the output may not be meaningful.
Abstract Class
The second approach is to create Computer
as an abstract class.
abstract class Computer {
String screen;
abstract void moveCursor();
}
class Laptop extends Computer {
int batteryHour;
Laptop(String screen, int batteryHour) {
super.screen = screen;
this.batteryHour = batteryHour;
}
@Override
void moveCursor(){
System.out.println("Using the trackpad to move the cursor");
}
}
class Desktop extends Computer {
Desktop(String screen){
super.screen = screen;
}
@Override
void moveCursor(){
System.out.println("Using the mouse to move the cursor");
}
}
Abstract class
- Cannot be instantiated
- Contains abstract methods that do not have implementation details and must be implemented by its subclass, will be enforced by the compiler
- Can contain properties
- Can contain concrete methods with implementations as well
This pretty much solves the few issues we have with concrete class. If the parent class does not need to be instantiated, it will be safe to say that letting the parent class be abstract is usually better. Now, the drawback of the second approach, at least for Java, is that a class can only inherit from one parent class.
Let say you want to have a Chargeable
class for objects that can be charged:
abstract class Chargeable {
abstract Chargeable charge();
}
Then you are faced with a dilemma. Given that a class can only subclass one parent class, which one to choose?
Side note on multiple inheritance
- You may want to inherit all methods of different parents, but not all properties.
- Will
PineapplePen
be a logical object to you?
class PineapplePen extends Pineapple, Pen{
//...
}
Interfaces
The next option in line is interfaces:
abstract class Computer {
String screen;
abstract void moveCursor();
}
interface Chargeable {
void charge();
}
class Laptop extends Computer implements Chargeable {
int batteryHour;
Laptop(String screen, int batteryHour) {
super.screen = screen;
this.batteryHour = batteryHour;
}
@Override
void moveCursor(){
System.out.println("Using the trackpad to move the cursor");
}
@Override
public void charge(){
// pretend that it does something
System.out.println("Charging the laptop");
}
}
class Desktop extends Computer implements Chargeable {
Desktop(String screen){
this.screen = screen;
}
@Override
void moveCursor(){
System.out.println("Using the mouse to move the cursor");
}
@Override
public void charge(){
// pretend that it does something
System.out.println("Providing power to the desktop");
}
}
Interfaces have the following characteristics:
- Methods are implicitly abstract and public
- Cannot be instantiated
- Only constant variables allowed (static final) within interfaces
- Interface can extend multiple interfaces
- A class can implement multiple interfaces
In the final code snippet, there is a combination of concrete class, abstract class, and interface at work. There is no dominance of one over the other. Rather, exploring situations in which one of them will be most helpful will provide us the knowledge to weigh our options and choose the right one when designing our program.
Summary
Concrete Class | Abstract Class | Interface | |
---|---|---|---|
Implementation | Actual | Actual Abstract |
Abstract |
Instantiatable | Yes | No | No |
Till next time...
Top comments (0)