Design patterns are typical solutions to common problems in software design. They are like blueprints that can be customized to solve a particular design problem in your code. Understanding design patterns is crucial for building robust, maintainable, and scalable applications.

Key Concepts

  1. What are Design Patterns?

    • Reusable solutions to common software design problems.
    • Not code, but templates for how to solve a problem in various situations.
    • Help in improving code readability and reusability.
  2. Types of Design Patterns:

    • Creational Patterns: Deal with object creation mechanisms.
    • Structural Patterns: Deal with object composition or structure.
    • Behavioral Patterns: Deal with object interaction and responsibility.

Creational Patterns

Singleton Pattern

Ensures a class has only one instance and provides a global point of access to it.

Example:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // private constructor to prevent instantiation
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Explanation:

  • The constructor is private to prevent instantiation from outside the class.
  • The getInstance method ensures that only one instance of the class is created.

Factory Pattern

Creates objects without specifying the exact class of object that will be created.

Example:

public interface Shape {
    void draw();
}

public class Circle implements Shape {
    public void draw() {
        System.out.println("Drawing Circle");
    }
}

public class Square implements Shape {
    public void draw() {
        System.out.println("Drawing Square");
    }
}

public class ShapeFactory {
    public Shape getShape(String shapeType) {
        if (shapeType == null) {
            return null;
        }
        if (shapeType.equalsIgnoreCase("CIRCLE")) {
            return new Circle();
        } else if (shapeType.equalsIgnoreCase("SQUARE")) {
            return new Square();
        }
        return null;
    }
}

Explanation:

  • The ShapeFactory class creates objects of Circle and Square without exposing the creation logic to the client.

Structural Patterns

Adapter Pattern

Allows incompatible interfaces to work together.

Example:

public interface MediaPlayer {
    void play(String audioType, String fileName);
}

public class AudioPlayer implements MediaPlayer {
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file. Name: " + fileName);
        } else {
            System.out.println("Invalid media. " + audioType + " format not supported");
        }
    }
}

public interface AdvancedMediaPlayer {
    void playVlc(String fileName);
    void playMp4(String fileName);
}

public class VlcPlayer implements AdvancedMediaPlayer {
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file. Name: " + fileName);
    }

    public void playMp4(String fileName) {
        // do nothing
    }
}

public class Mp4Player implements AdvancedMediaPlayer {
    public void playVlc(String fileName) {
        // do nothing
    }

    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file. Name: " + fileName);
    }
}

public class MediaAdapter implements MediaPlayer {
    AdvancedMediaPlayer advancedMusicPlayer;

    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer = new VlcPlayer();
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer = new Mp4Player();
        }
    }

    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer.playVlc(fileName);
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer.playMp4(fileName);
        }
    }
}

Explanation:

  • MediaAdapter allows AudioPlayer to play vlc and mp4 files by adapting the AdvancedMediaPlayer interface.

Behavioral Patterns

Observer Pattern

Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Example:

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

public class Subject {
    private List<Observer> observers = new ArrayList<>();
    private int state;

    public int getState() {
        return state;
    }

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

    public void attach(Observer observer) {
        observers.add(observer);
    }

    public void notifyAllObservers() {
        for (Observer observer : observers) {
            observer.update();
        }
    }
}

public abstract class Observer {
    protected Subject subject;
    public abstract void update();
}

public class BinaryObserver extends Observer {
    public BinaryObserver(Subject subject) {
        this.subject = subject;
        this.subject.attach(this);
    }

    public void update() {
        System.out.println("Binary String: " + Integer.toBinaryString(subject.getState()));
    }
}

public class HexObserver extends Observer {
    public HexObserver(Subject subject) {
        this.subject = subject;
        this.subject.attach(this);
    }

    public void update() {
        System.out.println("Hex String: " + Integer.toHexString(subject.getState()));
    }
}

public class ObserverPatternDemo {
    public static void main(String[] args) {
        Subject subject = new Subject();

        new BinaryObserver(subject);
        new HexObserver(subject);

        System.out.println("First state change: 15");
        subject.setState(15);
        System.out.println("Second state change: 10");
        subject.setState(10);
    }
}

Explanation:

  • Subject maintains a list of observers and notifies them of any state changes.
  • Observer is an abstract class that is extended by concrete observer classes like BinaryObserver and HexObserver.

Practical Exercises

Exercise 1: Implement a Singleton Pattern

Task: Create a singleton class DatabaseConnection that ensures only one instance of the connection is created.

Solution:

public class DatabaseConnection {
    private static DatabaseConnection instance;

    private DatabaseConnection() {
        // private constructor to prevent instantiation
    }

    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }
}

Exercise 2: Implement a Factory Pattern

Task: Create a factory class AnimalFactory that creates objects of Dog and Cat classes.

Solution:

public interface Animal {
    void speak();
}

public class Dog implements Animal {
    public void speak() {
        System.out.println("Woof");
    }
}

public class Cat implements Animal {
    public void speak() {
        System.out.println("Meow");
    }
}

public class AnimalFactory {
    public Animal getAnimal(String animalType) {
        if (animalType == null) {
            return null;
        }
        if (animalType.equalsIgnoreCase("DOG")) {
            return new Dog();
        } else if (animalType.equalsIgnoreCase("CAT")) {
            return new Cat();
        }
        return null;
    }
}

Exercise 3: Implement an Observer Pattern

Task: Create a subject class WeatherStation and observer classes TemperatureDisplay and PressureDisplay that update their display when the weather station's state changes.

Solution:

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

public class WeatherStation {
    private List<Observer> observers = new ArrayList<>();
    private int temperature;
    private int pressure;

    public int getTemperature() {
        return temperature;
    }

    public void setTemperature(int temperature) {
        this.temperature = temperature;
        notifyAllObservers();
    }

    public int getPressure() {
        return pressure;
    }

    public void setPressure(int pressure) {
        this.pressure = pressure;
        notifyAllObservers();
    }

    public void attach(Observer observer) {
        observers.add(observer);
    }

    public void notifyAllObservers() {
        for (Observer observer : observers) {
            observer.update();
        }
    }
}

public abstract class Observer {
    protected WeatherStation weatherStation;
    public abstract void update();
}

public class TemperatureDisplay extends Observer {
    public TemperatureDisplay(WeatherStation weatherStation) {
        this.weatherStation = weatherStation;
        this.weatherStation.attach(this);
    }

    public void update() {
        System.out.println("Temperature: " + weatherStation.getTemperature());
    }
}

public class PressureDisplay extends Observer {
    public PressureDisplay(WeatherStation weatherStation) {
        this.weatherStation = weatherStation;
        this.weatherStation.attach(this);
    }

    public void update() {
        System.out.println("Pressure: " + weatherStation.getPressure());
    }
}

public class ObserverPatternDemo {
    public static void main(String[] args) {
        WeatherStation weatherStation = new WeatherStation();

        new TemperatureDisplay(weatherStation);
        new PressureDisplay(weatherStation);

        System.out.println("First state change: Temperature 30, Pressure 1000");
        weatherStation.setTemperature(30);
        weatherStation.setPressure(1000);
        System.out.println("Second state change: Temperature 25, Pressure 1010");
        weatherStation.setTemperature(25);
        weatherStation.setPressure(1010);
    }
}

Conclusion

Design patterns are essential tools for any software developer. They provide proven solutions to common problems and help in writing code that is easier to understand, maintain, and extend. By mastering design patterns, you can significantly improve the quality of your software designs and become a more effective programmer.

Java Programming Course

Module 1: Introduction to Java

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File I/O

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2024. All rights reserved