Abstraction is the idea of dividing a class into several parts, to be able to share (via a parameter passed in a method for example) only one of these parts, rather than the object entirely. To understand the utility of this principle and how to apply it, take for example a class Human
since this is something that we know well.
class Human {
int air;
int satiety;
float energy;
void breathe() {
air += 10;
}
void eat() {
satiety += 5;
}
void move() {
System.out.println("A human wants to move!");
}
void consumeEnergy(float value) {
this.energy -= value;
}
}
Now, imagine that we have a class Hiking
that allows us to organise a walk. Something like:
class Hiking {
List<Human> participants;
public Hiking() { this.participants = new ArrayList<>(); }
void addParticipant(Human participant) { this.participants.add(participant); }
void moveAll() {
for(Human human : participants) {
human.move();
human.consumeEnergy(3);
}
}
}
So far so good, we have what we want. This code works perfectly well, except that, in reality, it contains two major problems.
- We restraint the hiking of the objects of type Human, whereas anything that can walk and be exhausted could take a hike. It’s not really the kind to forbid to elephants and to tractors to participate :(
- We ask for a whole human but all we need is their legs (hum). This hiking class however currently has access to the human breathing system (via
breathe()
) and to his stomach (viaeat()
)! So, we give too much power to this class, since it is able to do things that it shouldn’t be able to do.
So, how do we get around these two problems?
Interfaces come to our rescue (interfaces are great). Let's make a Walking
interface, which allows describing something that moves and gets tired:
interface Walking {
void move();
void consumeEnergy(float value);
}
So now, our class Human
will be able to implement this awesome interface, and all that's to do is to add implements Walking
and put a little @Override
on our move
and consumeEnergy
methods, and that's it, since this is a behavior that we had already implemented beforehand, only without the interface.
class Human implements Walking {
int air;
int satiety;
float energy;
void breathe() {
air += 10;
}
void eat() {
satiety += 5;
}
@Override
void move() {
System.out.println("A human wants to move!");
}
@Override
void consumeEnergy(float value) {
this.energy -= value;
}
}
But, what has changed? Well now, instead of making a Human list in our Hiking class, we will do a Walking list (it’s more politically correct). And, there is almost nothing to change, which is a good sign:
class Hiking {
List<Walking> participants;
public Hiking() { this.participants = new ArrayList<>(); }
void addParticipant(Walking participant) { this.participants.add(participant); }
void moveAll() {
for(Walking walking: participants) {
walking.move();
walking.consumeEnergy(3);
}
}
}
And, henceforth, not only doesn't our Hiking class have access to the Human methods breathe
and eat
, since, we never specified that it's humans we're working with (think about it, if hiking could make you swallow peas, it would be creepy, right?), but also, it accepts elephants and tractors!
But that would assuming that they implement Walking
too… and, who knows how to make a tractor tired.
By making an abstraction to our Human class, we allowed two very important yet opposite things:
- An access restriction, since
Hiking
doesn’t have access to the functions that are specific to theHuman
class - An ease of access, since
Hiking
accepts henceforth every object whose type implementsWalking
, instead of being limited to humans.
For more information on the [SOLID] (https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)) principle called Dependency Inversion (which is mainly based on abstraction), I invite you to read the Wikipedia article related to this topic.