Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: dp.md
Browse files Browse the repository at this point in the history
Fraguinha committed Dec 31, 2024
1 parent fb7d41d commit e765f17
Showing 1 changed file with 929 additions and 0 deletions.
929 changes: 929 additions & 0 deletions content/post/dp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,929 @@
---
title: "DP - Design Patterns"
description: "Design Patterns in Java."
date: 2024-12-30T13:00:00Z
draft: false
---

Overview of the simplest and most common design patterns according to the book *"Design Patterns Elements of Reusable Object-Oriented Software"*:

- Abstract Factory
- Factory Method
- Adapter
- Composite
- Decorator
- Observer
- Strategy
- Template Method

with additional patterns which I personally consider common as well:

- Singleton
- Builder
- Proxy
- Command
- Chain of Responsibility
- Mediator
- Visitor

# Creational Patterns

**Abstract Factory**:

Use when your system needs to create families of related or dependent objects without specifying their concrete classes.

**Signs you might need it**:

If adding new families of related objects requires significant changes to client code or tight coupling to concrete classes makes the system rigid and hard to extend.

```java
public interface ButtonAbstractFactory {
Button createButton();
}

public class WindowsButtonFactory implements ButtonAbstractFactory {
public Button createButton() {
return new WindowsButton();
}
}

public class MacButtonFactory implements ButtonAbstractFactory {
public Button createButton() {
return new MacButton();
}
}

public interface Button {
void render();
}

public class WindowsButton implements Button {
public void render() {
System.out.println("Rendering Windows Button");
}
}

public class MacButton implements Button {
public void render() {
System.out.println("Rendering Mac Button");
}
}

public class Application {
private final Button button;

public Application(ButtonAbstractFactory factory) {
this.button = factory.createButton();
}

public void render() {
button.render();
}
}

public class Client {
public static void main(String[] args) {
ButtonAbstractFactory factory = new MacButtonFactory();
Application app = new Application(factory);
app.render();
}
}
```

**Factory Method**:

Use when a class can't anticipate the exact type of objects it must create or when the creation logic should be delegated to subclasses.

**Signs you might need it**:

If the code is cluttered with hardcoded object creation, making it difficult to modify or extend without altering the client code.

```java
public interface Button {
void render();
}

public class WindowsButton implements Button {
public void render() {
System.out.println("Rendering Windows Button");
}
}

public class MacButton implements Button {
public void render() {
System.out.println("Rendering Mac Button");
}
}

public abstract class ButtonFactory {
public abstract Button createButton();
}

public class WindowsButtonFactory extends ButtonFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
}

public class MacButtonFactory extends ButtonFactory {
@Override
public Button createButton() {
return new MacButton();
}
}

public class Client {
public static void main(String[] args) {
ButtonFactory factory = new WindowsButtonFactory();
Button button = factory.createButton();
button.render();
}
}
```

**Singleton**

Use when you need to ensure only one instance of a class exists and provide a global access point to it.

**Signs you might need it**:

If multiple instances of a class lead to inconsistent state or resource conflicts across the application.

```java
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;
}
}

public class Client {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton anotherInstance = Singleton.getInstance();
System.out.println("Instances are the same: " + (instance == anotherInstance));
}
}
```

**Builder**

Use when constructing a complex object with many optional parameters or configurations that should be constructed step-by-step.

**Signs you might need it**:

If constructors become unwieldy due to numerous optional parameters or configurations, leading to unclear and error-prone code.

```java
public class House {
private int rooms;
private boolean hasGarage;
private boolean hasGarden;

private House(Builder builder) {
this.rooms = builder.rooms;
this.hasGarage = builder.hasGarage;
this.hasGarden = builder.hasGarden;
}

public static class Builder {
private int rooms;
private boolean hasGarage;
private boolean hasGarden;

public Builder setRooms(int rooms) {
this.rooms = rooms;
return this;
}

public Builder setGarage(boolean hasGarage) {
this.hasGarage = hasGarage;
return this;
}

public Builder setGarden(boolean hasGarden) {
this.hasGarden = hasGarden;
return this;
}

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

public class Client {
public static void main(String[] args) {
House house = new House.Builder()
.setRooms(3)
.setGarage(true)
.setGarden(false)
.build();
System.out.println("House built: " + house);
}
}
```

# Structural Patterns

**Adapter**:

Use when you need to make two incompatible interfaces work together without altering their source code.

**Signs you might need it**:

If incompatible interfaces prevent the integration of existing components or systems, forcing duplication or rewriting of code.

```java
public interface Celsius {
double getCelsiusTemperature();
}

public class Fahrenheit {
private double temperature;

public Fahrenheit(double temperature) {
this.temperature = temperature;
}

public double getFahrenheitTemperature() {
return temperature;
}
}

public class FahrenheitToCelsiusAdapter implements Celsius {
private Fahrenheit device;

public FahrenheitToCelsiusAdapter(Fahrenheit device) {
this.device = device;
}

@Override
public double getCelsiusTemperature() {
double fahrenheit = device.getFahrenheitTemperature();
return (fahrenheit - 32) * 5 / 9;
}
}

public class Client {
public static void main(String[] args) {
Fahrenheit fahrenheitDevice = new Fahrenheit(98.6);
Celsius temperatureAdapter = new FahrenheitToCelsiusAdapter(fahrenheitDevice);
System.out.println("Temperature in Celsius: " + temperatureAdapter.getCelsiusTemperature());
}
}
```

**Composite**:

Use when you need to represent a part-whole hierarchy and treat individual objects and groups of objects uniformly.

**Signs you might need it**:

If managing hierarchies of objects results in duplicated logic or complicated client code that treats individual and composite objects differently.

```java
public interface JSONElement {
String toJSONString();
}

public class JSONValue implements JSONElement {
private Object value;

public JSONValue(Object value) {
this.value = value;
}

@Override
public String toJSONString() {
return (value instanceof String) ? "\"" + value + "\"" : value.toString();
}
}

public class JSONArray implements JSONElement {
private List<JSONElement> elements = new ArrayList<>();

public void add(JSONElement element) {
elements.add(element);
}

@Override
public String toJSONString() {
return elements.stream()
.map(JSONElement::toJSONString)
.collect(Collectors.joining(", ", "[", "]"));
}
}

public class JSONObject implements JSONElement {
private Map<String, JSONElement> elements = new HashMap<>();

public void add(String key, JSONElement element) {
elements.put(key, element);
}

@Override
public String toJSONString() {
return elements.entrySet().stream()
.map(e -> "\"" + e.getKey() + "\": " + e.getValue().toJSONString())
.collect(Collectors.joining(", ", "{", "}"));
}
}

public class Client {
public static void main(String[] args) {
JSONObject root = new JSONObject();
root.add("title", new JSONValue("Design Patterns"));

JSONArray authors = new JSONArray();
authors.add(new JSONValue("Gamma"));
authors.add(new JSONValue("Helm"));
authors.add(new JSONValue("Johnson"));
authors.add(new JSONValue("Vlissides"));
root.add("authors", authors);

System.out.println(root.toJSONString());
}
}
```

**Decorator**:

Use when you need to dynamically add or modify behavior to an object without affecting others.

**Signs you might need it**:

If extending the functionality of a class leads to an explosion of subclasses for every possible variation.

```java
public interface DataSource {
void writeData(String data);
String readData();
}

public class FileDataSource implements DataSource {
private String filename;

public FileDataSource(String filename) {
this.filename = filename;
}

@Override
public void writeData(String data) {
System.out.println("Writing data to " + filename);
}

@Override
public String readData() {
return "Reading data from " + filename;
}
}

public class CompressionDecorator implements DataSource {
private DataSource source;

public CompressionDecorator(DataSource source) {
this.source = source;
}

@Override
public void writeData(String data) {
source.writeData(compress(data));
}

@Override
public String readData() {
return decompress(source.readData());
}

private String compress(String data) {
return "Compressed[" + data + "]";
}

private String decompress(String data) {
return data.replace("Compressed[", "").replace("]", "");
}
}

public class EncryptionDecorator implements DataSource {
private DataSource source;

public EncryptionDecorator(DataSource source) {
this.source = source;
}

@Override
public void writeData(String data) {
source.writeData(encrypt(data));
}

@Override
public String readData() {
return decrypt(source.readData());
}

private String encrypt(String data) {
return "Encrypted[" + data + "]";
}

private String decrypt(String data) {
return data.replace("Encrypted[", "").replace("]", "");
}
}

public class Client {
public static void main(String[] args) {
DataSource dataSource = new FileDataSource("data.txt");
DataSource compressedAndEncrypted = new CompressionDecorator(new EncryptionDecorator(dataSource));

compressedAndEncrypted.writeData("Important Data");
System.out.println(compressedAndEncrypted.readData());
}
}
```

**Proxy**:

Use when you need to control access to an object, for purposes like lazy initialization, logging, or access control.

**Signs you might need it**:

If direct access to resource-intensive or sensitive objects causes performance issues or security risks.

```java
public interface Database {
void query(String sql);
}

public class RealDatabase implements Database {
public RealDatabase() {
connectToDatabase();
}

private void connectToDatabase() {
System.out.println("Connecting to the database...");
}

@Override
public void query(String sql) {
System.out.println("Executing query: " + sql);
}
}

public class DatabaseProxy implements Database {
private RealDatabase realDatabase;

@Override
public void query(String sql) {
if (realDatabase == null) {
realDatabase = new RealDatabase();
}
realDatabase.query(sql);
}
}

public class Client {
public static void main(String[] args) {
Database db = new DatabaseProxy();
db.query("SELECT * FROM Users");
}
}
```

# Behavioral Patterns

**Observer**:

Use when an object needs to notify multiple dependent objects of state changes, maintaining a one-to-many relationship.

**Signs you might need it**:

If manually updating multiple dependent objects becomes error-prone and leads to tight coupling between the subject and its observers.

```java
public interface Observer {
void update(String message);
}

public class Subscriber implements Observer {
private String name;

public Subscriber(String name) {
this.name = name;
}

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

public class Publisher {
private List<Observer> observers = new ArrayList<>();

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

public void unsubscribe(Observer observer) {
observers.remove(observer);
}

public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}

public class Client {
public static void main(String[] args) {
Publisher newsPublisher = new Publisher();

Observer alice = new Subscriber("Alice");
Observer bob = new Subscriber("Bob");

newsPublisher.subscribe(alice);
newsPublisher.subscribe(bob);

newsPublisher.notifyObservers("Breaking News: Design Patterns Book is Awesome!");
}
}
```

**Strategy**:

Use when you want to define a family of algorithms, encapsulate them, and make them interchangeable at runtime.

**Signs you might need it**:

If hardcoded algorithms or behaviors make the system inflexible and difficult to extend or modify.

```java
public interface PaymentStrategy {
void pay(int amount);
}

public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using credit card.");
}
}

public class BankTransferPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using bank transfer.");
}
}

public class Client {
public static void main(String[] args) {
PaymentStrategy creditCardPayment = new CreditCardPayment();
PaymentStrategy bankTransferPayment = new BankTransferPayment();

creditCardPayment.pay(500);
bankTransferPayment.pay(1000);
}
}
```

**Template Method**:

Use when you need to define the skeleton of an algorithm but allow subclasses to override specific steps.

**Signs you might need it**:

If duplicated logic for similar algorithms creates maintenance challenges and increases the likelihood of inconsistencies.

```java
public abstract class Game {
public final void play() {
initialize();
startPlay();
endPlay();
}

protected abstract void initialize();
protected abstract void startPlay();
protected abstract void endPlay();
}

public class Chess extends Game {
@Override
protected void initialize() {
System.out.println("Chess Game Initialized!");
}

@Override
protected void startPlay() {
System.out.println("Chess Game Started!");
}

@Override
protected void endPlay() {
System.out.println("Chess Game Finished!");
}
}

public class Client {
public static void main(String[] args) {
Game chess = new Chess();
chess.play();
}
}
```

**Command**:

Use when you need to encapsulate requests as objects, enabling parameterization, queuing, or undo operations.

**Signs you might need it**:

If request handling logic is tightly coupled to specific actions, making it hard to implement features like queuing, logging, or undo.

```java
public interface Command {
void execute();
}

public class Light {
public void turnOn() {
System.out.println("Light is ON");
}

public void turnOff() {
System.out.println("Light is OFF");
}
}

public class TurnOnLightCommand implements Command {
private Light light;

public TurnOnLightCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
light.turnOn();
}
}

public class TurnOffLightCommand implements Command {
private Light light;

public TurnOffLightCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
light.turnOff();
}
}

public class RemoteControl {
private Command command;

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

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

public class Client {
public static void main(String[] args) {
Light livingRoomLight = new Light();
Command turnOn = new TurnOnLightCommand(livingRoomLight);
Command turnOff = new TurnOffLightCommand(livingRoomLight);

RemoteControl remote = new RemoteControl();
remote.setCommand(turnOn);
remote.pressButton();

remote.setCommand(turnOff);
remote.pressButton();
}
}
```

**Chain of Responsibility**:

Use when multiple objects might handle a request, but the specific handler is determined at runtime.

**Signs you might need it**:

If request handling becomes rigid and changes to handlers require altering the request-sender code.

```java
public abstract class Handler {
private Handler next;

public void setNext(Handler next) {
this.next = next;
}

public void handleRequest(String request) {
if (next != null) {
next.handleRequest(request);
}
}
}

public class LoggingHandler extends Handler {
@Override
public void handleRequest(String request) {
System.out.println("Logging request: " + request);
super.handleRequest(request);
}
}

public class AuthenticationHandler extends Handler {
@Override
public void handleRequest(String request) {
System.out.println("Authentication successful.");
super.handleRequest(request);
}
}

public class AuthorizationHandler extends Handler {
@Override
public void handleRequest(String request) {
System.out.println("Authorization successful.");
super.handleRequest(request);
}
}

public class Client {
public static void main(String[] args) {
Handler logger = new LoggingHandler();
Handler authenticator = new AuthenticationHandler();
Handler authorizer = new AuthorizationHandler();

logger.setNext(authenticator);
authenticator.setNext(authorizer);

logger.handleRequest("Request data");
}
}
```

**Mediator**:

Use when you need to reduce the direct communication dependencies between objects by centralizing interactions.

**Signs you might need it**:

If direct communication between objects leads to overly complex and tightly coupled dependencies.

```java
public interface Mediator {
void sendMessage(String message, Person sender);
}

public abstract class Person {
protected Mediator mediator;

public Person(Mediator mediator) {
this.mediator = mediator;
}

public abstract void receiveMessage(String message);
}

public class ConcreteMediator implements Mediator {
private Person person1;
private Person person2;

public void setPerson1(Person person) {
this.person1 = person;
}

public void setPerson2(Person person) {
this.person2 = person;
}

@Override
public void sendMessage(String message, Person sender) {
if (sender.equals(person1)) {
person2.receiveMessage(message);
} else {
person1.receiveMessage(message);
}
}
}

public class User extends Person {
public User(Mediator mediator) {
super(mediator);
}

@Override
public void receiveMessage(String message) {
System.out.println("User received: " + message);
}
}

public class Client {
public static void main(String[] args) {
ConcreteMediator mediator = new ConcreteMediator();
User user1 = new User(mediator);
User user2 = new User(mediator);

mediator.setPerson1(user1);
mediator.setPerson2(user2);

user1.mediator.sendMessage("Hello from user1", user1);
}
}
```

**Visitor**:

Use when you need to perform new operations on elements of a complex object structure without modifying the elements.

**Signs you might need it**:

If adding new operations to a structure of objects requires modifying the objects themselves, breaking encapsulation and increasing rigidity.

```java
public interface Visitor {
void visit(Book book);
void visit(Game game);
}

public interface ItemElement {
void accept(Visitor visitor);
}

public class Book implements ItemElement {
private String title;

public Book(String title) {
this.title = title;
}

public String getTitle() {
return title;
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}

public class Game implements ItemElement {
private String name;

public game(String name) {
this.name = name;
}

public String getName() {
return name;
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}

public class ShoppingCartVisitor implements Visitor {
@Override
public void visit(Book book) {
System.out.println("Buying book: " + book.getTitle());
}

@Override
public void visit(Game game) {
System.out.println("Buying game: " + game.getName());
}
}

public class Client {
public static void main(String[] args) {
Book book = new Book("Design Patterns");
Game game = new Game("Chess");

Visitor visitor = new ShoppingCartVisitor();
book.accept(visitor);
game.accept(visitor);
}
}
```

0 comments on commit e765f17

Please sign in to comment.