JavaRush /Java Blog /Random EN /Coffee break #90. 4 Pillars of Object Oriented Programmin...

Coffee break #90. 4 Pillars of Object Oriented Programming

Published in the Random EN group
Source: The Geek Asian Let's take a look at the four fundamentals of object-oriented programming and try to understand how they work. Object-oriented programming (OOP) is one of the main programming paradigms. It can be easy and simple or, on the contrary, very complex. It all depends on how you decide to develop your application. Coffee break #90.  4 pillars of object-oriented programming - 1There are 4 pillars of OOP:
  1. Encapsulation.
  2. Inheritance.
  3. Abstraction.
  4. Polymorphism.
We will now discuss each of them with a brief explanation and a real code example.

1. Encapsulation

We've all studied encapsulation as hiding data elements and allowing users to access data using public methods. We call these getters and setters. Now let's forget about this and find a simpler definition. Encapsulation is a method of restricting the user from directly changing data members or class variables to maintain data integrity. How do we do this? We restrict access to variables by switching the access modifier to private and exposing public methods that can be used to access data. Let's look at specific examples below. This will help us understand how we can use encapsulation to maintain data integrity. Without encapsulation:
/**
 * @author thegeekyasian.com
 */
public class Account {

  public double balance;

  public static void main(String[] args) {

  	Account theGeekyAsianAccount = new Account();

  	theGeekyAsianAccount.balance = -54;
  }
}
In the code snippet above, the main() method accesses the balance variable directly. This allows the user to set any double value to the balance variable of the Account class . We can lose data integrity by allowing anyone to set balance to any invalid number, such as -54 in this case. With encapsulation:
/**
 * @author thegeekyasian.com
 */
public class Account {

  private double balance;

  public void setBalance(double balance) {

    if(balance >= 0) { // Validating input data in order to maintain data integrity
	  this.balance = balance;
    }

    throw new IllegalArgumentException("Balance cannot be less than zero (0)");
  }

  public static void main(String[] args) {

  	Account theGeekyAsianAccount = new Account();

  	theGeekyAsianAccount.setBalance(1); // Valid input - Allowed
  	theGeekyAsianAccount.setBalance(-55); // Stops user and throws exception
  }
}
In this code, we have restricted access to the balance variable and added a setBalance() method that allows users to set the balance value for Account . The setter checks the provided value before assigning it to the variable. If the value is less than zero, an exception is thrown. This ensures that the integrity of the data is not compromised. After explaining the above examples, I hope the value of encapsulation as one of the four pillars of OOP is clear.

2. Inheritance

Inheritance is a method of obtaining properties of another class that share common features. This allows us to increase reusability and reduce code duplication. The method also has the principle of child-parent interaction, when a child element inherits the properties of its parent. Let's dive into two quick examples and see how inheritance makes code simpler and more reusable. Without inheritance:
/**
 * @author thegeekyasian
 */
public class Rectangle {

  private int width;
  private int height;

  public Rectangle(int width, int height) {
	this.width = width;
	this.height = height;
  }

  public int getArea() {
	return width * height;
  }
}

public class Square {

  private int width; // Duplicate property, also used in class Rectangle

  public Square(int width) {
	this.width = width;
  }

  public int getArea() { // Duplicate method, similar to the class Rectangle
	return this.width * this.width;
  }
}
The two similar classes share the width properties and the getArea() method . We can increase code reuse by doing a little refactoring where the Square class ends up inheriting from the Rectangle class . With inheritance:
/**
 * @author thegeekyasian
 */
public class Rectangle {

  private int width;
  private int height;

  public Rectangle(int width, int height) {
	this.width = width;
	this.height = height;
  }

  public int getArea() {
	return width * height;
  }
}

public class Square extends Rectangle {

  public Square(int width) {
	super(width, width); // A rectangle with the same height as width is a square
  }
}
By simply extending the Rectangle class , we get the Square class as a Rectangle type . This means that it inherits all the properties common to Square and Rectangle . In the examples above, we see how inheritance plays an important role in making code reusable. It also allows a class to inherit the behavior of its parent class.

3. Abstraction

Abstraction is a technique of presenting only essential details to the user by hiding unnecessary or irrelevant details of an object. It helps reduce operational complexity on the user side. Abstraction allows us to provide a simple interface to the user without asking for complex details to perform an action. Simply put, it gives the user the ability to drive a car without requiring them to understand exactly how the engine works. Let's look at an example first and then discuss how abstraction helps us.
/**
* @author thegeekyasian.com
*/
public class Car {

  public void lock() {}
  public void unlock() {}

  public void startCar() {

	checkFuel();
	checkBattery();
	whatHappensWhenTheCarStarts();
  }

  private void checkFuel() {
	// Check fuel level
  }

  private void checkBattery() {
	// Check car battery
  }

  private void whatHappensWhenTheCarStarts() {
	// Magic happens here
  }
}
In the above code, the lock() , unlock() and startCar() methods are public and the rest are private to the class. We have made it easier for the user to “drive the car.” Of course, he could manually check checkFuel() and checkBattery() before starting the car with startCar() , but that would just complicate the process. With the above code, all the user needs to do is use startCar() and the class will take care of the rest. This is what we call abstraction.

4. Polymorphism

The last and most important of the four pillars of OOP is polymorphism. Polymorphism means “many forms.” As the name suggests, it is a function that allows you to perform an action in multiple or different ways. When we talk about polymorphism, there isn't much to discuss unless we talk about its types. There are two types of polymorphism:
  1. Method overloading - static polymorphism (Static Binding).
  2. Method overriding - dynamic polymorphism (Dynamic Binding).
Let's discuss each of these types and see what the difference is between them.

Method overloading - static polymorphism:

Method overloading or static polymorphism, also known as Static Binding or compile-time binding, is a type in which method calls are determined at compile time. Method overloading allows us to have multiple methods with the same name, having different parameter data types, or different numbers of parameters, or both. But the question is, why is method overloading (or static polymorphism) useful? Let's look at the below examples to better understand method overloading. Without method overloading:
/**
* @author thegeekyasian.com
*/
public class Number {

  public void sumInt(int a, int b) {
	System.out.println("Sum: " + (a + b));
  }

  public void sumDouble(double a, double b) {
	System.out.println("Sum: " + (a + b));
  }

  public static void main(String[] args) {

	Number number = new Number();

	number.sumInt(1, 2);
	number.sumDouble(1.8, 2.5);
  }
}
In the above example, we created two methods with different names, just to add two different types of numbers. If we continue with a similar implementation, we will have multiple methods with different names. This will reduce the quality and availability of the code. To improve this, we can use method overloading by using the same name for different methods. This will allow the user to have one option as an entry point for summing different types of numbers. Method overloading works when two or more methods have the same name but different parameters. The return type can be the same or different. But if two methods have the same name, the same parameters, but different return types, then this will cause overloading and a compilation error! With method overloading:
/**
* @author thegeekyasian.com
*/
public class Number {

  public void sum(int a, int b) {
	System.out.println("Sum: " + (a + b));
  }

  public void sum(double a, double b) {
	System.out.println("Sum: " + (a + b));
  }

  public static void main(String[] args) {

	Number number = new Number();

	number.sum(1, 2);
	number.sum(1.8, 2.5);
  }
}
In the same code, with a few minor changes, we were able to overload both methods, making the names the same for both. The user can now specify their specific data types as method parameters. It will then perform an action based on the data type it is provided. This method binding is done at compile time because the compiler knows which method will be called with the specified parameter type. That's why we call it compile-time binding.

Method overriding - dynamic polymorphism:

Unlike method overloading, method overriding allows you to have exactly the same signature as multiple methods, but they must be in multiple different classes. The question is, what's so special about it? These classes have an IS-A relationship, that is, they must inherit from each other. In other words, in method overriding or dynamic polymorphism, methods are dynamically processed at runtime when the method is called. This is done based on the reference to the object with which it is initialized. Here is a small example of method overriding:
/**
* @author thegeekyasian.com
*/
public class Animal {

  public void walk() {
	System.out.println("Animal walks");
  }
}

public class Cat extends Animal {

  @Override
  public void walk() {
	System.out.println("Cat walks");
  }
}

public class Dog extends Animal {

  @Override
  public void walk() {
	System.out.println("Dog walks");
  }
}

public class Main {

  public static void main(String[] args) {

	Animal animal = new Animal();
	animal.walk(); // Animal walks

	Cat cat = new Cat();
	cat.walk(); // Cat walks

	Dog dog = new Dog();
	dog.walk(); // Dog walks

	Animal animalCat = new Cat(); // Dynamic Polymorphism
	animalCat.walk(); // Cat walks

	Animal animalDog = new Dog(); // Dynamic Polymorphism
	animalDog.walk(); //Dog walks
  }
}
In this overriding example, we dynamically assigned objects of type “Dog” and “Cat” to type “Animal”. This allows us to call the walk() method on referenced instances dynamically at runtime. We can do this using method overriding (or dynamic polymorphism). This concludes our brief discussion on the four pillars of OOP and I hope you find it useful.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION