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
-
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.
-
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 ofCircle
andSquare
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
allowsAudioPlayer
to playvlc
andmp4
files by adapting theAdvancedMediaPlayer
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 likeBinaryObserver
andHexObserver
.
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
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
Module 2: Control Flow
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
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
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection