To Understand the difference between these three design pattern, first we need to understand each one of them:
Inheritance
-
What is Inheritance?
Inheritance is one of the object-oriented programming concepts in Java. Inheritance enables the acquisition of data members and properties from one class to another.
Base Class (Parent Class)
The base class provides the data members and methods in alternative words. Any base class that needs methods or data members will have to borrow them from their respective parent or base class.-
Subclass (Child Class)
The subclass is also known as the child class. The implementation of its parent class recreates a new class, which is the child class.To inherit the parent class, a child class must include a keyword called "extends." The keyword "extends" enables the compiler to understand that the child class derives the functionalities and members of its parent class.
- Why Do We Need Inheritance?
We need use Inheritance for two main reason:
Run-Time Polymorphism
Runtime, also known as dynamic polymorphism, is a method call in the execution process that overrides a different method in the run time.Code Reusability
The process of inheritance involves reusing the methods and data members defined in the parent class. Inheritance eliminates the need to write the same code in the child class—saving time as a result.
- Example here is the class diagram:
The code:
package org.designpattern;
public class Main {
public static void main(String[] args) {
// Create an array of Shape objects
Shape[] shapes = new Shape[3];
shapes[0] = new Circle("Red", 5);
shapes[1] = new Rectangle("Blue", 4, 6);
shapes[2] = new Circle("Green", 3);
// Iterate through the shapes and demonstrate polymorphism
for (Shape shape : shapes) {
shape.displayColor();
System.out.println("Area: " + shape.calculateArea());
// Demonstrate instanceof and casting
if (shape instanceof Circle) {
System.out.println("This is a circle");
} else if (shape instanceof Rectangle) {
System.out.println("This is a rectangle");
}
System.out.println(); // For readability
}
}
}
/** Parent class
* this class should have at least one abstract method (in this cas it is calculateArea())
**/
abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// abstract method
public abstract double calculateArea();
public void displayColor() {
System.out.println("This shape is " + color);
}
}
// Child class Circle
class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
// Child class Rectangle
class Rectangle extends Shape {
private double length;
private double width;
public Rectangle(String color, double length, double width) {
super(color);
this.length = length;
this.width = width;
}
@Override
public double calculateArea() {
return length * width;
}
}
We have a class abstract which is the parent (in another base class) (NB: this class should have at least one method abstract).
What is composition
Composition in java is the design technique to implement has-a relationship in classes. We can use java inheritance or Object composition in java for code reuse.
Java composition is achieved by using instance variables that refers to other objects. For example, a House has a Room. Let’s see this with a java composition example code.
import java.util.ArrayList;
import java.util.List;
class House {
private List<Room> rooms;
public House() {
this.rooms = new ArrayList<>();
}
public void addRoom(String name, long width, long length, long height) {
Room room = new Room(name, width, length, height);
rooms.add(room);
}
public List<Room> getRooms() {
return rooms;
}
public void printHouseDetails() {
for (Room room : rooms) {
System.out.println("Room: " + room.getName());
System.out.println("Dimensions (WxLxH): " + room.getWidth() + " x " + room.getLength() + " x " + room.getHeight());
System.out.println();
}
}
}
class Room {
private String name;
private long width;
private long length;
private long height;
public Room(String name, long width, long length, long height) {
this.name = name;
this.width = width;
this.length = length;
this.height = height;
}
public String getName() {
return name;
}
public long getWidth() {
return width;
}
public long getLength() {
return length;
}
public long getHeight() {
return height;
}
}
public class Composition {
public static void main(String[] args) {
House house = new House();
house.addRoom("Living Room", 500, 600, 300);
house.addRoom("Bedroom", 400, 500, 300);
house.addRoom("Kitchen", 300, 400, 300);
house.printHouseDetails();
}
}
Why do we need Composition
Notice that above test program for composition in java is not affected by any change in the Room object. If you are looking for code reuse and the relationship between two classes is has-a then you should use composition rather than inheritance. Benefit of using composition in java is that we can control the visibility of other object to client classes and reuse only what we need. Also if there is any change in the other class implementation, for example getRooms returning list of Room, we need to change House class to accommodate it but client classes doesn’t need to change. Composition allows creation of back-end class when it’s needed.
types of composition:
One to One:
In a one-to-one composition, one object is composed of exactly one instance of another object. This implies a strong relationship where the composed object is a crucial part of the composing object.
Example: A Person and their Heart. Each person has exactly one heart, and the heart cannot exist independently of the person.
class Person {
private Heart heart;
public Person() {
this.heart = new Heart();
}
// getters and other methods
}
class Heart {
// Heart properties and methods
}
One to Many:
In a one-to-many composition, one object is composed of multiple instances of another object. The composed objects are part of the lifecycle of the composing object.
Example: A Library and Books. A library contains many books, but those books are part of the library's collection.
import java.util.ArrayList;
import java.util.List;
class Library {
private List<Book> books;
public Library() {
this.books = new ArrayList<>();
}
public void addBook(Book book) {
books.add(book);
}
// getters and other methods
}
class Book {
private String title;
public Book(String title) {
this.title = title;
}
// getters and other methods
}
Many to One:
In a many-to-one composition, multiple objects are composed within one instance of another object. This often implies that the composed object has a shared dependency on a single instance of the other object.
Example: Several Employees working in a single Company. Each employee works for one company, and the company manages all its employees.
import java.util.ArrayList;
import java.util.List;
class Company {
private List<Employee> employees;
public Company() {
this.employees = new ArrayList<>();
}
public void addEmployee(Employee employee) {
employees.add(employee);
}
// getters and other methods
}
class Employee {
private String name;
public Employee(String name) {
this.name = name;
}
// getters and other methods
}
What is decorator
The decorator design pattern allows us to dynamically add functionality and behavior to an object without affecting the behavior of other existing objects in the same class.
We use inheritance or composition to extend the behavior of an object but this is done at compile time and its applicable to all the instances of the class. We can’t add any new functionality of remove any existing behavior at runtime - this is when Decorator pattern comes into picture. Suppose we want to implement different kinds of cars - we can create interface Car to define the assemble method and then we can have a Basic car, further more we can extend it to Sports car and Luxury Car. The implementation hierarchy will look like below image.
// Step 1: Define the Car interface
interface Car {
void assemble();
}
// Step 2: Create a basic implementation of Car
class BasicCar implements Car {
@Override
public void assemble() {
System.out.print("Basic Car.");
}
}
// Step 3: Create the CarDecorator class implementing the Car interface
class CarDecorator implements Car {
protected Car car;
public CarDecorator(Car car) {
this.car = car;
}
@Override
public void assemble() {
this.car.assemble(); // Delegates the call to the wrapped car object
}
}
// Step 4: Create specific decorators by extending CarDecorator
class SportsCar extends CarDecorator {
public SportsCar(Car car) {
super(car);
}
@Override
public void assemble() {
super.assemble();
System.out.print(" Adding features of a Sports Car.");
}
}
class LuxuryCar extends CarDecorator {
public LuxuryCar(Car car) {
super(car);
}
@Override
public void assemble() {
super.assemble();
System.out.print(" Adding features of a Luxury Car.");
}
}
// Step 5: Demonstrate the usage of the Decorator Pattern
public class DecoratorPatternTest {
public static void main(String[] args) {
Car sportsCar = new SportsCar(new BasicCar());
sportsCar.assemble();
System.out.println("\n-----");
Car luxuryCar = new LuxuryCar(new BasicCar());
luxuryCar.assemble();
System.out.println("\n-----");
Car sportsLuxuryCar = new SportsCar(new LuxuryCar(new BasicCar()));
sportsLuxuryCar.assemble();
}
}
output
Basic Car. Adding features of a Sports Car.
Basic Car. Adding features of a Luxury Car.
Basic Car. Adding features of a Luxury Car. Adding features of a Sports Car.
Conclusion:
To understand the differences between inheritance, composition, and decorator design patterns, it’s important to grasp each concept individually:
Inheritance is used for an "is-a" relationship where classes inherit methods and properties from a parent class. It promotes code reusability and run-time polymorphism but can lead to rigid class hierarchies.
Composition implements a "has-a" relationship and is favored for code reuse where objects are composed of other objects. It offers more flexibility and avoids the tight coupling seen in inheritance. Composition allows objects to contain other objects, fostering easier maintenance and flexibility.
Decorator Pattern is a structural pattern that allows behavior to be added dynamically to an object at runtime without affecting other objects. Unlike inheritance, which happens at compile-time, decorators provide a flexible alternative, allowing functionalities to be composed as needed.
Each pattern addresses different use cases:
- Use inheritance when objects share a clear hierarchical relationship.
- Use composition for flexibility when creating complex objects.
- Use decorators when you need to add functionality dynamically at runtime, without modifying existing class structures.
Top comments (0)