Design patterns are general reusable solutions to problems that occur frequently in software design. They are like blueprints that can be customized to solve specific design problems in a particular context. There are three main categories of design patterns: creational, structural, and behavioral.
Modern Java, starting from Java 8, introduced several features that have had a profound impact on design pattern implementation:
Let’s take a look at a classic example of the Observer pattern in traditional Java.
import java.util.ArrayList;
import java.util.List;
// Subject interface
interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// Observer interface
interface Observer {
void update();
}
// Concrete subject
class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
// Concrete observer
class ConcreteObserver implements Observer {
@Override
public void update() {
System.out.println("Observer updated");
}
}
// Main class
public class TraditionalObserverExample {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
ConcreteObserver observer = new ConcreteObserver();
subject.registerObserver(observer);
subject.notifyObservers();
}
}
In this example, the ConcreteSubject
maintains a list of Observer
objects and notifies them when a change occurs.
With the introduction of lambda expressions, we can simplify the implementation of the Observer pattern.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
// Subject class
class ModernSubject {
private List<Consumer<String>> observers = new ArrayList<>();
public void registerObserver(Consumer<String> observer) {
observers.add(observer);
}
public void removeObserver(Consumer<String> observer) {
observers.remove(observer);
}
public void notifyObservers(String message) {
observers.forEach(observer -> observer.accept(message));
}
}
// Main class
public class ModernObserverExample {
public static void main(String[] args) {
ModernSubject subject = new ModernSubject();
subject.registerObserver(message -> System.out.println("Received message: " + message));
subject.notifyObservers("Hello, World!");
}
}
In this modern version, we use a Consumer
functional interface to represent observers. Lambda expressions are used to define the behavior of the observers, making the code more concise.
The Strategy pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable.
// Strategy interface
@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
}
// Context class
class Calculator {
private MathOperation operation;
public Calculator(MathOperation operation) {
this.operation = operation;
}
public int calculate(int a, int b) {
return operation.operate(a, b);
}
}
// Main class
public class StrategyExample {
public static void main(String[] args) {
MathOperation addition = (a, b) -> a + b;
MathOperation subtraction = (a, b) -> a - b;
Calculator calculator = new Calculator(addition);
int result = calculator.calculate(5, 3);
System.out.println("Addition result: " + result);
calculator = new Calculator(subtraction);
result = calculator.calculate(5, 3);
System.out.println("Subtraction result: " + result);
}
}
Here, the MathOperation
is a functional interface, and lambda expressions are used to define different strategies.
import java.util.function.Supplier;
// Product interface
interface Shape {
void draw();
}
// Concrete products
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
// Factory class
class ShapeFactory {
public static Shape createShape(Supplier<Shape> shapeSupplier) {
return shapeSupplier.get();
}
}
// Main class
public class FactoryExample {
public static void main(String[] args) {
Shape circle = ShapeFactory.createShape(Circle::new);
circle.draw();
Shape rectangle = ShapeFactory.createShape(Rectangle::new);
rectangle.draw();
}
}
The evolution of design patterns in modern Java has been driven by the introduction of new features like lambda expressions, streams, and functional interfaces. These features have made it possible to implement design patterns in a more concise and flexible way. However, it’s important to use these features wisely and follow best practices to ensure that the code remains maintainable and understandable. By understanding the traditional design patterns and their modern implementations, developers can write more efficient and effective Java code.