diff --git a/content/post/dp.md b/content/post/dp.md new file mode 100644 index 0000000..e8fdada --- /dev/null +++ b/content/post/dp.md @@ -0,0 +1,978 @@ +--- +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 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 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 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 paymentStrategy; + int totalAmount = 1200; + + if (totalAmount > 1000) { + paymentStrategy = new BankTransferPayment(); + } else { + paymentStrategy = new CreditCardPayment(); + } + + paymentStrategy.pay(totalAmount); + } +} +``` + +**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) { + Command[] commands = new Command[] { + new TurnOnLightCommand(new Light()), + new TurnOffLightCommand(new Light()) + }; + + for (Command command : commands) { + command.execute(); + } + } +} +``` + +**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) { + List middleware = new ArrayList<>(); + middleware.add(new LoggingHandler()); + middleware.add(new AuthenticationHandler()); + middleware.add(new AuthorizationHandler()); + + for (int i = 0; i < middleware.size() - 1; i++) { + middleware.get(i).setNext(middleware.get(i + 1)); + } + + middleware.get(0).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 addConsumer(Consumer consumer); + void addProducer(Producer producer); + void sendMessage(Producer producer, String message); + String readMessage(Consumer consumer); +} + +public class MessageQueue implements Mediator { + private final List consumers = new ArrayList<>(); + private final List producers = new ArrayList<>(); + private final Queue messageQueue = new ArrayDeque<>(); + + @Override + public void addConsumer(Consumer consumer) { + consumers.add(consumer); + } + + @Override + public void addProducer(Producer producer) { + producers.add(producer); + } + + @Override + public void sendMessage(Producer producer, String message) { + if (!producers.contains(producer)) { + System.out.println("Producer not registered with the messaging queue."); + return; + } + + System.out.println("MessageQueue received message: " + message); + messageQueue.offer(message); + } + + @Override + public String readMessage(Consumer consumer) { + if (!consumers.contains(consumer)) { + System.out.println("Consumer not registered with the messaging queue."); + return null; + } + + System.out.println("MessageQueue sent message: " + message); + return messageQueue.poll(); + } +} + +public class Producer { + private final Mediator messageQueue; + private final String name; + + public Producer(Mediator messageQueue, String name) { + this.messageQueue = messageQueue; + this.name = name; + } + + public void produceMessage(String message) { + System.out.println(name + " sending: " + message); + messageQueue.sendMessage(this, message); + } +} + +public class Consumer { + private final Mediator messageQueue; + private final String name; + + public Consumer(Mediator messageQueue, String name) { + this.messageQueue = messageQueue; + this.name = name; + } + + public void readMessage() { + String message = messageQueue.readMessage(this); + + if (message) { + System.out.println(name + " read: " + message); + } else { + System.out.println(name + " found no messages to read."); + } + } +} + +public class Client { + public static void main(String[] args) { + MessageQueue queue = new MessageQueue(); + + Producer producer = new Producer(queue, "Producer"); + queue.addProducer(producer); + + Consumer consumer1 = new Consumer(queue, "Consumer1"); + Consumer consumer2 = new Consumer(queue, "Consumer2"); + + queue.addConsumer(consumer1); + queue.addConsumer(consumer2); + + producer.produceMessage("Message 1"); + producer.produceMessage("Message 2"); + + consumer1.readMessage(); + consumer2.readMessage(); + } +} +``` + +**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 ShoppingItem { + void accept(Visitor visitor); +} + +public class Book implements ShoppingItem { + 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 ShoppingItem { + 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) { + ShoppingItem[] shoppingCart = new ShoppingItem[]{ + new Book("Design Patterns"), + new Game("Chess") + }; + + Visitor visitor = new ShoppingCartVisitor(); + for (ShoppingItem item : shoppingCart) { + item.accept(visitor); + } + } +} +```