DEV Community

Chukwuma Anyadike
Chukwuma Anyadike

Posted on • Updated on

Object Oriented Programming: The Four Pillars

In the professional world of tech, there is an increasing demand for programmers adept in object oriented programming (OOP). OOP means that real world entities are represented by objects. Objects consist of state (fields or variables) and behaviors (methods). Classes are blueprints for these objects and are used to create or instantiate objects. OOP languages include but are definitely not limited to Java, C++/C#, Ruby and Python. In our examples today we will be using Java. This leads up to the four pillars are abstraction, encapsulation, inheritance, and polymorphism. These build on each other.

The four pillars of OOP

Table of Contents

Abstraction
Encapsulation
Inheritance
Polymorphism
Recap

1) Abstraction

To abstract something away means to hide away the implementation details. For example, we know that cars take us from point A to point B but we do not know nor need to know exactly how a car engine works. The two main ways of doing this is by using interfaces and abstract classes.

Let's start with abstract classes. An abstract class is created using the abstract keyword. An abstract class can have zero or more abstract methods. In a class a method is declared abstract using the abstract keyword. Now, we will build an abstract doctor class.

public abstract class DoctorAbstractClass {
    abstract void printNameAndTitle(String firstName, String lastName, String title);
    abstract void printGreeting(String firstName, String lastName, String title);    
}
Enter fullscreen mode Exit fullscreen mode

Note that there are two abstract methods without a method body. This is known as a method stub. Any class extending (inheriting) an abstract class MUST implement ALL of these abstract methods (unless the the child class is abstract).

Let's build a doctor interface with two abstract methods. An interface is created using the interface keyword. Note that unless otherwise specified any method defined in an interface is public and abstract.

public interface DoctorInterface {
    void printNameAndTitle(String firstName, String lastName, String title);
    void printGreeting(String firstName, String lastName, String title);
}
Enter fullscreen mode Exit fullscreen mode

Again, note that there are two abstract methods (method stubs). Any concrete class extending an abstract class MUST implement ALL of these abstract methods.

Note: Abstract classes cannot be instantiated. One cannot create objects from abstract classes.

Now let's see what abstraction looks like using both interfaces and abstract classes.

Using our abstract class below. Note the use of the extends keyword.

public class Doctor extends DoctorAbstractClass {
    public void printNameAndTitle(String firstName, String lastName, String title){
        System.out.println(firstName + " " + lastName + " " + title);
    }
    public void printGreeting(String firstName, String lastName, String title){
        System.out.println("My name is " + firstName + " " + lastName + " " + title + ".  How may I help you?");
    }    
}
Enter fullscreen mode Exit fullscreen mode

Using our interface. Note the implements keyword.

public class Doctor implements DoctorInterface {
    public void printNameAndTitle(String firstName, String lastName, String title){
        System.out.println(firstName + " " + lastName + " " + title);
    }
    public void printGreeting(String firstName, String lastName, String title){
        System.out.println("My name is " + firstName + " " + lastName + " " + title + ".  How may I help you?");
    }    
}
Enter fullscreen mode Exit fullscreen mode

Now, we will create a Main class to use these two classes.

public class Main {
  public static void main(String[] args) {
    System.out.println("This is abstraction using an abstract class");
    DoctorChildClass doctorFromAbstractClass = new DoctorChildClass();
    doctorFromAbstractClass.printNameAndTitle("Alfred", "Whipple", "M.D.");
    doctorFromAbstractClass.printGreeting("Alfred", "Whipple", "M.D.");

    System.out.println();
    System.out.println("This is abstraction using an interface");
    Doctor doctorFromInterface = new Doctor();
    doctorFromInterface.printNameAndTitle("Thomas", "DeBakey", "M.D.");
    doctorFromInterface.printGreeting("Thomas", "DeBakey", "M.D.");
  }
}
Enter fullscreen mode Exit fullscreen mode

Now run this code. Our output is as follows.

This is abstraction using an abstract class
Alfred Whipple M.D.
My name is Alfred Whipple M.D..  How may I help you?

This is abstraction using an interface
Thomas DeBakey M.D.
My name is Thomas DeBakey M.D..  How may I help you?
Enter fullscreen mode Exit fullscreen mode

As we can see abstraction (hiding implementation details) can be achieved using interfaces and abstract classes.

2) Encapsulation

The definition of encapsulation is "the action of enclosing something in or as if in a capsule". Removing access to parts of your code and making things private is exactly what encapsulation is all about (aka data hiding). In Java, this is done by making the fields in the class private and the use of getter and setter methods. In this way outside classes cannot directly access data inside a class. It is best practice to construct your classes this way.

Encapsulation

Here is an example of encapsulation using private fields and getter and setter methods. Here is the Profile class.

public class Profile {

    //these fields are made private to prevent direct access to data
    private String firstName;
    private String lastName;
    private int age;
    private String email;

    public Profile(String firstName, String lastName, int age, String email){
        setFirstName(firstName);
        setLastName(lastName);
        setAge(age);
        setEmail(email);
    }

    //setter methods
    public void setFirstName(String firstName){
        this.firstName = firstName;
    }
    public void setLastName(String lastName){
        this.lastName = lastName;
    }   
    public void setAge(int age){
        this.age = age;
    }    
    public void setEmail(String email){
        this.email = email;
    }    

    //getter methods
    public String getFirstName(){
        return this.firstName;
    }
    public String getLastName(){
        return this.lastName;
    }   
    public int getAge(){
        return this.age;
    }    
    public String getEmail(){
        return this.email;
    }    
}
Enter fullscreen mode Exit fullscreen mode

Here is a Main class to execute some code demonstrating the use of getter and setter methods.

public class Main {
    public static void main(String[] args){
        Profile billie = new Profile("Billie", "Jean", 37, "billie@gmail.com");
        System.out.println("This is after instantiation.");
        System.out.println("First name: " + billie.getFirstName());
        System.out.println("Last name: " + billie.getLastName());
        System.out.println("Age: " + billie.getAge());
        System.out.println("Email address: " + billie.getEmail());

        billie.setAge(38);
        billie.setLastName("Gene");
        System.out.println();
        System.out.println("After changing age and last name.");
        System.out.println("First name: " + billie.getFirstName());
        System.out.println("Last name: " + billie.getLastName());
        System.out.println("Age: " + billie.getAge());
        System.out.println("Email address: " + billie.getEmail());
    }
}
Enter fullscreen mode Exit fullscreen mode

Now when we run this code the output is as follows.

This is after instantiation.
First name: Billie
Last name: Jean
Age: 37
Email address: billie@gmail.com

After changing age and last name.
First name: Billie
Last name: Gene
Age: 38
Email address: billie@gmail.com
Enter fullscreen mode Exit fullscreen mode

However, if I try to access a field directly then, we get an error.

public class Main {
    //rest of class code

        billie.age;
    }
}

Output:
The field Profile.age is not visible
Enter fullscreen mode Exit fullscreen mode

This expected since age is now a private variable within the Profile class.

3) Inheritance

Inheritance allows an object to acquire the properties and methods of a parent object. This allows new classes (sub classes or child classes) to be created on top of existing classes (super classes or parent classes) in which the existing properties and methods can be reused. As already mentioned the extends keyword is used when a child class inherits from a parent class.

There are four types of inheritance (see diagram below):

  1. Single level: subclasses inherit the features of one superclass
  2. Multiple level: a derived class will be inheriting a base class, and as well as the derived class also acts as the base class for other classes.
  3. Hierarchical: one class serves as a superclass (base class) for more than one subclass
  4. Multiple inheritance: not found in Java but can be found in Python and C++

Types of Inheritance

Start with a base class called Hero which contains named states and behaviors below.

public class Hero {
    String name, alterEgo, powers, weapons;

    public Hero (String name, String alterEgo, String powers, String weapons){
        this.name = name;
        this.alterEgo = alterEgo;
        this.powers = powers;
        this.weapons = weapons;
    }

    public String generateProfile (){
        return "Code name: " + name + 
            "\n Alter ego: " + alterEgo + 
            "\n Powers: " + powers + 
            "\n Weapons: " + weapons;
    }

    public void printProfile (){
        System.out.println(this.generateProfile());
    }
}
Enter fullscreen mode Exit fullscreen mode

Single level inheritance: MutantHero inherits (extends) Hero.

Note: since the constructor of the parent class takes 4 arguments, each child class needs its own constructor. Then the super keyword is used to call the parent constructor. If there is no need to take arguments in the child constructor then the default no-args (no arguments) constructor is implicitly called without having to explicitly specify a constructor.

public class MutantHero extends Hero {

    public MutantHero(String name, String alterEgo, String powers, String weapons) {
        super(name, alterEgo, powers, weapons);
    }

}
Enter fullscreen mode Exit fullscreen mode

Multiple level inheritance: TelepathHero extends MutantHero (which itself extends Hero).

public class TelepathHero extends MutantHero{

    public TelepathHero(String name, String alterEgo, String powers, String weapons) {
        super(name, alterEgo, powers, weapons);
    }

}
Enter fullscreen mode Exit fullscreen mode

Hierarchical inheritance: SpeedsterHero (in addition to MutantHero) extends Hero.

public class SpeedsterHero extends Hero{

    public SpeedsterHero(String name, String alterEgo, String powers, String weapons) {
        super(name, alterEgo, powers, weapons);
    }

}
Enter fullscreen mode Exit fullscreen mode

Now let's utilize our main method to create instances of these classes and demonstrate that they all exhibit shared behavior.

public class Main {
    public static void main(String[] args) {
        Hero batman = new Hero("Batman", "BruceWayne", "I'm rich", "Utility belt, bullet proof bat-suit");
        MutantHero wolverine = new MutantHero("Wolverine", "Logan", "Healing Factor", "retractable Adamantium claws");
        SpeedsterHero flash = new SpeedsterHero("The Flash", "Barry Allen", "Super speed far exceeding that of light", "Electrokinesis");
        TelepathHero professorX = new TelepathHero("Professor X", "Charles Xavier", "Telepathy", "Psychic blast");

        batman.printProfile();
        wolverine.printProfile();
        flash.printProfile();
        professorX.printProfile();
    }
}

Output:
Code name: Batman
 Alter ego: BruceWayne
 Powers: I'm rich
 Weapons: Utility belt, bullet proof bat-suit
Code name: Wolverine
 Alter ego: Logan
 Powers: Healing Factor
 Weapons: retractable Adamantium claws
Code name: The Flash
 Alter ego: Barry Allen
 Powers: Super speed far exceeding that of light
 Weapons: Electrokinesis
Code name: Professor X
 Alter ego: Charles Xavier
 Powers: Telepathy
 Weapons: Psychic blast
Enter fullscreen mode Exit fullscreen mode

Here we were able to create multiple child classes which retain the state and behaviors of the parent class without having to write duplicate code by the use of inheritance.

4) Polymorphism

Polymorphism means "the condition of occurring in several different forms." It allows objects to behave differently in different contexts.

There are two main types of polymorphism:

  1. Static (Compile time) polymorphism: characterized by method overloading in which the method signature varies due to the number, data types, or order of parameters. This occurs within the same class.
  2. Dynamic (Runtime) polymorphism: characterized by method overriding in which the implementation (method body) of the method varies. This occurs between parent and child classes (involves inheritance).

Static vs Dynamic Polymorphism

Method Overloading with Hero Class (Static polymorphism)

public class Hero {
    String name, alterEgo, powers, weapons;

    public Hero (String name, String alterEgo, String powers, String weapons){
        this.name = name;
        this.alterEgo = alterEgo;
        this.powers = powers;
        this.weapons = weapons;
    }

    public String generateProfile (){
        return "Code name: " + name + 
            "\n Alter ego: " + alterEgo + 
            "\n Powers: " + powers + 
            "\n Weapons: " + weapons;
    }

    //Overloaded method
    public String generateProfile(String catchPhrase){
        return "Code name: " + name + 
            "\n Alter ego: " + alterEgo + 
            "\n Powers: " + powers + 
            "\n Weapons: " + weapons +
            "\n Catch Phrase: " + catchPhrase;
    }

    public String addPowers (String power){
        return powers + " " + power;
    }
}
Enter fullscreen mode Exit fullscreen mode

Method overriding in child class SuperHero (Dynamic polymorphism)

public class SuperHero extends Hero{

    public SuperHero(String name, String alterEgo, String powers, String weapons) {
        super(name, alterEgo, powers, weapons);
    }

    //Overridden method
    @Override 
    public String generateProfile(String catchPhrase){
        String profile = "Code name: " + name + 
            "\n Alter ego: " + alterEgo + 
            "\n Powers: " + powers + 
            "\n Weapons: " + weapons +
            "\n Catch Phrase: " + catchPhrase;
        System.out.println(profile);
        return profile;
    }

}
Enter fullscreen mode Exit fullscreen mode

Now lets run some code and put this together.

public class Main {
    static public void main (String[] args){
        Hero batman = new Hero("Batman", "Bruce Wayne", "I'm rich", "Utility belt, bullet proof bat-suit");
        System.out.println("Batman instance method with no arguments");
        System.out.println(batman.generateProfile());
        System.out.println();
        System.out.println("Batman instance method with one argument.  This is method overloading");
        System.out.println(batman.generateProfile("I am vengeance, I'm Batman!"));

        System.out.println("==========================================================");
        SuperHero superman = new SuperHero("Superman", "Clark Kent", "Flight, Super strength, Invulnerability", "Heat vision");

        System.out.println("Superman instance with overridden method from parent class which prints and returns a value");
        superman.generateProfile("I believe in justice");
    }
}
Enter fullscreen mode Exit fullscreen mode

The output

Batman instance method with no arguments
Code name: Batman
 Alter ego: Bruce Wayne
 Powers: I'm rich
 Weapons: Utility belt, bullet proof bat-suit

Batman instance method with one argument.  This is method overloading
Code name: Batman
 Alter ego: Bruce Wayne
 Powers: I'm rich
 Weapons: Utility belt, bullet proof bat-suit
 Catch Phrase: I am vengeance, I'm Batman!
==========================================================
Superman instance with overridden method from parent class which prints and returns a value
Code name: Superman
 Alter ego: Clark Kent
 Powers: Flight, Super strength, Invulnerability
 Weapons: Heat vision
 Catch Phrase: I believe in justice
Enter fullscreen mode Exit fullscreen mode

Note that these objects are executing the same methods with different results depending on the context. This gives flexibility to our programs.

Recap:

  1. Abstraction allows the hiding of implementation details by implementing interfaces and extending abstract classes.
  2. Encapsulation is data hiding using private fields with public getter and setter methods.
  3. Inheritance allows child classes to inherit variables and methods by extending parent classes.
  4. Polymorphism allow objects to execute methods which produce different results in different contexts through method overloading and method overriding.

The four pillars of OOP, abstraction, encapsulation, inheritance, and polymorphism allow us to write code that does some amazing and powerful things. It adds flexibility and modularity to our programs. Now keep on coding on. See ya!

Top comments (0)