Esta quinta y última entrega se centra en los patrones de comportamiento: cómo colaboran los objetos, cómo se comunican y cómo reparten responsabilidades. Verás cuándo elegir cada patrón para desacoplar lógica, encapsular decisiones, propagar eventos o modelar flujos de trabajo, con implementaciones modernas en Java 25.
Patrones de Diseño de Comportamiento
Los patrones de comportamiento se centran en la comunicación entre objetos, definiendo cómo interactúan y distribuyen responsabilidades.
Chain of Responsibility
El patrón Chain of Responsibility permite pasar solicitudes a través de una cadena de manejadores potenciales. Cada manejador decide si procesa la solicitud o la pasa al siguiente en la cadena. Esto desacopla al emisor de la solicitud de sus receptores, dando a múltiples objetos la oportunidad de manejarla.
Este patrón proporciona flexibilidad para determinar dinámicamente qué objeto maneja cada solicitud. Es especialmente útil para implementar pipelines de procesamiento, middlewares o sistemas de validación donde múltiples filtros deben aplicarse en secuencia.
Ejemplo: Pipeline de middleware HTTP
// Interfaz del manejadorpublic interface HttpHandler { HttpResponse handle(HttpRequest request);}
// Clase base abstracta para middlewarepublic abstract class Middleware implements HttpHandler { protected HttpHandler next;
public Middleware setNext(HttpHandler next) { this.next = next; return this; }
protected HttpResponse passToNext(HttpRequest request) { if (next != null) { return next.handle(request); } return HttpResponse.notFound("No handler found"); }}
// Middleware de loggingpublic class LoggingMiddleware extends Middleware {
private static final Logger log = Logger.getLogger(LoggingMiddleware.class.getName());
@Override public HttpResponse handle(HttpRequest request) { long startTime = System.currentTimeMillis(); String requestId = UUID.randomUUID().toString().substring(0, 8);
log.info("[%s] → %s %s".formatted(requestId, request.method(), request.path()));
HttpResponse response = passToNext(request);
long duration = System.currentTimeMillis() - startTime; log.info("[%s] ← %d (%dms)".formatted(requestId, response.status(), duration));
return response; }}
// Middleware de cachépublic class CacheMiddleware extends Middleware {
private final Cache<String, HttpResponse> cache; private final Duration ttl;
public CacheMiddleware(Cache<String, HttpResponse> cache, Duration ttl) { this.cache = cache; this.ttl = ttl; }
@Override public HttpResponse handle(HttpRequest request) { // Solo cachear GET requests if (!"GET".equals(request.method())) { return passToNext(request); }
String cacheKey = request.method() + ":" + request.path() + ":" + request.queryString();
HttpResponse cached = cache.get(cacheKey); if (cached != null) { return cached.withHeader("X-Cache", "HIT"); }
HttpResponse response = passToNext(request);
if (response.status() == 200) { cache.put(cacheKey, response, ttl); }
return response.withHeader("X-Cache", "MISS"); }}
// Controlador final (termina la cadena)public class ApiController implements HttpHandler {
private final Map<String, Function<HttpRequest, HttpResponse>> routes;
public ApiController() { this.routes = new HashMap<>(); }
public void addRoute(String path, Function<HttpRequest, HttpResponse> handler) { routes.put(path, handler); }
@Override public HttpResponse handle(HttpRequest request) { return routes.getOrDefault(request.path(), r -> HttpResponse.notFound("Route not found")) .apply(request); }}
// Builder para construir el pipelinepublic class MiddlewarePipeline {
private final List<Middleware> middlewares = new ArrayList<>(); private HttpHandler finalHandler;
public MiddlewarePipeline use(Middleware middleware) { middlewares.add(middleware); return this; }
public MiddlewarePipeline endpoint(HttpHandler handler) { this.finalHandler = handler; return this; }
public HttpHandler build() { if (finalHandler == null) { throw new IllegalStateException("Final handler not set"); }
HttpHandler current = finalHandler; for (int i = middlewares.size() - 1; i >= 0; i--) { middlewares.get(i).setNext(current); current = middlewares.get(i); }
return current; }}
// Uso del Chain of Responsibilityfinal var controller = new ApiController();controller.addRoute("/api/users", req -> HttpResponse.ok("{\"users\": []}"));controller.addRoute("/api/products", req -> HttpResponse.ok("{\"products\": []}"));
final var pipeline = new MiddlewarePipeline() .use(new LoggingMiddleware()) .use(new CacheMiddleware(new LRUCache<>(), Duration.ofMinutes(5))) .endpoint(controller) .build();
HttpResponse response = pipeline.handle(new HttpRequest("GET", "/api/users"));Command
El patrón Command encapsula una solicitud como un objeto, permitiendo parametrizar clientes con diferentes solicitudes, encolar o registrar solicitudes, y soportar operaciones reversibles. Desacopla el objeto que invoca la operación del que sabe cómo ejecutarla.
Cada comando es un objeto autónomo que contiene toda la información necesaria para ejecutar una acción. Esto permite crear colas de comandos, implementar undo/redo, registrar historial de operaciones y crear comandos compuestos (macros).
Ejemplo: Sistema de editor de texto con undo/redo
// Interfaz Commandpublic interface TextCommand { void execute(); void undo(); String description();}
// Receptor - el documento sobre el que operan los comandospublic class TextDocument { private final StringBuilder content; private final List<TextDocumentListener> listeners = new ArrayList<>();
public TextDocument() { this.content = new StringBuilder(); }
public TextDocument(String initialContent) { this.content = new StringBuilder(initialContent); }
public void insert(int position, String text) { content.insert(position, text); notifyListeners(); }
public String delete(int start, int length) { String deleted = content.substring(start, start + length); content.delete(start, start + length); notifyListeners(); return deleted; }
public String replace(int start, int end, String newText) { String replaced = content.substring(start, end); content.replace(start, end, newText); notifyListeners(); return replaced; }
public String getText() { return content.toString(); }
public int length() { return content.length(); }
public void addListener(TextDocumentListener listener) { listeners.add(listener); }
private void notifyListeners() { listeners.forEach(l -> l.onDocumentChanged(this)); }}
// Comandos concretospublic final class InsertCommand implements TextCommand { private final TextDocument document; private final int position; private final String text;
public InsertCommand(TextDocument document, int position, String text) { this.document = document; this.position = position; this.text = text; }
@Override public void execute() { document.insert(position, text); }
@Override public void undo() { document.delete(position, text.length()); }
@Override public String description() { return "Insert '%s' at position %d".formatted( text.length() > 20 ? text.substring(0, 20) + "..." : text, position); }}
public final class DeleteCommand implements TextCommand { private final TextDocument document; private final int start; private final int length; private String deletedText;
public DeleteCommand(TextDocument document, int start, int length) { this.document = document; this.start = start; this.length = length; }
@Override public void execute() { deletedText = document.delete(start, length); }
@Override public void undo() { document.insert(start, deletedText); }
@Override public String description() { return "Delete %d characters at position %d".formatted(length, start); }}
// Otros comandos ...
// Comando compuesto (Macro)public final class MacroCommand implements TextCommand { private final String name; private final List<TextCommand> commands;
public MacroCommand(String name, List<TextCommand> commands) { this.name = name; this.commands = new ArrayList<>(commands); }
@Override public void execute() { commands.forEach(TextCommand::execute); }
@Override public void undo() { // Undo en orden inverso for (int i = commands.size() - 1; i >= 0; i--) { commands.get(i).undo(); } }
@Override public String description() { return "Macro '%s' (%d commands)".formatted(name, commands.size()); }}
// Invoker - maneja el historial y undo/redopublic class CommandHistory { private final Deque<TextCommand> undoStack = new ArrayDeque<>(); private final Deque<TextCommand> redoStack = new ArrayDeque<>(); private final int maxHistory;
public CommandHistory(int maxHistory) { this.maxHistory = maxHistory; }
public void execute(TextCommand command) { command.execute(); undoStack.push(command); redoStack.clear(); // Limpiar redo al ejecutar nuevo comando
// Mantener límite de historial while (undoStack.size() > maxHistory) { undoStack.removeLast(); } }
public boolean canUndo() { return !undoStack.isEmpty(); }
public boolean canRedo() { return !redoStack.isEmpty(); }
public void undo() { if (canUndo()) { TextCommand command = undoStack.pop(); command.undo(); redoStack.push(command); } }
public void redo() { if (canRedo()) { TextCommand command = redoStack.pop(); command.execute(); undoStack.push(command); } }
public List<String> getUndoHistory() { return undoStack.stream().map(TextCommand::description).toList(); }
public List<String> getRedoHistory() { return redoStack.stream().map(TextCommand::description).toList(); }}
// Cliente - Editor de textopublic class TextEditor { private final TextDocument document; private final CommandHistory history; private int cursorPosition = 0;
public TextEditor() { this.document = new TextDocument(); this.history = new CommandHistory(100); }
public void type(String text) { history.execute(new InsertCommand(document, cursorPosition, text)); cursorPosition += text.length(); }
public void delete(int length) { if (cursorPosition > 0 && length > 0) { int deleteStart = Math.max(0, cursorPosition - length); int actualLength = cursorPosition - deleteStart; history.execute(new DeleteCommand(document, deleteStart, actualLength)); cursorPosition = deleteStart; } }
public void replaceSelection(int selectionLength, String newText) { history.execute(new ReplaceCommand( document, cursorPosition, cursorPosition + selectionLength, newText)); cursorPosition += newText.length(); }
public void undo() { history.undo(); } public void redo() { history.redo(); }
public String getText() { return document.getText(); }}
// Uso del patrón Commandfinal var editor = new TextEditor();
editor.type("Hello ");editor.type("World");IO.println(editor.getText()); // "Hello World"
editor.undo();IO.println(editor.getText()); // "Hello "
editor.redo();IO.println(editor.getText()); // "Hello World"
editor.type("!");IO.println(editor.getText()); // "Hello World!"Interpreter
El patrón Interpreter define una representación gramatical para un lenguaje y un intérprete que usa esa representación para interpretar sentencias del lenguaje. Es útil cuando existe un problema que ocurre frecuentemente y puede expresarse mediante un lenguaje simple.
El patrón utiliza un árbol de sintaxis abstracta (AST) donde cada nodo es una expresión. Las expresiones terminales representan elementos atómicos del lenguaje, mientras que las no terminales componen otras expresiones. Aunque no es el más eficiente, es ideal para lenguajes simples de dominio específico (DSL).
Ejemplo: Intérprete de expresiones de filtrado
// Contexto de interpretaciónpublic record FilterContext<T>(T item, Map<String, Object> variables) {
@SuppressWarnings("unchecked") public <V> V getVariable(String name) { return (V) variables.get(name); }
public Object getProperty(String propertyName) { try { var getter = item.getClass().getMethod(propertyName); return getter.invoke(item); } catch (Exception e) { throw new RuntimeException("Cannot access property: " + propertyName, e); } }}
// Expresión abstractapublic sealed interface FilterExpression<T> permits PropertyExpression, LiteralExpression, ComparisonExpression, AndExpression, OrExpression, NotExpression, InExpression {
boolean interpret(FilterContext<T> context); String toQueryString();}
// Expresiones terminalespublic record PropertyExpression<T>(String propertyName) implements FilterExpression<T> { @Override public boolean interpret(FilterContext<T> context) { Object value = context.getProperty(propertyName); return value instanceof Boolean b ? b : value != null; }
@Override public String toQueryString() { return propertyName; }
public Object getValue(FilterContext<T> context) { return context.getProperty(propertyName); }}
// Expresiones no terminales - comparacionespublic record ComparisonExpression<T>( PropertyExpression<T> left, ComparisonOperator operator, FilterExpression<T> right) implements FilterExpression<T> {
public enum ComparisonOperator { EQUALS("="), NOT_EQUALS("!="), GREATER_THAN(">"), LESS_THAN("<"), GREATER_OR_EQUALS(">="), LESS_OR_EQUALS("<="), CONTAINS("CONTAINS"), STARTS_WITH("STARTS_WITH");
private final String symbol; ComparisonOperator(String symbol) { this.symbol = symbol; } public String symbol() { return symbol; } }
@Override public boolean interpret(FilterContext<T> context) { Object leftValue = left.getValue(context); Object rightValue = right instanceof LiteralExpression<T> lit ? lit.value() : right.interpret(context);
return switch (operator) { case EQUALS -> Objects.equals(leftValue, rightValue); case NOT_EQUALS -> !Objects.equals(leftValue, rightValue); case GREATER_THAN -> compare(leftValue, rightValue) > 0; case LESS_THAN -> compare(leftValue, rightValue) < 0; case GREATER_OR_EQUALS -> compare(leftValue, rightValue) >= 0; case LESS_OR_EQUALS -> compare(leftValue, rightValue) <= 0; case CONTAINS -> String.valueOf(leftValue).contains(String.valueOf(rightValue)); case STARTS_WITH -> String.valueOf(leftValue).startsWith(String.valueOf(rightValue)); }; }
@SuppressWarnings("unchecked") private int compare(Object a, Object b) { if (a instanceof Comparable c1 && b instanceof Comparable c2) { return c1.compareTo(c2); } throw new IllegalArgumentException("Cannot compare: " + a + " with " + b); }
@Override public String toQueryString() { return "%s %s %s".formatted(left.toQueryString(), operator.symbol(), right.toQueryString()); }}
// Expresiones lógicaspublic record AndExpression<T>(FilterExpression<T> left, FilterExpression<T> right) implements FilterExpression<T> {
@Override public boolean interpret(FilterContext<T> context) { return left.interpret(context) && right.interpret(context); }
@Override public String toQueryString() { return "(%s AND %s)".formatted(left.toQueryString(), right.toQueryString()); }}
public record OrExpression<T>(FilterExpression<T> left, FilterExpression<T> right) implements FilterExpression<T> {
@Override public boolean interpret(FilterContext<T> context) { return left.interpret(context) || right.interpret(context); }
@Override public String toQueryString() { return "(%s OR %s)".formatted(left.toQueryString(), right.toQueryString()); }}
// Otras implementaciones
// Builder DSL para construir expresionespublic class FilterBuilder<T> {
public PropertyBuilder<T> where(String property) { return new PropertyBuilder<>(new PropertyExpression<>(property)); }
public static class PropertyBuilder<T> { private final PropertyExpression<T> property;
PropertyBuilder(PropertyExpression<T> property) { this.property = property; }
public FilterExpression<T> equals(Object value) { return new ComparisonExpression<>(property, ComparisonExpression.ComparisonOperator.EQUALS, new LiteralExpression<>(value)); }
public FilterExpression<T> greaterThan(Object value) { return new ComparisonExpression<>(property, ComparisonExpression.ComparisonOperator.GREATER_THAN, new LiteralExpression<>(value)); }
public FilterExpression<T> contains(String value) { return new ComparisonExpression<>(property, ComparisonExpression.ComparisonOperator.CONTAINS, new LiteralExpression<>(value)); }
public FilterExpression<T> in(Object... values) { return new InExpression<>(property, List.of(values)); } }
public static <T> FilterExpression<T> and(FilterExpression<T> left, FilterExpression<T> right) { return new AndExpression<>(left, right); }
public static <T> FilterExpression<T> or(FilterExpression<T> left, FilterExpression<T> right) { return new OrExpression<>(left, right); }
public static <T> FilterExpression<T> not(FilterExpression<T> expr) { return new NotExpression<>(expr); }}
// Uso del Interpreterpublic record Product(String name, String category, double price, boolean active) {}
final var filter = new FilterBuilder<Product>();
// Construir expresión: category = 'Electronics' AND price > 100 AND active = truefinal FilterExpression<Product> expression = FilterBuilder.and( FilterBuilder.and( filter.where("category").equals("Electronics"), filter.where("price").greaterThan(100.0) ), filter.where("active").equals(true));
IO.println("Query: " + expression.toQueryString());// Output: ((category = 'Electronics' AND price > 100.0) AND active = true)
// Filtrar productosfinal List<Product> products = List.of( new Product("Laptop", "Electronics", 999.99, true), new Product("Mouse", "Electronics", 29.99, true), new Product("Desk", "Furniture", 199.99, true), new Product("Monitor", "Electronics", 299.99, false));
final var filtered = products.stream() .filter(p -> expression.interpret(new FilterContext<>(p, Map.of()))) .toList();
IO.println("Filtered: " + filtered);// Output: [Product[name=Laptop, category=Electronics, price=999.99, active=true]]Iterator
El patrón Iterator proporciona una forma de acceder secuencialmente a los elementos de una colección sin exponer su representación interna. Separa la responsabilidad de recorrer la colección de la colección misma, permitiendo diferentes estrategias de iteración.
Este patrón ofrece flexibilidad para implementar múltiples formas de recorrer una estructura de datos. En Java moderno, el patrón está integrado en el lenguaje a través de Iterable, Iterator y la API de Streams. Sin embargo, sigue siendo valioso implementarlo explícitamente para estructuras de datos personalizadas o cuando se necesitan iteradores especializados.
Ejemplo: Iteradores personalizados para árbol binario
// Estructura del nodopublic class TreeNode<T> { private final T value; private TreeNode<T> left; private TreeNode<T> right;
public TreeNode(T value) { this.value = value; }
public T value() { return value; } public TreeNode<T> left() { return left; } public TreeNode<T> right() { return right; }
public void setLeft(TreeNode<T> left) { this.left = left; } public void setRight(TreeNode<T> right) { this.right = right; }}
// Árbol binario con múltiples estrategias de iteraciónpublic class BinaryTree<T> implements Iterable<T> {
private TreeNode<T> root;
public enum TraversalOrder { IN_ORDER }
private TraversalOrder defaultOrder = TraversalOrder.IN_ORDER;
public void setRoot(TreeNode<T> root) { this.root = root; }
public void setDefaultTraversal(TraversalOrder order) { this.defaultOrder = order; }
@Override public Iterator<T> iterator() { return iterator(defaultOrder); }
public Iterator<T> iterator(TraversalOrder order) { return switch (order) { case IN_ORDER -> new InOrderIterator<>(root); }; }
public Iterable<T> inOrder() { return () -> iterator(TraversalOrder.IN_ORDER); }
// Stream support public Stream<T> stream() { return stream(defaultOrder); }
public Stream<T> stream(TraversalOrder order) { return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iterator(order), Spliterator.ORDERED), false ); }}
// Iterador In-Order (izquierda -> raíz -> derecha)class InOrderIterator<T> implements Iterator<T> { private final Deque<TreeNode<T>> stack = new ArrayDeque<>();
InOrderIterator(TreeNode<T> root) { pushLeft(root); }
private void pushLeft(TreeNode<T> node) { while (node != null) { stack.push(node); node = node.left(); } }
@Override public boolean hasNext() { return !stack.isEmpty(); }
@Override public T next() { if (!hasNext()) throw new NoSuchElementException(); TreeNode<T> current = stack.pop(); pushLeft(current.right()); return current.value(); }}
// Uso del patrón Iteratorfinal var tree = new BinaryTree<Integer>();// 4// / \// 2 6// / \ / \// 1 3 5 7
final var root = new TreeNode<>(4);root.setLeft(new TreeNode<>(2));root.setRight(new TreeNode<>(6));root.left().setLeft(new TreeNode<>(1));root.left().setRight(new TreeNode<>(3));root.right().setLeft(new TreeNode<>(5));root.right().setRight(new TreeNode<>(7));tree.setRoot(root);
// Diferentes formas de iterarIO.println("In-Order: " + tree.stream(TraversalOrder.IN_ORDER).toList());// [1, 2, 3, 4, 5, 6, 7]
// Uso con for-eachfor (Integer value : tree.inOrder()) { System.out.print(value + " ");}
// Uso con Streams para operaciones funcionalesint sum = tree.stream() .filter(n -> n % 2 == 0) .mapToInt(Integer::intValue) .sum();Mediator
El patrón Mediator define un objeto que encapsula cómo interactúan un conjunto de objetos. Promueve el acoplamiento débil evitando que los objetos se refieran entre sí explícitamente, y permite variar sus interacciones de forma independiente.
Este patrón centraliza el control de las comunicaciones complejas entre objetos relacionados. Los objetos participantes (colegas) solo conocen al mediador, no a los otros objetos con los que interactúan. Esto simplifica el mantenimiento y hace más fácil reutilizar los objetos individuales.
Ejemplo: Chat room como mediador
// Interfaz del mediadorpublic interface ChatMediator { void sendMessage(String message, User sender); void sendPrivateMessage(String message, User sender, User recipient); void addUser(User user); void removeUser(User user); List<User> getOnlineUsers();}
// Interfaz del colega (usuario)public abstract class User { protected final String id; protected final String name; protected ChatMediator mediator; protected boolean online;
protected User(String id, String name) { this.id = id; this.name = name; this.online = false; }
public String id() { return id; } public String name() { return name; } public boolean isOnline() { return online; }
public void join(ChatMediator mediator) { this.mediator = mediator; this.online = true; mediator.addUser(this); }
public void leave() { if (mediator != null) { mediator.removeUser(this); this.online = false; } }
public abstract void send(String message); public abstract void sendPrivate(String message, User recipient); public abstract void receive(String message, User sender); public abstract void receivePrivate(String message, User sender);}
// Implementación concreta del usuariopublic class ChatUser extends User { private final List<ChatMessage> messageHistory = new ArrayList<>();
public record ChatMessage(String content, String senderId, Instant timestamp, boolean isPrivate) {}
public ChatUser(String id, String name) { super(id, name); }
@Override public void send(String message) { if (mediator != null && online) { IO.println("[" + name + "] sends: " + message); mediator.sendMessage(message, this); } }
@Override public void sendPrivate(String message, User recipient) { if (mediator != null && online) { IO.println("[" + name + "] whispers to [" + recipient.name() + "]: " + message); mediator.sendPrivateMessage(message, this, recipient); } }
@Override public void receive(String message, User sender) { var chatMessage = new ChatMessage(message, sender.id(), Instant.now(), false); messageHistory.add(chatMessage); IO.println(" [" + name + "] received from [" + sender.name() + "]: " + message); }
@Override public void receivePrivate(String message, User sender) { var chatMessage = new ChatMessage(message, sender.id(), Instant.now(), true); messageHistory.add(chatMessage); IO.println(" [" + name + "] received private from [" + sender.name() + "]: " + message); }
public List<ChatMessage> getHistory() { return Collections.unmodifiableList(messageHistory); }}
// Implementación del mediadorpublic class ChatRoom implements ChatMediator { private final String roomId; private final String roomName; private final Set<User> users = ConcurrentHashMap.newKeySet(); private final List<BroadcastMessage> broadcastHistory = new ArrayList<>(); private final Map<String, List<String>> blockedUsers = new ConcurrentHashMap<>();
public record BroadcastMessage(String content, String senderId, Instant timestamp) {}
public ChatRoom(String roomId, String roomName) { this.roomId = roomId; this.roomName = roomName; }
@Override public void sendMessage(String message, User sender) { broadcastHistory.add(new BroadcastMessage(message, sender.id(), Instant.now()));
// El mediador coordina quién recibe el mensaje users.stream() .filter(user -> !user.equals(sender)) .filter(User::isOnline) .filter(user -> !isBlocked(sender.id(), user.id())) .forEach(user -> user.receive(message, sender)); }
@Override public void sendPrivateMessage(String message, User sender, User recipient) { if (users.contains(recipient) && recipient.isOnline() && !isBlocked(sender.id(), recipient.id())) { recipient.receivePrivate(message, sender); } }
@Override public void addUser(User user) { users.add(user); // Notificar a otros usuarios sendSystemMessage(user.name() + " se ha unido al chat"); }
@Override public void removeUser(User user) { users.remove(user); sendSystemMessage(user.name() + " ha salido del chat"); }
@Override public List<User> getOnlineUsers() { return users.stream().filter(User::isOnline).toList(); }
public void blockUser(String blockerId, String blockedId) { blockedUsers.computeIfAbsent(blockerId, k -> new ArrayList<>()).add(blockedId); }
private boolean isBlocked(String senderId, String recipientId) { return blockedUsers.getOrDefault(recipientId, List.of()).contains(senderId); }
private void sendSystemMessage(String message) { users.stream() .filter(User::isOnline) .forEach(user -> IO.println(" [SYSTEM -> " + user.name() + "]: " + message)); }}
// Uso del patrón Mediatorvar chatRoom = new ChatRoom("room-1", "General");
var alice = new ChatUser("1", "Alice");var bob = new ChatUser("2", "Bob");var charlie = new ChatUser("3", "Charlie");
// Los usuarios se unen a través del mediadoralice.join(chatRoom);bob.join(chatRoom);charlie.join(chatRoom);
// Comunicación mediadaalice.send("Hola a todos!");// [Alice] sends: Hola a todos!// [Bob] received from [Alice]: Hola a todos!// [Charlie] received from [Alice]: Hola a todos!
bob.sendPrivate("Hola Alice, ¿cómo estás?", alice);// [Bob] whispers to [Alice]: Hola Alice, ¿cómo estás?// [Alice] received private from [Bob]: Hola Alice, ¿cómo estás?
// Bloquear usuariochatRoom.blockUser("Charlie", "Bob");bob.send("Este mensaje no llegará a Charlie");
alice.leave();// [SYSTEM -> Bob]: Alice ha salido del chat// [SYSTEM -> Charlie]: Alice ha salido del chatMemento
El patrón Memento permite capturar y externalizar el estado interno de un objeto sin violar su encapsulamiento, de modo que pueda restaurarse a ese estado posteriormente. Es especialmente útil para implementar funcionalidades de undo, checkpoints o recuperación ante errores.
El patrón involucra tres participantes: el Originator (el objeto cuyo estado se guarda), el Memento (almacena el estado) y el Caretaker (gestiona los mementos sin acceder a su contenido). El Memento tiene una interfaz estrecha para el Caretaker pero amplia para el Originator.
Ejemplo: Editor de código con snapshots
// Memento - almacena el estadopublic final class EditorMemento { private final String content; private final int cursorPosition; private final Set<Integer> breakpoints; private final Instant savedAt; private final String description;
// Constructor package-private - solo Originator puede crear mementos EditorMemento(String content, int cursorPosition, Set<Integer> breakpoints, String description) { this.content = content; this.cursorPosition = cursorPosition; this.breakpoints = Set.copyOf(breakpoints); this.savedAt = Instant.now(); this.description = description; }
// Interfaz pública limitada para el Caretaker public Instant savedAt() { return savedAt; } public String description() { return description; }
// Métodos package-private para el Originator String content() { return content; } int cursorPosition() { return cursorPosition; } Set<Integer> breakpoints() { return breakpoints; }}
// Originator - el editorpublic class CodeEditor { private StringBuilder content; private int cursorPosition; private final Set<Integer> breakpoints; private String filename;
public CodeEditor() { this.content = new StringBuilder(); this.cursorPosition = 0; this.breakpoints = new HashSet<>(); }
public void newFile(String filename) { this.filename = filename; this.content = new StringBuilder(); this.cursorPosition = 0; this.breakpoints.clear(); }
public void type(String text) { content.insert(cursorPosition, text); cursorPosition += text.length(); }
public void delete(int count) { int start = Math.max(0, cursorPosition - count); content.delete(start, cursorPosition); cursorPosition = start; }
public void moveCursor(int position) { this.cursorPosition = Math.max(0, Math.min(position, content.length())); }
public void toggleBreakpoint(int line) { if (breakpoints.contains(line)) { breakpoints.remove(line); } else { breakpoints.add(line); } }
public String getContent() { return content.toString(); }
// Crear memento public EditorMemento save(String description) { return new EditorMemento( content.toString(), cursorPosition, breakpoints, description ); }
// Restaurar desde memento public void restore(EditorMemento memento) { this.content = new StringBuilder(memento.content()); this.cursorPosition = memento.cursorPosition(); this.breakpoints.clear(); this.breakpoints.addAll(memento.breakpoints()); }
public void displayStatus() { IO.println("=== Editor Status ==="); IO.println("File: " + filename); IO.println("Content: " + content); IO.println("Cursor: " + cursorPosition); IO.println("Breakpoints: " + breakpoints); IO.println("===================="); }}
// Caretaker - gestiona el historial de mementospublic class EditorHistory { private final Deque<EditorMemento> history = new ArrayDeque<>(); private final Deque<EditorMemento> redoStack = new ArrayDeque<>(); private final int maxSnapshots;
public EditorHistory(int maxSnapshots) { this.maxSnapshots = maxSnapshots; }
public void save(EditorMemento memento) { history.push(memento); redoStack.clear();
while (history.size() > maxSnapshots) { history.removeLast(); } }
public Optional<EditorMemento> undo() { if (history.size() > 1) { EditorMemento current = history.pop(); redoStack.push(current); return Optional.of(history.peek()); } return Optional.empty(); }
public Optional<EditorMemento> redo() { if (!redoStack.isEmpty()) { EditorMemento memento = redoStack.pop(); history.push(memento); return Optional.of(memento); } return Optional.empty(); }
public List<String> getSnapshotDescriptions() { return history.stream() .map(m -> "[%s] %s".formatted( m.savedAt().toString().substring(11, 19), m.description())) .toList(); }
public Optional<EditorMemento> getSnapshot(int index) { return history.stream().skip(index).findFirst(); }}
// Facade para simplificar el usopublic class EditorWithHistory { private final CodeEditor editor; private final EditorHistory history;
public EditorWithHistory() { this.editor = new CodeEditor(); this.history = new EditorHistory(50); }
public void newFile(String filename) { editor.newFile(filename); saveSnapshot("New file: " + filename); }
public void type(String text) { editor.type(text); saveSnapshot("Typed: " + truncate(text, 20)); }
public void delete(int count) { editor.delete(count); saveSnapshot("Deleted " + count + " chars"); }
public void undo() { history.undo().ifPresent(memento -> { editor.restore(memento); IO.println("Undo: " + memento.description()); }); }
public void redo() { history.redo().ifPresent(memento -> { editor.restore(memento); IO.println("Redo: " + memento.description()); }); }
public void showHistory() { IO.println("=== Snapshot History ==="); history.getSnapshotDescriptions().forEach(System.out::println); }
private void saveSnapshot(String description) { history.save(editor.save(description)); }
private String truncate(String s, int maxLen) { return s.length() > maxLen ? s.substring(0, maxLen) + "..." : s; }
public String getContent() { return editor.getContent(); } public void displayStatus() { editor.displayStatus(); }}
// Uso del patrón Mementovar editor = new EditorWithHistory();
editor.newFile("Main.java");editor.type("public class Main {\n");editor.type(" public static void main(String[] args) {\n");editor.type(" IO.println(\"Hello\");\n");editor.type(" }\n");editor.type("}");
IO.println(editor.getContent());
editor.showHistory();
// Deshacer cambioseditor.undo();editor.undo();IO.println("After 2 undos:\n" + editor.getContent());
// Rehacereditor.redo();IO.println("After redo:\n" + editor.getContent());Observer
El patrón Observer define una relación de uno a muchos entre objetos, de modo que cuando un objeto (subject) cambia su estado, todos sus dependientes (observers) son notificados y actualizados automáticamente. También conocido como publish-subscribe, este patrón es fundamental para implementar sistemas de eventos desacoplados.
El subject mantiene una lista de observers sin conocer sus clases concretas, proporcionando máxima flexibilidad. Los observers pueden suscribirse, desuscribirse o ser notificados de cambios específicos. Un objeto puede actuar simultáneamente como subject y observer de otros objetos.
Ejemplo: Sistema de eventos reactivo
// Eventos tipadospublic interface DomainEvent { String eventId(); Instant occurredAt(); String aggregateId();}
public record OrderCreated( String eventId, Instant occurredAt, String aggregateId, String customerId, List<OrderItem> items, Money total) implements DomainEvent {}
public record OrderShipped( String eventId, Instant occurredAt, String aggregateId, String trackingNumber, String carrier) implements DomainEvent {}
// Observer interface con soporte genérico@FunctionalInterfacepublic interface EventListener<E extends DomainEvent> { void onEvent(E event);}
// Subject - EventBus centralpublic class EventBus {
private final Map<Class<? extends DomainEvent>, Set<EventListener<?>>> listeners = new ConcurrentHashMap<>();
private final ExecutorService asyncExecutor = Executors.newVirtualThreadPerTaskExecutor();
@SuppressWarnings("unchecked") public <E extends DomainEvent> Subscription subscribe(Class<E> eventType, EventListener<E> listener) { listeners.computeIfAbsent(eventType, k -> ConcurrentHashMap.newKeySet()) .add(listener);
return () -> unsubscribe(eventType, listener); }
public <E extends DomainEvent> void unsubscribe(Class<E> eventType, EventListener<E> listener) { var eventListeners = listeners.get(eventType); if (eventListeners != null) { eventListeners.remove(listener); } }
@SuppressWarnings("unchecked") public <E extends DomainEvent> void publish(E event) { var eventListeners = listeners.get(event.getClass()); if (eventListeners != null) { eventListeners.forEach(listener -> ((EventListener<E>) listener).onEvent(event)); } }
@SuppressWarnings("unchecked") public <E extends DomainEvent> void publishAsync(E event) { var eventListeners = listeners.get(event.getClass()); if (eventListeners != null) { eventListeners.forEach(listener -> asyncExecutor.submit(() -> { try { ((EventListener<E>) listener).onEvent(event); } catch (Exception e) { System.err.println("Error handling event: " + e.getMessage()); } })); } }
// Interface funcional para unsubscribe @FunctionalInterface public interface Subscription extends AutoCloseable { void unsubscribe();
@Override default void close() { unsubscribe(); } }}
// Observers concretospublic class InventoryService implements EventListener<OrderCreated> {
@Override public void onEvent(OrderCreated event) { IO.println("📦 Reserving inventory for order: " + event.aggregateId()); event.items().forEach(item -> IO.println(" - Reserving " + item.quantity() + " x " + item.productId())); }}
public class AnalyticsService {
private final Map<String, AtomicInteger> ordersByCustomer = new ConcurrentHashMap<>(); private final AtomicLong totalRevenue = new AtomicLong();
public void trackOrderCreated(OrderCreated event) { ordersByCustomer.computeIfAbsent(event.customerId(), k -> new AtomicInteger()) .incrementAndGet(); IO.println("📊 Analytics: New order tracked for customer " + event.customerId()); }}
// Otras implementaciones de observers...
// Uso del patrón Observerfinal var eventBus = new EventBus();
// Registrar observersfinal var emailSender = new EmailSender();final var notificationService = new NotificationService(emailSender);final var inventoryService = new InventoryService();final var analyticsService = new AnalyticsService();final var shippingNotifier = new ShippingNotifier();
// Suscripciones - diferentes observers para el mismo eventofinal var sub1 = eventBus.subscribe(OrderCreated.class, notificationService);final var sub2 = eventBus.subscribe(OrderCreated.class, inventoryService);final var sub3 = eventBus.subscribe(OrderCreated.class, analyticsService::trackOrderCreated);final var sub4 = eventBus.subscribe(OrderShipped.class, shippingNotifier);
// Publicar eventosfinal var orderCreated = new OrderCreated( UUID.randomUUID().toString(), Instant.now(), "ORD-001", "CUST-123", List.of(new OrderItem("PROD-A", 2), new OrderItem("PROD-B", 1)), new Money(299.99, "USD"));
eventBus.publish(orderCreated);// Output:// 📧 Sending order confirmation for order: ORD-001// 📦 Reserving inventory for order: ORD-001// - Reserving 2 x PROD-A// - Reserving 1 x PROD-B// 📊 Analytics: New order tracked for customer CUST-123
// Desuscribirsesub1.unsubscribe();// O usando try-with-resourcestry (var subscription = eventBus.subscribe(OrderCreated.class, e -> IO.println("Temporary listener: " + e.aggregateId()))) { eventBus.publish(orderCreated);}State
El patrón State permite que un objeto altere su comportamiento cuando cambia su estado interno. El objeto parecerá cambiar de clase, ya que cada estado encapsula el comportamiento específico asociado a él en una clase separada.
Este patrón elimina las sentencias condicionales extensas que verifican el estado actual del objeto. En su lugar, cada estado posible se representa como una clase que implementa el comportamiento apropiado. El contexto delega las operaciones al objeto de estado actual, y las transiciones entre estados pueden ser gestionadas por el contexto o por los propios estados.
Ejemplo: Máquina de estados para proceso de pedido
// Interfaz Statepublic sealed interface OrderState permits DraftState, PendingPaymentState, PaidState, ShippedState, DeliveredState, CancelledState {
void addItem(OrderContext order, OrderItem item); void removeItem(OrderContext order, String productId); void submitOrder(OrderContext order); void processPayment(OrderContext order, PaymentInfo payment); void shipOrder(OrderContext order, ShippingInfo shipping); void deliverOrder(OrderContext order); void cancelOrder(OrderContext order, String reason);
String stateName();
// Comportamiento por defecto - operación no permitida default void throwInvalidOperation(String operation) { throw new IllegalStateException( "Cannot " + operation + " in state: " + stateName()); }}
// Estados concretospublic final class DraftState implements OrderState { public static final DraftState INSTANCE = new DraftState(); private DraftState() {}
@Override public String stateName() { return "DRAFT"; }
@Override public void addItem(OrderContext order, OrderItem item) { order.getItems().add(item); IO.println("✓ Item added: " + item); }
@Override public void removeItem(OrderContext order, String productId) { order.getItems().removeIf(i -> i.productId().equals(productId)); IO.println("✓ Item removed: " + productId); }
@Override public void submitOrder(OrderContext order) { if (order.getItems().isEmpty()) { throw new IllegalStateException("Cannot submit empty order"); } order.setState(PendingPaymentState.INSTANCE); IO.println("✓ Order submitted, awaiting payment"); }
@Override public void processPayment(OrderContext order, PaymentInfo payment) { throwInvalidOperation("process payment"); }
@Override public void shipOrder(OrderContext order, ShippingInfo shipping) { throwInvalidOperation("ship"); }
@Override public void deliverOrder(OrderContext order) { throwInvalidOperation("deliver"); }
@Override public void cancelOrder(OrderContext order, String reason) { order.setState(CancelledState.INSTANCE); order.setCancellationReason(reason); IO.println("✓ Draft order cancelled: " + reason); }}
public final class PendingPaymentState implements OrderState { public static final PendingPaymentState INSTANCE = new PendingPaymentState(); private PendingPaymentState() {}
@Override public String stateName() { return "PENDING_PAYMENT"; }
@Override public void addItem(OrderContext order, OrderItem item) { throwInvalidOperation("add items"); }
@Override public void removeItem(OrderContext order, String productId) { throwInvalidOperation("remove items"); }
@Override public void submitOrder(OrderContext order) { throwInvalidOperation("submit"); }
@Override public void processPayment(OrderContext order, PaymentInfo payment) { // Validar y procesar pago if (payment.amount().compareTo(order.getTotal()) < 0) { throw new IllegalArgumentException("Insufficient payment amount"); } order.setPaymentInfo(payment); order.setState(PaidState.INSTANCE); IO.println("✓ Payment processed: " + payment.amount()); }
@Override public void shipOrder(OrderContext order, ShippingInfo shipping) { throwInvalidOperation("ship"); }
@Override public void deliverOrder(OrderContext order) { throwInvalidOperation("deliver"); }
@Override public void cancelOrder(OrderContext order, String reason) { order.setState(CancelledState.INSTANCE); order.setCancellationReason(reason); IO.println("✓ Order cancelled before payment: " + reason); }}
// Implementación de otros estados
// Context@Getter@Setterpublic class OrderContext { private final String orderId; private final List<OrderItem> items = new ArrayList<>(); private OrderState state = DraftState.INSTANCE; private PaymentInfo paymentInfo; private ShippingInfo shippingInfo; private String cancellationReason; private boolean refundRequired; private Instant deliveredAt;
public OrderContext(String orderId) { this.orderId = orderId; }
// Delegación al estado actual public void addItem(OrderItem item) { state.addItem(this, item); } public void removeItem(String productId) { state.removeItem(this, productId); } public void submit() { state.submitOrder(this); } public void pay(PaymentInfo payment) { state.processPayment(this, payment); } public void ship(ShippingInfo shipping) { state.shipOrder(this, shipping); } public void deliver() { state.deliverOrder(this); } public void cancel(String reason) { state.cancelOrder(this, reason); }
public Money getTotal() { return items.stream() .map(i -> i.price().multiply(i.quantity())) .reduce(Money.ZERO, Money::add); }
public void printStatus() { IO.println("\n=== Order " + orderId + " ==="); IO.println("State: " + state.stateName()); IO.println("Items: " + items); IO.println("Total: " + getTotal()); }}
// Uso del patrón Statefinal var order = new OrderContext("ORD-001");
order.addItem(new OrderItem("LAPTOP", 1, new Money(999.99, "USD")));order.addItem(new OrderItem("MOUSE", 2, new Money(29.99, "USD")));order.printStatus(); // State: DRAFT
order.submit(); // ✓ Order submitted, awaiting paymentorder.printStatus(); // State: PENDING_PAYMENT
order.pay(new PaymentInfo("4111111111111111", new Money(1059.97, "USD")));// ✓ Payment processed
order.ship(new ShippingInfo("TRACK-123", "FedEx"));// ✓ Order shipped
order.deliver();// ✓ Order delivered!
order.printStatus(); // State: DELIVERED
// Intentar operación inválidatry { order.cancel("Changed my mind");} catch (IllegalStateException e) { IO.println("Error: " + e.getMessage()); // Error: Cannot cancel delivered in state: DELIVERED}Strategy
El patrón Strategy define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Permite que el algoritmo varíe independientemente de los clientes que lo usan. Este patrón es ideal cuando se tienen múltiples formas de realizar una tarea y la elección del algoritmo debe hacerse en tiempo de ejecución.
A diferencia del patrón State donde el comportamiento cambia según el estado interno, en Strategy la elección del algoritmo depende de condiciones externas como configuración, preferencias del usuario o tipo de datos. En Java moderno, las estrategias pueden implementarse elegantemente usando interfaces funcionales y lambdas.
Ejemplo: Sistema de procesamiento de pagos con estrategias
// Interfaz Strategy@FunctionalInterfacepublic interface CalculatePrice extends Function<Order, Money> {
default String strategyName() { return this.getClass().getSimpleName(); }}
// Estrategias concretaspublic class StandardPricing implements PricingStrategy { @Override public Money apply(Order order) { return order.subtotal(); }
@Override public String strategyName() { return "Standard Pricing"; }}
public class MembershipPricing implements PricingStrategy { private final MembershipLevel level;
@Getter @RequiredArgsConstructor public enum MembershipLevel { SILVER(0.05), GOLD(0.10), PLATINUM(0.15);
private final double discount; }
public MembershipPricing(MembershipLevel level) { this.level = level; }
@Override public Money apply(Order order) { return order.subtotal().multiply(1 - level.getDiscount()); }
@Override public String strategyName() { return level + " Membership (" + (int)(level.getDiscount() * 100) + "% off)"; }}
// Estrategia compuesta - aplica múltiples estrategiaspublic class CompositePricingStrategy implements PricingStrategy { private final List<PricingStrategy> strategies; private final CombinationMode mode;
public enum CombinationMode { BEST_PRICE, STACK_ALL }
public CompositePricingStrategy(CombinationMode mode, PricingStrategy... strategies) { this.mode = mode; this.strategies = List.of(strategies); }
@Override public Money apply(Order order) { return switch (mode) { case BEST_PRICE -> strategies.stream() .map(PricingStrategy) .min(Comparator.naturalOrder()) .orElse(order.subtotal()); case STACK_ALL -> { Money current = order.subtotal(); for (var strategy : strategies) { Order tempOrder = order.withSubtotal(current); current = strategy.apply(tempOrder); } yield current; } }; }
@Override public String strategyName() { return "Composite: " + mode; }}
// Uso con lambdas - estrategias como funciones@RequiredArgsConstructorpublic class FunctionalPricingExample {
public Money applyDiscount(Order order, PricingStrategy strategy) { return strategy.apply(order); }
public static void main(String[] args) { var order = new Order("ORD-001", List.of( new OrderItem("PROD-A", 5, new Money(20, "USD")), new OrderItem("PROD-B", 3, new Money(15, "USD")) ));
// Estrategias como lambdas PricingStrategy standard = new StandardPricing(); PricingStrategy members = new MembershipPricing(GOLD);
IO.println("Standard discount: " + applyDiscount(order, standard)); IO.println("10% off for members: " + applyDiscount(order, members)); IO.println("Compound: " + applyDiscount(order, new CompositePricingStrategy(BEST_PRICE, standard, members))); }}Template Method
El patrón Template Method define el esqueleto de un algoritmo en una operación, difiriendo algunos pasos a las subclases. Permite que las subclases redefinan ciertos pasos del algoritmo sin cambiar su estructura general. La filosofía es “No nos llames, nosotros te llamamos” (Hollywood Principle).
La clase base define la secuencia de pasos y proporciona implementaciones por defecto o abstractas para cada paso. Las subclases pueden sobrescribir pasos específicos mientras la estructura del algoritmo permanece intacta. También pueden incluirse “hooks” - métodos vacíos que las subclases pueden sobrescribir opcionalmente.
Ejemplo: Pipeline de procesamiento de datos
// Template abstractopublic abstract class DataProcessor<T, R> {
// Template method - define la estructura del algoritmo public final ProcessingResult<R> process(DataSource<T> source) { final var startTime = Instant.now(); final var metrics = new ProcessingMetrics();
try { // Hook - preparación opcional beforeProcessing(source);
// Paso 1: Extraer datos (abstracto) final List<T> rawData = extractData(source); metrics.setRecordsRead(rawData.size());
// Paso 2: Validar datos (con implementación por defecto) final List<T> validData = validateData(rawData, metrics);
// Paso 3: Transformar datos (abstracto) final List<R> transformedData = transformData(validData); metrics.setRecordsTransformed(transformedData.size());
// Paso 4: Filtrar datos (con implementación por defecto) final List<R> filteredData = filterData(transformedData); metrics.setRecordsFiltered(transformedData.size() - filteredData.size());
// Paso 5: Cargar/guardar datos (abstracto) final int loaded = loadData(filteredData); metrics.setRecordsLoaded(loaded);
// Hook - finalización opcional afterProcessing(metrics);
metrics.setDuration(Duration.between(startTime, Instant.now())); return ProcessingResult.success(filteredData, metrics);
} catch (Exception e) { handleError(e, metrics); metrics.setDuration(Duration.between(startTime, Instant.now())); return ProcessingResult.failure(e.getMessage(), metrics); } }
// Métodos abstractos - las subclases DEBEN implementar protected abstract List<T> extractData(DataSource<T> source); protected abstract List<R> transformData(List<T> data); protected abstract int loadData(List<R> data);
// Métodos con implementación por defecto - las subclases PUEDEN sobrescribir protected List<T> validateData(List<T> data, ProcessingMetrics metrics) { // Por defecto, todos los datos son válidos return data; }
protected List<R> filterData(List<R> data) { // Por defecto, no se filtra nada return data; }
// Hooks - métodos vacíos que las subclases pueden sobrescribir protected void beforeProcessing(DataSource<T> source) { // Hook vacío por defecto }
protected void afterProcessing(ProcessingMetrics metrics) { // Hook vacío por defecto }
protected void handleError(Exception e, ProcessingMetrics metrics) { System.err.println("Error processing: " + e.getMessage()); metrics.setError(e.getMessage()); }}
// Implementación concreta: procesador de API a Base de datos@RequiredArgsConstructorpublic class ApiToDatabaseProcessor extends DataProcessor<ApiResponse, DatabaseRecord> {
private final DatabaseConnection db; private final String tableName;
@Override protected List<ApiResponse> extractData(DataSource<ApiResponse> source) { return source.readAll(); }
@Override protected List<DatabaseRecord> transformData(List<ApiResponse> data) { return data.stream() .map(response -> new DatabaseRecord( response.id(), response.data(), Instant.now() )) .toList(); }
@Override protected int loadData(List<DatabaseRecord> data) { return db.batchInsert(tableName, data); }
@Override protected void handleError(Exception e, ProcessingMetrics metrics) { // Logging más detallado para errores de BD System.err.println("Database error: " + e.getMessage()); db.rollback(); super.handleError(e, metrics); }}
// Uso del Template Methodfinal var apiToDbProcessor = new ApiToDatabaseProcessor(posgresqlConn, "processingTable");
final var result = apiToDbProcessor.process(apiResponse);
if (result.success()) { IO.println("Created %d records".formatted(result.data().size()));} else { System.err.println("Processing failed: " + result.errorMessage());}Visitor
El patrón Visitor permite definir nuevas operaciones sobre una estructura de objetos sin modificar las clases de los elementos sobre los que opera. Separa un algoritmo de la estructura de objetos sobre la que opera, facilitando agregar nuevas operaciones sin cambiar las clases existentes.
Este patrón es ideal cuando la estructura de objetos es estable pero las operaciones sobre ella cambian frecuentemente. Define una doble despacho: el elemento acepta un visitor y el visitor determina qué operación ejecutar basándose en el tipo concreto del elemento.
Ejemplo: Sistema de reportes para documentos
// Interfaz Visitorpublic interface DocumentVisitor<T> { T visitParagraph(Paragraph paragraph); T visitHeading(Heading heading); T visitCodeBlock(CodeBlock codeBlock);
// Método por defecto para elementos compuestos default T visitDocument(Document document) { document.elements().forEach(e -> e.accept(this)); return null; }}
// Interfaz Elementpublic interface DocumentElement { <T> T accept(DocumentVisitor<T> visitor);}
// Elementos concretospublic record Paragraph(String text, TextStyle style) implements DocumentElement { public enum TextStyle { NORMAL, BOLD, ITALIC, QUOTE }
@Override public <T> T accept(DocumentVisitor<T> visitor) { return visitor.visitParagraph(this); }}
public record Heading(String text, int level) implements DocumentElement { public Heading { if (level < 1 || level > 6) throw new IllegalArgumentException("Level must be 1-6"); }
@Override public <T> T accept(DocumentVisitor<T> visitor) { return visitor.visitHeading(this); }}
public record CodeBlock(String code, String language) implements DocumentElement { @Override public <T> T accept(DocumentVisitor<T> visitor) { return visitor.visitCodeBlock(this); }}
public record Document(String title, List<DocumentElement> elements) implements DocumentElement { @Override public <T> T accept(DocumentVisitor<T> visitor) { return visitor.visitDocument(this); }}
// Visitor concreto: Exportar a Markdownpublic class MarkdownExportVisitor implements DocumentVisitor<String> {
private final StringBuilder markdown = new StringBuilder();
@Override public String visitParagraph(Paragraph paragraph) { String result = switch (paragraph.style()) { case NORMAL -> paragraph.text(); case BOLD -> "**" + paragraph.text() + "**"; case ITALIC -> "*" + paragraph.text() + "*"; case QUOTE -> "> " + paragraph.text(); }; markdown.append(result).append("\n\n"); return result; }
@Override public String visitHeading(Heading heading) { String result = "#".repeat(heading.level()) + " " + heading.text(); markdown.append(result).append("\n\n"); return result; }
@Override public String visitCodeBlock(CodeBlock codeBlock) { String result = "```%s\n%s\n```".formatted(codeBlock.language(), codeBlock.code()); markdown.append(result).append("\n\n"); return result; }
@Override public String visitDocument(Document document) { markdown.append("# ").append(document.title()).append("\n\n"); document.elements().forEach(e -> e.accept(this)); return markdown.toString(); }
public String getMarkdown() { return markdown.toString(); }}
// Uso del patrón Visitorfinal var document = new Document("Mi Documento", List.of( new Heading("Introducción", 1), new Paragraph("Este es un documento de ejemplo.", Paragraph.TextStyle.NORMAL), new Heading("Datos", 2), new Heading("Código", 2), new CodeBlock("IO.println(\"Hello!\");", "java")));
// Exportar a Markdownfinal var markdownVisitor = new MarkdownExportVisitor();document.accept(markdownVisitor);IO.println(markdownVisitor.getMarkdown());