Introduction To Design Patterns

Design Patterns are reusable templates or blueprints towards the solution of common problems in software design. When communicating with other developers using a pattern in description, others know exactly the design you have in mind, as by using the pattern name, you communicate the whole set of qualities, characteristics and constraints that the pattern represents.

But why should I be learning design patterns?

  • Reusable solutions: Design patterns provides tried and tested way to solve a problem.
  • Provides a shared vocabulary to interact with other developers in the team.
  • Encourages loosely coupled, easy to maintain and highly scalable applications.
  • Often asked in technical interviews 😋.

Now that we know what design patterns are and why we need them, there is just one more thing we need in our arsenal before we embark on our journey towards learning design patterns.

SOLID Principles

Solid principles are a set of 5 design principles that helps you write clean, maintainable and scalable object-oriented code.
Lets understand each of these 5 principles…

S – Single Responsibility Principle

SRP states that a class(any component) must have only one reason to change, that is, it should have only one responsibility.

If a class takes on more than one responsibility, it would mean that if one responsibility changes, since the class has multiple responsibilities, other responsibilities may also be affected – leading to a ripple effect of changes throughout the codebase.

Let’s take a real life example to understand this:

Imagine a shopkeeper responsible for:

  • Selling items
  • Restocking inventory
  • Cleaning store
  • Billing

Now, if the shopkeeper is busy cleaning, they might not be able to bill items. If they are handling inventory, they might ignore the customers and so on. As a result, overall efficiency and quality of service drops.

Instead, if the responsibilities are split,

  • Shopkeeper focuses on making sales
  • Stock manages restocks inventory
  • Cleaner cleans the store
  • And accountant manages the finances
Single Responsibility Principle

Hence, each person has a single focused responsibility – exactly what SRP promotes.

O – Open Closed Principle

Open Close Principle states that software components(classes, methods etc.) should be open for extension (inheritance), but closed for modification.

The behavior of a component should be extended without modifying it’s source code. This will reduce the risk of breaking functionalities when requirements change.

Let’s take an example to understand this principle.

Suppose you own a company that produces miniature cars (always been a fan of those 😄).

Now your company provides a discount of 10% all it’s regular customers.

// Discount class returns the discount for a customer
class Discount {
    public double getDiscount() {
        return 0.1;
    }
}

// Calculate the total bill of the customer
class Bill {
    public double getInvoice(double productPrice) {
        Discount discount = new Discount();
        return productPrice - productPrice * discount.getDiscount();
    }
}

Now after a while your company decides that it will provide 20% discount to all it’s premium customers. Now, if you are unaware of Open Close principle, below is the code you would write.

// Discount class returns the discount for a customer
class Discount {
    public double getDiscount(String CustomerType) {
        if(customerType.equals("Regular")) return 0.1;
        if(customerType.equals("Premium")) return 0.2;
        return 0.0;
    }
}

// Calculate the total bill of the customer
class Bill {
    public double getInvoice(double productPrice) {
        Discount discount = new Discount();
        // ❌ breaks as there is a change in getDiscount, and getInvoice does not know about it
        return productPrice - productPrice * discount.getDiscount(); 
    }
}

So, we see how our code breaks in unforeseen ways when we do not follow open close principle.
Now let’s make our code Open Close Principle compliant.

// Define an interface
interface Discount {
    double getDiscount();
}

// Implement concrete discount class for regular customer
class RegularDiscount implements Discount {
    public double getDiscount() {
        return 0.1;
    }
}

// Implement concrete discount class for premium customer
class PremiumDiscount implements Discount {
    public double getDiscount() {
        return 0.2;
    }
}

// Bill class uses dicount via constructor
class Bill {
    private Discount discount;

    public Bill(Discount discount) {
        this.discount = discount;
    }

    public double getInvoice(double productPrice) {
        return productPrice - productPrice * discount.getDiscount();
    }
}

Hence our code now follows Open Close Principle.
Note: Open Close principle is specially useful if a component is expected to change over time.

L – Liskov Substitution Principle

Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program.

Let’s understand this with the help of an example.
Suppose we are payments service provider and we provide both online and offline payments.

// Superclass / Interface
interface PaymentProcessor {
    void processPayment(double amount);
}

// Subclass for credit card payments
class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

// Subclass for internet banking
class InternetBankingProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing internet banking payment of $" + amount);
    }
}

// Subclass for offline payments
class CashPaymentProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        throw new UnsupportedOperationException("Cash payments not supported online");
    }
}

Now, even though CashPaymentProcessor is a subclass of PaymentProcessor, but we know that cash cannot be supported as an online payment. So we will not be able to substitute an object of type CashPaymentProcessor with an object of PaymentProcessor as it would change the behavior of the program hence violating LSP.

public class PaymentTest {
    public static void main(String[] args) {
        PaymentProcessor processor = new CashPaymentProcessor();
        processor.processPayment(50);  // ❌ Violates LSP — throws unexpected exception
    }
}

Now that we have understood the problem, let’s see how to make this scenario LSP compliant.
We know that there are two kinds of payments:

  • Online Payments
  • Offline Payments

So, we will make sure to consider both the kinds of payments separately.

interface OnlinePaymentProcessor {
    void processOnlinePayment(double amount);
}

interface OfflinePaymentProcessor {
    void processOfflinePayment(double amount);
}

Now CreditCardProcessor and InternetBankingProcessor can be subclasses of OnlinePaymentProcessor , whereas CashPaymentProcessor can be a subclass of OfflinePaymentProcessor.

class CreditCardProcessor implements OnlinePaymentProcessor {
    @Override
    public void processOnlinePayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

class InternetBankingProcessor implements OnlinePaymentProcessor {
    @Override
    public void processOnlinePayment(double amount) {
        System.out.println("Processing internet banking payment of $" + amount);
    }
}

class CashPaymentProcessor implements OfflinePaymentProcessor {
    @Override
    public void processOfflinePayment(double amount) {
        System.out.println("Accepting cash payment of $" + amount);
    }
}

So, now CashPaymentProcessor can be replaced with it’s superclass which is OfflinePaymentProcessor without changing the behavior of the program.

public class PaymentTest {
    public static void main(String[] args) {
        OfflinePaymentProcessor processor = new CashPaymentProcessor();
        processor.processPayment(50);  // ✅ follows LSP
    }
}

Hence, now our code follows Liskov Substitution Principle.

I – Interface Segregation Principle

Interface Segregation Principle states that clients should not be forced to depend on interfaces that they do not use.

We should not :

  • make our interfaces big
  • cram unrelated methods in one big interface and make the classes in our module implement the interface.

Let us understand this with an example.
Suppose we have an interface for restaurant staff.

interface RestaurantStaff {
    void takeOrder();   // Responsibility of waiter
    void cookFood();    // Responsibility of chef
    void deliverFood(); // Responsibility of waiter
}

Now, even though we want to create an object of chef, we are still forced to implement takeOrder() and deliverFood() and similarly for a waiter, we are forced to implement cookFood() even though, cooking food will never be a waiter’s responsibility. Thereby, we are forced to implement unrelated methods because of which this would be a bad design.

Let’s try breaking down the interface and have only related methods together.

interface WaiterStaff {
    void takeOrder();   // Responsibility of waiter
    void deliverFood(); // Responsibility of waiter
}

interface ChefStaff {
    void cookFood();    // Responsibility of chef
}
Interface Segregation Principle

Now we see that an implementation of WaiterStaff will only have the responsibilities of a waiter and and that of ChefStaff will only have responsibilities of a chef – thereby following Interface Segregation Principle.

D – Dependency Inversion Principle

Dependency Inversion Principle states that:

  • High level module should not depend on low level modules.
  • Both high level modules and low level module should depend on abstraction.
  • Abstractions should not depend on details. Details should depend on abstraction.

Let’s understand this with an example.
Suppose we have a notification service wherein we provide Email and SMS notification.

Consider the below code which does not follow dependency inversion.

// Low level module
class EmailService {
    public void sendEmail(String message) {
        System.out.println("Sending email: " + message);
    }
}

// Low level module
class SMSService {
    public void sendSMS(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

// High level module
class NotificationManager {
    private EmailService emailService;

    public NotificationManager() {
    
        // ❌ High level modules should not depend on low level modules
        this.emailService = new EmailService(); 
    }

    public void send(String message) {
        emailService.sendEmail(message);
    }
}

We can see that the high level module(NotificationManager) is very tightly coupled with a low level module(EmailService). Moreover we see that SMSService implements a different method(send()) from EmailService which implements sendEmail(), and hence changing the notification from email to SMS in the NotificationManager would actually turn into a nightmare😱, as a lot of changes will have to be made.

But why does this happen??

It happens because there is no abstraction forcing us to keep same method names in all kinds of notification services. This brings us to the second point of Dependency Inversion principle’s definition, that is “Both high level modules and low level module should depend on abstraction.”

So let’s first create an abstraction.

// Abstraction
interface NotificationService {
    void send(String message);
}

Now that our abstraction is all set, all kinds of notification services are bound to follow this abstraction.

// Low level module
class EmailService implements NotificationService {
    public void send(String message) {
        System.out.println("Sending email: " + message);
    }
}

// Low level module
class SMSService implements NotificationService {
    public void send(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

Now that our low level modules depend on abstraction, by definition, our high level modules must also depend on abstraction.

// High-level module
class NotificationManager {
    private NotificationService service;

    // Inject abstraction
    public NotificationManager(NotificationService service) {
        this.service = service;
    }

    public void notify(String message) {
        service.send(message);
    }
}

This is how we can make our code Dependency Inversion compliant.

With this we come to an end of SOLID principles 🥳.
With our next few blogs we will be exploring the depths of design patterns.