Design Patterns in Java: An Easy Learning

Design Patterns in Java: An Easy Learning

19 Sep 2024
Beginner
157 Views
88 min read
Learn with an interactive course and practical hands-on labs

⭐ .NET Design Patterns Course: Design Patterns in C# Online Training

Unlocking Design Patterns

Design patterns are an important concept in software development. They provide tested answers to common design issues, helping developers reduce code structure and improve maintainability. Design patterns allow you to construct resilient and scalable systems while also supporting excellent software architectural practices.

In the Design Pattern tutorial, we'll learn about what are design patterns in java,types of design patterns in java, when to use them, and how they might help you write better code. Let us begin by examining "What are Design Patterns?"

What are Design Patterns?

  • A design pattern is a general, recurring solution to a commonly encountered problem in software design that is utilized in software engineering.
  • It is not a comprehensive design that can be implemented in code straight immediately.
  • It is a description or model for issue solving that may be used in a variety of settings.
  • Design patterns are similar to cooking recipes in that they provide a tried-and-true approach to producing consistent results without having to start from scratch each time.
Read More:
Different Types of Software Design Principles
What is an IoC Container or DI Container

Types of Design Patterns

There are three types of Design Patterns that we have:
  1. Creational Patterns
  2. Structural Patterns
  3. Behavioral Patterns
Now, we will discuss each of them step by step:

1. Creational Design Patterns

  • Creational design patterns are one category of design patterns used in software development.
  • They work on improving the flexibility and efficiency of the object production process.
  • Creational design patterns define ways to create objects.
Read More: 7 Phases of the Software Development Life Cycle

Creational Design Patterns

  • The aim of these design patterns is not to compose objects directly but to introduce different ways for object creation so as to make the code easier and more flexible.
  • Using creational design patterns, developers can maintain the creation process in one place, separately from other code, thus enabling easy maintenance and upgrade of a program.
  • They basically help in building systems where the way things are created does not interfere with the overall design.

There are five types of creational design patterns:

  1. Singleton Design Pattern
  2. Factory Method Design Pattern
  3. Abstract Factory Design Pattern
  4. Builder Design Pattern
  5. Prototype Design Pattern

Let's start with a singleton design pattern.

1. Singleton design pattern

  • Singleton Design Pattern ensures that a class has only one instance throughout the application and provides a global point to access it.
  • This is useful when you need exactly one object, such as a configuration manager, to coordinate actions across the system.

Example

// Singleton class
class ScholarHatSingleton {
    // Single instance of the class
    private static ScholarHatSingleton instance;
    
    // Private constructor to prevent instantiation
    private ScholarHatSingleton() { }
    
    // Public method to provide access to the instance
    public static ScholarHatSingleton getInstance() {
        if (instance == null) {
            instance = new ScholarHatSingleton();
        }
        return instance;
    }
    
    // Method to display a message
    public void showMessage() {
        System.out.println("Welcome to ScholarHat Singleton!");
    }
}

// Main class
public class SingletonExample {
    public static void main(String[] args) {
        // Getting the single instance of ScholarHatSingleton
        ScholarHatSingleton singleton = ScholarHatSingleton.getInstance();
        
        // Displaying the message
        singleton.showMessage();
    }
}

Output

Welcome to ScholarHat Singleton!

Explanation

  • This code uses the Singleton design pattern. The ScholarHatSingleton class assures that just one instance of itself may be produced.
  • It offers a global point of access via the getInstance() method.
  • The showMessage() method displays a message, demonstrating the use of a singleton instance.
  • The Main class fetches a single instance and invokes the showMessage() function.

2. Factory Method Design Pattern

  • Factory Method Design Pattern defines a method for creating objects but lets subclasses decide which class to instantiate.
  • This pattern is useful when you need to create objects from a family of related classes. It allows you to use a common interface while delegating the actual creation to subclasses.
  • For example, a document editor may use a factory method to create different types of documents (e.g., text, spreadsheet) based on user input without knowing the specific class of the document being created.

Example

// Product Interface
interface Course {
    void getCourseDetails();
}

// Concrete Product Classes
class JavaCourse implements Course {
    @Override
    public void getCourseDetails() {
        System.out.println("ScholarHat offers an in-depth Java course.");
    }
}

class PythonCourse implements Course {
    @Override
    public void getCourseDetails() {
        System.out.println("ScholarHat offers a comprehensive Python course.");
    }
}

// Factory Class
class CourseFactory {
    // Method to get the course based on type
    public static Course getCourse(String courseType) {
        if (courseType == null) {
            return null;
        }
        if (courseType.equalsIgnoreCase("JAVA")) {
            return new JavaCourse();
        } else if (courseType.equalsIgnoreCase("PYTHON")) {
            return new PythonCourse();
        }
        return null;
    }
}

// Main Class
public class FactoryPatternExample {
    public static void main(String[] args) {
        // Creating Java course using Factory
        Course javaCourse = CourseFactory.getCourse("JAVA");
        javaCourse.getCourseDetails();
        
        // Creating Python course using Factory
        Course pythonCourse = CourseFactory.getCourse("PYTHON");
        pythonCourse.getCourseDetails();
    }
}

Output

ScholarHat offers an in-depth Java course.
ScholarHat offers a comprehensive Python course.

Explanation

  • This code demonstrates the Factory Method design pattern.
  • The Course interface provides the method getCourseDetails(), which implements concrete classes such as JavaCourse and PythonCourse.
  • The CourseFactory class generates instances of these concrete classes using the course type specified.
  • The Main class shows how to create Java and Python courses using the factory and displays their details.
  • This pattern allows you to build objects without specifying the specific class.

3. Abstract Factory design Pattern

  • Abstract Factory Design Pattern provides an interface for creating families of related objects without specifying their concrete classes.
  • This pattern is useful when you need to create objects that are part of a larger group, ensuring that the objects fit together well.

Example

// Abstract Product Interfaces
interface Course {
    void getCourseDetails();
}

interface Certification {
    void getCertificationDetails();
}

// Concrete Products for Java
class JavaCourse implements Course {
    @Override
    public void getCourseDetails() {
        System.out.println("ScholarHat offers an in-depth Java course.");
    }
}

class JavaCertification implements Certification {
    @Override
    public void getCertificationDetails() {
        System.out.println("ScholarHat provides Java Certification after course completion.");
    }
}

// Concrete Products for Python
class PythonCourse implements Course {
    @Override
    public void getCourseDetails() {
        System.out.println("ScholarHat offers a comprehensive Python course.");
    }
}

class PythonCertification implements Certification {
    @Override
    public void getCertificationDetails() {
        System.out.println("ScholarHat provides Python Certification after course completion.");
    }
}

// Abstract Factory Interface
interface CourseFactory {
    Course createCourse();
    Certification createCertification();
}

// Concrete Factories
class JavaFactory implements CourseFactory {
    @Override
    public Course createCourse() {
        return new JavaCourse();
    }
    
    @Override
    public Certification createCertification() {
        return new JavaCertification();
    }
}

class PythonFactory implements CourseFactory {
    @Override
    public Course createCourse() {
        return new PythonCourse();
    }
    
    @Override
    public Certification createCertification() {
        return new PythonCertification();
    }
}

// Main Class
public class AbstractFactoryPatternExample {
    public static void main(String[] args) {
        // Creating Java factory
        CourseFactory javaFactory = new JavaFactory();
        Course javaCourse = javaFactory.createCourse();
        Certification javaCertification = javaFactory.createCertification();
        javaCourse.getCourseDetails();
        javaCertification.getCertificationDetails();
        
        // Creating Python factory
        CourseFactory pythonFactory = new PythonFactory();
        Course pythonCourse = pythonFactory.createCourse();
        Certification pythonCertification = pythonFactory.createCertification();
        pythonCourse.getCourseDetails();
        pythonCertification.getCertificationDetails();
    }
}

Output

ScholarHat offers an in-depth Java course.
ScholarHat provides Java Certification after course completion.
ScholarHat offers a comprehensive Python course.
ScholarHat provides Python Certification after course completion.

Explanation

  • This code uses the Abstract Factory design pattern, which entails establishing families of linked objects without identifying the concrete classes.
  • In this example, Course and Certification are abstract product interfaces, whereas their concrete implementations (JavaCourse, JavaCertification, PythonCourse, and PythonCertification) are unique products.
  • The CourseFactory interface provides methods for constructing these products, whereas the JavaFactory and PythonFactory are concrete factories that generate instances of the relevant products.
  • The Main class demonstrates how these factories are used to generate and access course and certification information for Java and Python.

4. Builder Design Pattern

  • Builder Pattern separates the process of constructing a complex object from its representation, allowing you to build different types of objects using the same construction process.
  • This pattern is helpful when creating complex objects with many optional parts, making the code cleaner and more manageable.

Example

// Product Class
class ScholarHatCourse {
    private String name;
    private String duration;
    private String level;

    // Constructor (private to enforce the builder pattern)
    private ScholarHatCourse(CourseBuilder builder) {
        this.name = builder.name;
        this.duration = builder.duration;
        this.level = builder.level;
    }

    // Getters
    public String getName() {
        return name;
    }

    public String getDuration() {
        return duration;
    }

    public String getLevel() {
        return level;
    }

    // Static Builder Class
    public static class CourseBuilder {
        private String name;
        private String duration;
        private String level;

        public CourseBuilder setName(String name) {
            this.name = name;
            return this;
        }

        public CourseBuilder setDuration(String duration) {
            this.duration = duration;
            return this;
        }

        public CourseBuilder setLevel(String level) {
            this.level = level;
            return this;
        }

        public ScholarHatCourse build() {
            return new ScholarHatCourse(this);
        }
    }
}

// Main Class
public class BuilderPatternExample {
    public static void main(String[] args) {
        // Creating a ScholarHat course using the Builder Pattern
        ScholarHatCourse javaCourse = new ScholarHatCourse.CourseBuilder()
            .setName("Java Programming")
            .setDuration("6 months")
            .setLevel("Intermediate")
            .build();
        
        // Displaying course details
        System.out.println("Course Name: " + javaCourse.getName());
        System.out.println("Duration: " + javaCourse.getDuration());
        System.out.println("Level: " + javaCourse.getLevel());

        // Creating another course
        ScholarHatCourse pythonCourse = new ScholarHatCourse.CourseBuilder()
            .setName("Python Programming")
            .setDuration("4 months")
            .setLevel("Beginner")
            .build();

        // Displaying course details
        System.out.println("\nCourse Name: " + pythonCourse.getName());
        System.out.println("Duration: " + pythonCourse.getDuration());
        System.out.println("Level: " + pythonCourse.getLevel());
    }
}

output

Course Name: Java Programming
Duration: 6 months
Level: Intermediate

Course Name: Python Programming
Duration: 4 months
Level: Beginner

Explanation

  • This code uses the Builder design pattern.
  • ScholarHatCourse instances are created using a static inner CourseBuilder class, which accepts optional parameters such as name, duration, and level.
  • This pattern is beneficial when there are several parameters in a class and makes the code more legible and maintainable.
  • The Main class uses the builder to generate two-course instances and prints out their data.

5. Prototype Design Pattern

  • Prototype Pattern creates new objects by copying an existing object, known as the prototype, rather than building them from scratch.
  • This is useful when object creation is costly or complex, allowing for faster and more efficient object creation.

Example

// Abstract Prototype Class
abstract class ScholarHatCourse implements Cloneable {
    private String courseName;
    private String courseLevel;

    // Getters and Setters
    public String getCourseName() {
        return courseName;
    }

    public void setCourseName(String courseName) {
        this.courseName = courseName;
    }

    public String getCourseLevel() {
        return courseLevel;
    }

    public void setCourseLevel(String courseLevel) {
        this.courseLevel = courseLevel;
    }

    // Abstract method to clone the object
    public abstract ScholarHatCourse clone();

    // Method to display course details
    public void showCourseDetails() {
        System.out.println("Course Name: " + courseName);
        System.out.println("Course Level: " + courseLevel);
    }
}

// Concrete Prototype Class for Java Course
class JavaCourse extends ScholarHatCourse {
    public JavaCourse() {
        this.setCourseName("Java Programming");
        this.setCourseLevel("Intermediate");
    }

    @Override
    public ScholarHatCourse clone() {
        JavaCourse clone = null;
        try {
            clone = (JavaCourse) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }
}

// Concrete Prototype Class for Python Course
class PythonCourse extends ScholarHatCourse {
    public PythonCourse() {
        this.setCourseName("Python Programming");
        this.setCourseLevel("Beginner");
    }

    @Override
    public ScholarHatCourse clone() {
        PythonCourse clone = null;
        try {
            clone = (PythonCourse) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }
}

// Main Class
public class PrototypePatternExample {
    public static void main(String[] args) {
        // Creating original Java course
        JavaCourse javaCourse = new JavaCourse();
        javaCourse.showCourseDetails();

        // Cloning the Java course
        ScholarHatCourse clonedJavaCourse = javaCourse.clone();
        clonedJavaCourse.showCourseDetails();

        // Creating original Python course
        PythonCourse pythonCourse = new PythonCourse();
        pythonCourse.showCourseDetails();

        // Cloning the Python course
        ScholarHatCourse clonedPythonCourse = pythonCourse.clone();
        clonedPythonCourse.showCourseDetails();
    }
}

Output

Course Name: Java Programming
Course Level: Intermediate
Course Name: Java Programming
Course Level: Intermediate
Course Name: Python Programming
Course Level: Beginner
Course Name: Python Programming
Course Level: Beginner

Example

  • This programming uses the Prototype design pattern.
  • The ScholarHatCourse class defines an abstract clone method and includes properties such as courseName and courseLevel.
  • JavaCourse and PythonCourse are concrete prototypes that support the clone method, allowing their instances to be duplicated.
  • The Main class generates original course instances and clones, demonstrating the ability to duplicate objects without relying on their concrete classes.

2. Structural Patterns

  • Structural design patterns are a type of design pattern in software development that focuses on combining classes or objects to construct larger, more complicated structures.

Structural Patterns

  • They aid in organizing and managing object interactions in software systems, resulting in increased flexibility, reusability, and maintainability.

There are seven types of Structural design patterns.

  1. Adapter Design Pattern
  2. Bridge Design Pattern
  3. Composite Design Pattern
  4. Decorator Design Pattern
  5. Facade Design Pattern
  6. Flyweight Design Pattern
  7. Proxy Design Pattern
Let's explore it one by one:

1. Adapter Pattern Design Pattern

  • Adapter Pattern allows incompatible interfaces to work together by wrapping one interface with another.
  • This pattern is like a translator that enables two systems with different languages to communicate seamlessly.

Example

// Target Interface
interface ScholarHatCourse {
    void getCourseDetails();
}

// Adaptee Class (Existing Class)
class PythonCourseDetails {
    public void displayCourseDetails() {
        System.out.println("ScholarHat offers a comprehensive Python course.");
    }
}

// Adapter Class
class PythonCourseAdapter implements ScholarHatCourse {
    private PythonCourseDetails pythonCourseDetails;

    public PythonCourseAdapter(PythonCourseDetails pythonCourseDetails) {
        this.pythonCourseDetails = pythonCourseDetails;
    }

    @Override
    public void getCourseDetails() {
        pythonCourseDetails.displayCourseDetails();
    }
}

// Main Class
public class AdapterPatternExample {
    public static void main(String[] args) {
        // Existing Python course details (Adaptee)
        PythonCourseDetails pythonDetails = new PythonCourseDetails();

        // Adapter makes PythonCourseDetails compatible with ScholarHatCourse interface
        ScholarHatCourse courseAdapter = new PythonCourseAdapter(pythonDetails);

        // Using the adapter to get course details
        courseAdapter.getCourseDetails();
    }}

Output

ScholarHat offers a comprehensive Python course.

Explanation

  • This code uses the Adapter design pattern.
  • Clients expect to see the ScholarHatCourse interface.
  • PythonCourseDetails is an existing class (adaptee) that contains a method for displaying course data, but it does not implement the target interface.
  • The PythonCourseAdapter converts PythonCourseDetails to the ScholarHatCourse interface, allowing the current class to be used anywhere the ScholarHatCourse interface is required.
  • The Main class explains how to use the adaptor to retrieve course details.

2. Bridge Pattern Design Pattern

  • A Bridge Pattern separates an object’s abstraction from its implementation, allowing it to vary independently.
  • This pattern is useful when you want to decouple a system so that both the abstraction and implementation can evolve without affecting each other.

Example

// Implementor Interface
interface CourseContent {
    void getContent();
}

// Concrete Implementor 1
class VideoContent implements CourseContent {
    @Override
    public void getContent() {
        System.out.println("ScholarHat provides video content for the course.");
    }
}

// Concrete Implementor 2
class ArticleContent implements CourseContent {
    @Override
    public void getContent() {
        System.out.println("ScholarHat provides article content for the course.");
    }
}

// Abstraction
abstract class ScholarHatCourse {
    protected CourseContent content;

    public ScholarHatCourse(CourseContent content) {
        this.content = content;
    }

    public abstract void showCourseDetails();
}

// Refined Abstraction 1
class JavaCourse extends ScholarHatCourse {
    public JavaCourse(CourseContent content) {
        super(content);
    }

    @Override
    public void showCourseDetails() {
        System.out.println("Java Programming Course:");
        content.getContent();
    }
}

// Refined Abstraction 2
class PythonCourse extends ScholarHatCourse {
    public PythonCourse(CourseContent content) {
        super(content);
    }

    @Override
    public void showCourseDetails() {
        System.out.println("Python Programming Course:");
        content.getContent();
    }
}

// Main Class
public class BridgePatternExample {
    public static void main(String[] args) {
        // Creating a Java course with video content
        ScholarHatCourse javaCourse = new JavaCourse(new VideoContent());
        javaCourse.showCourseDetails();

        // Creating a Python course with article content
        ScholarHatCourse pythonCourse = new PythonCourse(new ArticleContent());
        pythonCourse.showCourseDetails();
    }
}

Output

Java Programming Course:
ScholarHat provides video content for the course.
Python Programming Course:
ScholarHat provides article content for the course.

Explanation

  • This programming uses the Bridge design pattern.
  • CourseContent is the implementor interface for actual implementations such as VideoContent and ArticleContent.
  • ScholarHatCourse is an abstraction that contains a reference to a CourseContent object.
  • The JavaCourse and PythonCourse classes extend ScholarHatCourse by implementing the showCourseDetails method, which delegates content processing to the CourseContent object.
  • This strategy allows for greater flexibility in selecting different content types for different courses by divorcing course structure from content details.

3. Composite Pattern Design Pattern

  • Composite Pattern lets you compose objects into tree structures to represent part-whole hierarchies.
  • This pattern allows clients to treat individual objects and compositions of objects uniformly, simplifying the handling of complex structures.

Example

import java.util.ArrayList;
import java.util.List;

// Component Interface
interface ScholarHatCourse {
    void showCourseDetails();
}

// Leaf Class 1
class SingleCourse implements ScholarHatCourse {
    private String courseName;

    public SingleCourse(String courseName) {
        this.courseName = courseName;
    }

    @Override
    public void showCourseDetails() {
        System.out.println("Course: " + courseName);
    }
}

// Composite Class
class CourseBundle implements ScholarHatCourse {
    private List courses = new ArrayList<>();

    public void addCourse(ScholarHatCourse course) {
        courses.add(course);
    }

    public void removeCourse(ScholarHatCourse course) {
        courses.remove(course);
    }

    @Override
    public void showCourseDetails() {
        for (ScholarHatCourse course : courses) {
            course.showCourseDetails();
        }
    }
}

// Main Class
public class CompositePatternExample {
    public static void main(String[] args) {
        // Single courses (Leafs)
        ScholarHatCourse javaCourse = new SingleCourse("Java Programming");
        ScholarHatCourse pythonCourse = new SingleCourse("Python Programming");
        ScholarHatCourse dataScienceCourse = new SingleCourse("Data Science");

        // Course bundle (Composite)
        CourseBundle programmingBundle = new CourseBundle();
        programmingBundle.addCourse(javaCourse);
        programmingBundle.addCourse(pythonCourse);

        // Full bundle including programming and data science courses
        CourseBundle fullBundle = new CourseBundle();
        fullBundle.addCourse(programmingBundle);
        fullBundle.addCourse(dataScienceCourse);

        // Display details of the full bundle
        System.out.println("ScholarHat Full Course Bundle:");
        fullBundle.showCourseDetails();
    }
}

Output

ScholarHat Full Course Bundle:
Course: Java Programming
Course: Python Programming
Course: Data Science

Explanation

  • This code uses the Composite Design Pattern.
  • The ScholarHatCourse interface represents the component interface, whereas SingleCourse is a leaf class that manages individual courses.
  • The CourseBundle class is a composite that can include several ScholarHatCourse objects, allowing individual courses as well as course bundles to be treated identically.

4. Decorator Pattern Design Pattern

  • Decorator Pattern adds additional responsibilities to an object dynamically without altering its structure.
  • This pattern is useful when you want to enhance or modify the behavior of objects at runtime without changing their code.

Example

// Base Interface
interface Course {
    String getDescription();
    double getCost();
}

// Concrete Component
class BasicCourse implements Course {
    @Override
    public String getDescription() {
        return "Basic Course";
    }

    @Override
    public double getCost() {
        return 100.00;
    }
}

// Decorator
abstract class CourseDecorator implements Course {
    protected Course course;

    public CourseDecorator(Course course) {
        this.course = course;
    }

    @Override
    public String getDescription() {
        return course.getDescription();
    }

    @Override
    public double getCost() {
        return course.getCost();
    }
}

// Concrete Decorator 1
class PremiumContentDecorator extends CourseDecorator {
    public PremiumContentDecorator(Course course) {
        super(course);
    }

    @Override
    public String getDescription() {
        return course.getDescription() + " with Premium Content";
    }

    @Override
    public double getCost() {
        return course.getCost() + 50.00;
    }
}

// Main Class
public class DecoratorPatternExample {
    public static void main(String[] args) {
        Course course = new BasicCourse();
        System.out.println("Description: " + course.getDescription());
        System.out.println("Cost: $" + course.getCost());

        Course premiumCourse = new PremiumContentDecorator(course);
        System.out.println("\nDescription: " + premiumCourse.getDescription());
        System.out.println("Cost: $" + premiumCourse.getCost());
    }
}

Output

Description: Basic Course
Cost: $100.0

Description: Basic Course with Premium Content
Cost: $150.0

Explanation

  • This example shows how to use Java's Decorator Patternto add functionality to objects dynamically.
  • The BasicCourse class depicts a simple course, whereas PremiumContentDecorator improves it by adding premium material and changing the pricing.
  • This demonstrates how decorators can extend object behavior without changing the original class.

5. Facade Pattern Design Pattern

  • Facade Design Pattern provides a simplified interface to a complex system of classes, making it easier to interact with the system.
  • This pattern is like a front desk that handles communication with a complex backend, shielding users from the complexity.
// Subsystem Classes
class CourseRegistration {
    public void registerCourse(String courseName) {
        System.out.println("Registering for course: " + courseName);
    }
}

class PaymentProcessing {
    public void processPayment(double amount) {
        System.out.println("Processing payment of $" + amount);
    }
}

class Certification {
    public void issueCertificate(String courseName) {
        System.out.println("Issuing certificate for course: " + courseName);
    }
}

// Facade
class CourseFacade {
    private CourseRegistration registration;
    private PaymentProcessing payment;
    private Certification certification;

    public CourseFacade() {
        registration = new CourseRegistration();
        payment = new PaymentProcessing();
        certification = new Certification();
    }

    public void enrollInCourse(String courseName, double amount) {
        registration.registerCourse(courseName);
        payment.processPayment(amount);
        certification.issueCertificate(courseName);
    }
}

// Main Class
public class FacadePatternExample {
    public static void main(String[] args) {
        CourseFacade facade = new CourseFacade();
        facade.enrollInCourse("Java Programming", 100.00);
    }
}

Output

Registering for course: Java Programming
Processing payment of $100.0
Issuing certificate for course: Java Programming

Explanation

  • This example demonstrates the Facade Pattern, which streamlines interactions with complicated subsystems.
  • The CourseFacade class provides a consistent interface for course registration, money processing, and certification issuance, simplifying the process of enrolling in a course with one method call.

6. Flyweight Design Pattern

  • Flyweight Design Pattern minimizes memory usage by sharing as much data as possible between similar objects.
  • This pattern is useful when dealing with a large number of similar objects, as it reduces the overhead of object creation and memory consumption.
import java.util.HashMap;
import java.util.Map;

// Flyweight Interface
interface Course {
    void display(String studentName);
}

// Concrete Flyweight
class ConcreteCourse implements Course {
    private String courseName;

    public ConcreteCourse(String courseName) {
        this.courseName = courseName;
    }

    @Override
    public void display(String studentName) {
        System.out.println("Course: " + courseName + " for Student: " + studentName);
    }
}

// Flyweight Factory
class CourseFactory {
    private Map courseMap = new HashMap<>();

    public Course getCourse(String courseName) {
        Course course = courseMap.get(courseName);
        if (course == null) {
            course = new ConcreteCourse(courseName);
            courseMap.put(courseName, course);
        }
        return course;
    }
}

// Main Class
public class FlyweightPatternExample {
    public static void main(String[] args) {
        CourseFactory factory = new CourseFactory();

        Course javaCourse = factory.getCourse("Java Programming");
        javaCourse.display("Alice");

        Course pythonCourse = factory.getCourse("Python Programming");
        pythonCourse.display("Bob");

        Course javaCourse2 = factory.getCourse("Java Programming");
        javaCourse2.display("Charlie");
    }
}

Output

Course: Java Programming for Student: Alice
Course: Python Programming for Student: Bob
Course: Java Programming for Student: Charlie

Explanation

  • This example shows the Flyweight Pattern, which optimizes memory use by sharing similar objects.
  • The CourseFactory manages a pool of ConcreteCourse objects, building new ones only when needed.
  • This allows successive requests for the same course type to utilize existing objects, eliminating redundancy and increasing efficiency.

7. Proxy Pattern Design Pattern

  • Proxy Pattern provides a placeholder or surrogate for another object to control access to it.
  • This pattern is useful for managing access to resources, adding security, or optimizing performance by delaying expensive operations until they are needed.

Example

// Subject Interface
interface ScholarHatCourse {
    void enroll();
}

// Real Subject
class RealCourse implements ScholarHatCourse {
    private String courseName;

    public RealCourse(String courseName) {
        this.courseName = courseName;
    }

    @Override
    public void enroll() {
        System.out.println("Enrolling in course: " + courseName);
    }
}

// Proxy
class CourseProxy implements ScholarHatCourse {
    private RealCourse realCourse;
    private String courseName;

    public CourseProxy(String courseName) {
        this.courseName = courseName;
    }

    @Override
    public void enroll() {
        if (realCourse == null) {
            realCourse = new RealCourse(courseName);
        }
        realCourse.enroll();
    }
}

// Main Class
public class ProxyPatternExample {
    public static void main(String[] args) {
        ScholarHatCourse course = new CourseProxy("Java Programming");
        course.enroll();  // RealCourse is created and enrolled in
    }
}

Output

Enrolling in course: Java Programming

Explanation

  • This example demonstrates the Proxy Pattern, which controls access to an object via a surrogate.
  • The CourseProxy class postpones the creation of the RealCourse instance until its enroll() function is called, thus managing resource usage and adding an extra layer of control.

3. Behavioral Design Patterns

  • Behavioral design patterns are a type of design pattern in developing software that deals with the communication and interaction of objects and classes.
  • They emphasize how objects and classes work together and communicate to complete tasks and responsibilities.

Behavioral Patterns

The following are types of behavioral patterns:

  1. Observer Pattern Design Pattern
  2. Strategy Pattern Design Pattern
  3. Command Pattern Design Pattern
  4. State Pattern Design Pattern
  5. Template Method Pattern Design Pattern
  6. Chain of Responsibility Pattern Design Pattern
  7. Mediator Pattern Design Pattern
  8. Memento Pattern Design Pattern
Let's explore one by one:

1. Observer Design Pattern

  • Observer Design Pattern allows an object to notify other objects about changes in its state.
  • This pattern is useful when you need a system where changes in one part of the system automatically update other parts, like a news feed where subscribers get updates when new articles are published.

Example

import java.util.ArrayList;
import java.util.List;

// Subject Interface
interface CourseSubject {
    void addObserver(CourseObserver observer);
    void removeObserver(CourseObserver observer);
    void notifyObservers();
}

// Concrete Subject
class Course implements CourseSubject {
    private List observers = new ArrayList<>();
    private String courseName;

    public Course(String courseName) {
        this.courseName = courseName;
    }

    @Override
    public void addObserver(CourseObserver observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(CourseObserver observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (CourseObserver observer : observers) {
            observer.update(courseName);
        }
    }

    public void changeCourseName(String newCourseName) {
        this.courseName = newCourseName;
        notifyObservers();
    }
}

// Observer Interface
interface CourseObserver {
    void update(String courseName);
}

// Concrete Observer
class Student implements CourseObserver {
    private String studentName;

    public Student(String studentName) {
        this.studentName = studentName;
    }

    @Override
    public void update(String courseName) {
        System.out.println(studentName + " received update: " + courseName);
    }
}

// Main Class
public class ObserverPatternExample {
    public static void main(String[] args) {
        Course course = new Course("Java Programming");

        Student student1 = new Student("Alice");
        Student student2 = new Student("Bob");

        course.addObserver(student1);
        course.addObserver(student2);

        course.changeCourseName("Advanced Java Programming");
    }
}

Output

Alice received update: Advanced Java Programming
Bob received update: Advanced Java Programming

Explanation

  • This example introduces the Observer Pattern, which enables an object (the Course) to tell multiple observers (the Student instances) of changes.
  • When the Course's name changes, it tells all registered observers, allowing them to respond to the change.
  • This pattern decouples the subject and observer, allowing for dynamic updates without direct connection.

2. Strategy Pattern Design Pattern

  • Strategy Design Pattern enables selecting an algorithm at runtime without altering the code that uses it.
  • This pattern is proper when you have multiple ways to operate, such as different sorting algorithms, and you want to choose the best one dynamically.
Read More:
Data Structures Sorting: Types and Examples Explained
Sort in Python- An Easy Way to Learn

Example

// Strategy Interface
interface PaymentStrategy {
    void pay(double amount);
}

// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using Credit Card.");
    }
}

class PayPalPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using PayPal.");
    }
}

// Context
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(double amount) {
        paymentStrategy.pay(amount);
    }
}

// Main Class
public class StrategyPatternExample {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        cart.setPaymentStrategy(new CreditCardPayment());
        cart.checkout(100.00);

        cart.setPaymentStrategy(new PayPalPayment());
        cart.checkout(200.00);
    }
}

Output

Paid $100.0 using Credit Card.
Paid $200.0 using PayPal

Explanation

  • This example demonstrates the Strategy Pattern, which enables interchangeable algorithms or tactics.
  • The ShoppingCart class can transition between multiple PaymentStrategy implementations (CreditCardPayment and PayPalPayment) at runtime, allowing for more flexible payment options at checkout.
  • This solution separates the payment method from the cart, allowing for simple adjustments and extensions.

3. Command Design Pattern

  • Command Design Patternturns requests or simple operations into objects, allowing them to be executed, undone, or queued.
  • This pattern is handy for implementing action history or undo mechanisms, like in a text editor where you can undo previous actions.

Example

// Command Interface
interface Command {
    void execute();
}

// Concrete Command
class EnrollInCourseCommand implements Command {
    private String courseName;

    public EnrollInCourseCommand(String courseName) {
        this.courseName = courseName;
    }

    @Override
    public void execute() {
        System.out.println("Enrolled in course: " + courseName);
    }
}

// Invoker
class CourseRegistration {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void performAction() {
        command.execute();
    }
}

// Main Class
public class CommandPatternExample {
    public static void main(String[] args) {
        Command enrollCommand = new EnrollInCourseCommand("Java Programming");
        CourseRegistration registration = new CourseRegistration();
        
        registration.setCommand(enrollCommand);
        registration.performAction();
    }
}

Output

Enrolled in course: Java Programming

Explanation

  • This example demonstrates the Command Pattern, which wraps a request as an object, enabling customization and request queueing.
  • The CourseRegistration class functions as an invoker, carrying out the EnrollInCourseCommand, which handles the specific operation of registering in a course.
  • This technique allows you greater flexibility when controlling commands and their execution.

4. StateDesign Pattern

  • State Design Pattern allows an object to change its behavior when its internal state changes.
  • This pattern is useful for managing state-dependent behavior, such as a vending machine that changes its operations based on whether it's idle, accepting coins, or dispensing items.

Example

// State Interface
interface CourseState {
    void handle();
}

// Concrete States
class NotStartedState implements CourseState {
    @Override
    public void handle() {
        System.out.println("Course not started yet.");
    }
}

class InProgressState implements CourseState {
    @Override
    public void handle() {
        System.out.println("Course is in progress.");
    }
}

class CompletedState implements CourseState {
    @Override
    public void handle() {
        System.out.println("Course is completed.");
    }
}

// Context
class Course {
    private CourseState state;

    public void setState(CourseState state) {
        this.state = state;
    }

    public void request() {
        state.handle();
    }
}

// Main Class
public class StatePatternExample {
    public static void main(String[] args) {
        Course course = new Course();

        course.setState(new NotStartedState());
        course.request();

        course.setState(new InProgressState());
        course.request();

        course.setState(new CompletedState());
        course.request();
    }
}

Output

Course not started yet.
Course is in progress.
Course is completed

Explanation

  • This example shows the State Pattern, which lets an object adapt its behavior when its internal state changes.
  • The Course class transitions between three states (NotStartedState, InProgressState, and CompletedState), each of which implements the handle() method to give appropriate behavior.
  • This design simplifies state management and increases flexibility in dealing with state-dependent behavior.

5. Template Method Design Pattern

  • Template Method Design Pattern defines the skeleton of an algorithm in a base class but lets subclasses override specific steps.
  • This pattern ensures a consistent structure while allowing customization, like a cooking recipe where the steps are fixed, but the ingredients can vary.

Example

// Abstract Class
abstract class CourseTemplate {
    public final void enroll() {
        selectCourse();
        makePayment();
        sendConfirmation();
    }

    protected abstract void selectCourse();
    protected abstract void makePayment();
    protected void sendConfirmation() {
        System.out.println("Confirmation sent.");
    }
}

// Concrete Class 1
class JavaCourse extends CourseTemplate {
    @Override
    protected void selectCourse() {
        System.out.println("Java Programming course selected.");
    }

    @Override
    protected void makePayment() {
        System.out.println("Payment made for Java course.");
    }
}

// Concrete Class 2
class PythonCourse extends CourseTemplate {
    @Override
    protected void selectCourse() {
        System.out.println("Python Programming course selected.");
    }

    @Override
    protected void makePayment() {
        System.out.println("Payment made for Python course.");
    }
}

// Main Class
public class TemplateMethodPatternExample {
    public static void main(String[] args) {
        CourseTemplate javaCourse = new JavaCourse();
        javaCourse.enroll();

        CourseTemplate pythonCourse = new PythonCourse();
        pythonCourse.enroll();
    }
}

Output

Java Programming course selected.
Payment made for Java course.
Confirmation sent.

Python Programming course selected.
Payment made for Python course.
Confirmation sent.

Explanation

  • This example introduces the Template Method Pattern, which outlines the skeleton of an algorithm in a base class while allowing subclasses to implement individual phases.
  • The CourseTemplate class defines the enroll() method, including steps such as selectCourse() and makePayment(), although actual classes (JavaCourse and PythonCourse) supply their own implementations.
  • This design maintains consistency while allowing for customization.

6. Chain of Responsibility Design Pattern

  • Chain of Responsibility Design Pattern passes a request along a chain of handlers until one of them handles it.
  • This pattern is useful for processing requests in a sequence, such as filtering and handling user inputs or processing various levels of a request in a web application.

Example

// Handler Interface
interface CourseHandler {
    void setNextHandler(CourseHandler handler);
    void handleRequest(String request);
}

// Concrete Handlers
class EnrollmentHandler implements CourseHandler {
    private CourseHandler nextHandler;

    @Override
    public void setNextHandler(CourseHandler handler) {
        this.nextHandler = handler;
    }

    @Override
    public void handleRequest(String request) {
        if (request.equals("Enroll")) {
            System.out.println("Handling enrollment.");
        } else if (nextHandler != null) {
            nextHandler.handleRequest(request);
        }
    }
}

class PaymentHandler implements CourseHandler {
    private CourseHandler nextHandler;

    @Override
    public void setNextHandler(CourseHandler handler) {
        this.nextHandler = handler;
    }

    @Override
    public void handleRequest(String request) {
        if (request.equals("Payment")) {
            System.out.println("Handling payment.");
        } else if (nextHandler != null) {
            nextHandler.handleRequest(request);
        }
    }
}

class CertificationHandler implements CourseHandler {
    private CourseHandler nextHandler;

    @Override
    public void setNextHandler(CourseHandler handler) {
        this.nextHandler = handler;
    }

    @Override
    public void handleRequest(String request) {
        if (request.equals("Certificate")) {
            System.out.println("Handling certification.");
        } else if (nextHandler != null) {
            nextHandler.handleRequest(request);
        }
    }
}

// Main Class
public class ChainOfResponsibilityPatternExample {
    public static void main(String[] args) {
        CourseHandler enrollment = new EnrollmentHandler();
        CourseHandler payment = new PaymentHandler();
        CourseHandler certification = new CertificationHandler();

        enrollment.setNextHandler(payment);
        payment.setNextHandler(certification);

        enrollment.handleRequest("Payment");
        enrollment.handleRequest("Certificate");
    }
}

Output

Handling payment.
Handling certification.

Explanation

  • This example demonstrates the Chain of Responsibility Pattern, which involves many handlers processing requests in a chain.
  • Each handler (EnrollmentHandler, PaymentHandler, and CertificationHandler) determines whether it can handle the request; if not, it forwards the request to the next handler in the chain.
  • This pattern encourages flexibility in request handling and allows for dynamic modifications to the processing order.

7. Mediator Design Pattern

  • Mediator Design Patterncentralizes communication between objects, reducing the dependency between them.
  • This pattern is useful for managing complex interactions in a system, such as coordinating the components of a chat application to handle user messages and notifications.

Example

import java.util.ArrayList;
import java.util.List;

// Mediator Interface
interface CourseMediator {
    void registerStudent(Student student);
    void notifyStudents(String message);
}

// Concrete Mediator
class Course implements CourseMediator {
    private List students = new ArrayList<>();

    @Override
    public void registerStudent(Student student) {
        students.add(student);
    }

    @Override
    public void notifyStudents(String message) {
        for (Student student : students) {
            student.update(message);
        }
    }
}

// Colleague
class Student {
    private String name;
    private CourseMediator mediator;

    public Student(String name, CourseMediator mediator) {
        this.name = name;
        this.mediator = mediator;
        mediator.registerStudent(this);
    }

    public void update(String message) {
        System.out.println(name + " received message: " + message);
    }
}

// Main Class
public class MediatorPatternExample {
    public static void main(String[] args) {
        Course course = new Course();

        Student student1 = new Student("Alice", course);
        Student student2 = new Student("Bob", course);

        course.notifyStudents("Course registration opens tomorrow.");
    }
}

Output

Alice received message: Course registration opens tomorrow.
Bob received message: Course registration opens tomorrow.

Explanation

  • This example shows the Mediator Pattern, which centralizes communication between objects.
  • The Course class serves as an intermediary, facilitating interactions between Student instances. Instead of conversing directly, students rely on the Course to broadcast messages, which simplifies communication and reduces object dependencies.

8. Memento Design Pattern

  • The Memento Design Pattern captures and restores an object's state without exposing its internal structure.
  • This pattern is helpful in saving and retrieving an object's state, like implementing an undo feature in a text editor where you can revert to a previous version of the document.

Example

// Memento
class CourseMemento {
    private String courseName;

    public CourseMemento(String courseName) {
        this.courseName = courseName;
    }

    public String getCourseName() {
        return courseName;
    }
}

// Originator
class Course {
    private String courseName;

    public void setCourseName(String courseName) {
        this.courseName = courseName;
    }

    public CourseMemento save() {
        return new CourseMemento(courseName);
    }

    public void restore(CourseMemento memento) {
        this.courseName = memento.getCourseName();
    }

    @Override
    public String toString() {
        return "Course name: " + courseName;
    }
}

// Caretaker
class CourseCaretaker {
    private CourseMemento memento;

    public void saveMemento(CourseMemento memento) {
        this.memento = memento;
    }

    public CourseMemento getMemento() {
        return memento;
    }
}

// Main Class
public class MementoPatternExample {
    public static void main(String[] args) {
        Course course = new Course();
        CourseCaretaker caretaker = new CourseCaretaker();

        course.setCourseName("Java Programming");
        System.out.println(course);

        caretaker.saveMemento(course.save());

        course.setCourseName("Advanced Java Programming");
        System.out.println(course);

        course.restore(caretaker.getMemento());
        System.out.println(course);
    }
}

Output

Course name: Java Programming
Course name: Advanced Java Programming
Course name: Java Programming

Explanation

  • This example demonstrates the Memento Pattern, which captures and restores an object's state while preserving its underlying structure.
  • The Course class can save its current state as a CourseMemento and then restore it later.
  • The CourseCaretaker saves the remembrance, allowing the Course to return to a prior state as necessary, effectively handling state preservation and restoration.

When to Use Design Patterns?

  • Reoccurring Problems: Use design patterns when you meet reoccurring design issues with well-defined answers. Design patterns provide tried-and-true approaches to typical program design difficulties.
  • Flexibility and Reusability: Use design patterns to encourage code reuse, flexibility, and maintenance. They assist in structuring code so that it is easier to adapt and extend as requirements change.
  • Design Principles: Use design patterns to implement core design principles including dividing up concerns, encapsulation, and dependency inversion. They aid in improving modularity and decreasing dependency between components.
  • Communication: Design patterns can help team members communicate more effectively. Design patterns provide a shared vocabulary and understanding of how to tackle certain challenges, which aids cooperation and code comprehension.
  • Performance: Design patterns can sometimes increase performance by minimizing resource utilization, lowering overhead, or increasing code execution efficiency.

How to Choose the Right Design Pattern?

Choosing a design pattern depends on the problem that you are solving and the requirements for solving that problem; consider the following:

  • Problem Type: Match the pattern to the nature of the problem, such as creational, structural, or behavioral issues.
  • Flexibility: Assess if the pattern allows flexibility and scalability during future changes.
  • Complexity: Assure the complexity of the pattern doesn't complexify your design but simplifies it.
  • Common Solutions: Observe well-used patterns in similar circumstances and leverage solutions proven to work.

Choosing the right pattern will increase the maintainability and effectiveness of your design.

Best Practices for Using Design Patterns

Best practices for using design patterns:

  • Understand the Purpose: Ensure you understand the pattern's purpose and its applicability in order to avoid misapplication.
  • Keep It Simple: Use patterns only if they really simplify a problem and, therefore, never introduce extraneous complexity.
  • Follow Conventions: Stick to existing naming conventions and patterns, as this will give the code much more readability.
  • Use of Document: Clearly document why and how the pattern is used so that it is useful to future developers.
Read more:
.Net Design Patterns Interview Questions, You Must Know!
Most Frequently Asked Software Architect Interview Questions and Answers
Summary

This tutorial presents an in-depth overview of design patterns, including definitions and implementations. It also includes creational, structural, and behavioral patterns. It begins with an overview of each category, followed by extensive explanations and sample code for patterns including Singleton, Factory Method, Adapter, and Observer. This tutorial shows when to utilize design patterns, how to select the proper one, and the best practices for successful implementation. Also, consider our Software Architecture and Design Certification Training for a better understanding of other Java concepts.

FAQs

The Strategy pattern defines a family of algorithms, encapsulates each algorithm, and makes them interchangeable. It allows the algorithm to vary independently from the clients that use it.

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It is useful for constructing objects with many optional components or configurations. 

The Adapter pattern allows an interface to be compatible with an existing class, while the Proxy pattern provides a surrogate or placeholder for another object to control access to it. 

The Decorator pattern allows additional responsibilities to be added to an object dynamically. It provides a flexible alternative to subclassing for extending functionality.

 The Memento pattern captures and externalizes an object's internal state without violating encapsulation, allowing the object to be restored to that state later. It's particularly useful for implementing undo functionality in applications. 
Share Article
About Author
Shailendra Chauhan (Microsoft MVP, Founder & CEO at Scholarhat by DotNetTricks)

Shailendra Chauhan is the Founder and CEO at ScholarHat by DotNetTricks which is a brand when it comes to e-Learning. He provides training and consultation over an array of technologies like Cloud, .NET, Angular, React, Node, Microservices, Containers and Mobile Apps development. He has been awarded Microsoft MVP 9th time in a row (2016-2024). He has changed many lives with his writings and unique training programs. He has a number of most sought-after books to his name which has helped job aspirants in cracking tough interviews with ease.
Accept cookies & close this