DEV Community

Hodeem
Hodeem

Posted on • Updated on

Abstraction, Encapsulation & Inheritance

Hopefully, these explanations make the principles easier to understand. In this article, I'll tackle Abstraction, Encapsulation and Inheritance and illustrate them with examples written in Java.

Polymorphism deserves an article for itself, so I'll publish that at a later date.

Let's dive in.

Abstraction

When you're driving a car and you want it to go faster, what do you do? You "step on the gas". In other words, you step on the gas pedal and expect the car to accelerate.

If you want to go slower, you step on the brake pedal so that the car will decelerate.

Most likely, you do this without stopping to think about what's happening behind the scenes. You don't need to know the inner workings of the throttle valve, the engine or the braking system when you want the car to go faster or slower.

This emphasis on the essential details is the beauty of Abstraction. It allows you to focus more on the idea and less on how things get done ie. the implementation.

Once you understand Abstraction, you will start to see it all around you.

Two benefits of Abstraction

  1. It reduces complexity by only presenting the necessary level of detail.
  2. Abstraction gives us the flexibility to change the implementation without disturbing the interface (the part that the user/client interacts with).

One example of the second benefit is Tesla's Over-The-Air (OTA) updates which allow the company to improve how their vehicles operate without significantly changing how their owners interact with the vehicle.

Two drawbacks of Abstraction

  1. Too much Abstraction can make the code bulky.
  2. Poor Abstraction can make the code confusing or misleading. Imagine flipping a light switch that turns on the light, but water also comes pouring out of your garden hose. This would violate your expectations.

Abstraction example in Java

Two popular ways of accomplishing Abstraction in Java is through the use of abstract classes or interfaces.

Let's use an abstract Car class to illustrate this principle.

abstract class Car {}
Enter fullscreen mode Exit fullscreen mode

Without needing to go into the engineering details, we know that we expect a Car to be able to accelerate and decelerate.

Therefore, we can translate these expectations into abstract methods.

    abstract class Car {
        protected abstract void accelerate();
        protected abstract void decelerate();
    }
Enter fullscreen mode Exit fullscreen mode

Even though we can create different Cars based on this abstract class, each type must possess the ability to accelerate() and decelerate().

How each car decides to accelerate and decelerate is its business.

    abstract class Car {
        protected abstract void accelerate();
        protected abstract void decelerate();
    }

    public class HondaCRV extends Car {
        public void accelerate();
            //Go faster
        }

        public void decelerate() {
            //Go slower
        }
    }
Enter fullscreen mode Exit fullscreen mode

Encapsulation often gets confused with Abstraction because they both seem to be "hiding something".

Encapsulation is mainly about two things:

  1. Keeping data and the code that operates on that data all in one place ("capsule"), and
  2. Restricting access to that data.

When you think about it, it makes sense. Why not keep everything that's closely related together?
It's not much different from keeping all your books on the bookshelf, or all the dishes in the kitchen.

If done properly, then the data is treated like the special plates and glasses in the kitchen that only get brought out when guests come over.
The only way you can access them is by going through the "proper channels", or risk an ass-whooping.

In the same way, encapsulated classes make their data private and specify the getters and setters which must be used to access the private data.

Instead of simply modifying the private data, setters can perform checks to ensure that the data is being set to a valid value.

Or they can perform operations to ensure that all the data are kept up to date with each change.

If you really want to take it up a notch, the getters and setters are both optional. This means that you can make the class read-only or write-only.

On the flip-side, if there was unrestricted access to these data members, then keeping the data valid would be the responsibility of error-prone software developers.

Two benefits of Encapsulation

  1. Better organization and readability of the code.
  2. Enhanced security of the data in a class.

One drawback of Encapsulation

  1. The size of the code may increase as getters or setters (or both) may be added to the data members/fields.

Encapsulation example in Java

Let's continue with the special plates and glasses example to show Encapsulation in action.

First, we declare the fields and initialize them to an arbitrary quantity:

public class Kitchen {
    private int plates = 10;
    private int glasses = 10;
}
Enter fullscreen mode Exit fullscreen mode

We can add an additional field to indicate whether or not permission is granted to remove the plates or glasses from the kitchen.

public class Kitchen {
    private int plates = 10;
    private int glasses = 10;

    private boolean hasPermission = false;

    public void setPermission(boolean isPermitted){
        this.hasPermission = isPermitted;
    }
}
Enter fullscreen mode Exit fullscreen mode

Okay, that's lovely so far. Now it's time to create a getter for removing plates from the Kitchen.

public class Kitchen {
    private int plates = 10;
    private int glasses = 10;

    private boolean hasPermission = false;

    public void setPermission(boolean isPermitted){
        this.hasPermission = isPermitted;
    }

    public void getPlates(int numberOfPlates) {
        if(hasPermission == true) {
            this.plates -= numberOfPlates;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If the getPlates method is called, it will first check to see if hasPermission is set to true.

If so, then it will subtract the number of plates specified from the total plate tally in the plates field.

In the code above, getPlates() is the only method with permission to alter the value of plates. If you want, you can add a setter too.

Inheritance

When you look at a group of objects, people or places you'll subconsciously detect if they are similar or not.

This is because we understand that similar things share common traits and/or behaviours while dissimilar things don't.

Most dogs bark, wag their tails and drive you crazy in the wee hours of the morning. Similarly, the average car has a gas pedal, a brake pedal and a steering wheel.

So, instead of re-writing every feature for every dog breed or car model, we can create a class or interface which captures the common elements.

The different varieties can now inherit these characteristics from that class or interface and make the necessary extensions or modifications.

This way, instead of writing every feature for each descendant, you can write the features once and use them many times.

Two benefits of Inheritance

  1. It reduces code size by eliminating redundancy.
  2. Changes that affect multiple classes can be made in one place.

Two drawbacks of Inheritance

  1. Descendants (sub-classes or sub-interfaces) are dependent on their ancestors which makes it harder to re-use the descendants.
  2. Changes to the ancestor will affect all of its descendants. This is the flip-side of the second benefit, so proceed with caution when practising this principle.

Inheritance example in Java

Let's start with a basic Canine class that has some of the bare minimum requirements.

class Canine {
    protected int numberOfLegs = 4; 
    protected boolean isCarnivore = true; 
}
Enter fullscreen mode Exit fullscreen mode

Now let's create two descendants for this ancestor.

One is called "GoldenRetriever" (because who doesn't like Golden Retrievers) and another is called "Wolf".

class Canine {
    protected int numberOfLegs = 4; 
    protected boolean isCarnivore = true; 
}

class GoldenRetriever extends Canine {
    protected final boolean isCute = true; 
}

class Wolf extends Canine {
    protected String homeland = "Winterfell"; 
}
Enter fullscreen mode Exit fullscreen mode

Both "GoldenRetriever" and "Wolf" sub-classes (descendants) will inherit the numberOfLegs and isCarnivore fields from their super-class(ancestor), the "Canine" class.

This benefit may seem trivial now, but on a larger scale, the effects of redundancy can become a nightmare.

Conclusion

I hope these explanations were easy enough to follow. My next post will cover the Polymorphism OOP principle.

Follow me on Twitter to get the heads up and thanks for reading.

If you liked this article, then please support me

Top comments (0)