3.0 / 5
En el mundo de la ingenierÃa de software, convertir ideas en código funcional puede ser un desafÃo.
Como desarrolladores, nuestro objetivo no es solo hacer que las cosas funcionen, sino también asegurarnos de que nuestro código sea mantenible, escalable, adaptable y reutilizable.
Aquà entran en juego los patrones de diseño: los planos probados que nos permiten abordar problemas de diseño recurrentes con elegancia y eficiencia.
En esencia, un patrón de diseño es como una solución prefabricada para problemas comunes que enfrentamos al diseñar software. Estas soluciones actúan como atajos, ahorrándonos tiempo y esfuerzo mediante el uso de estrategias probadas que los expertos han perfeccionado a lo largo de los años.
En este artÃculo, profundizaremos en algunos de los patrones de diseño más importantes que todo desarrollador deberÃa conocer. Exploraremos sus principios, por qué son útiles y cómo puedes usarlos en proyectos reales. Ya sea que estés lidiando con la creación de objetos, organizando relaciones entre clases o gestionando cómo se comportan los objetos, hay un patrón de diseño que puede ayudarte.
¡Vamos a empezar!
El patrón Singleton es un patrón de diseño creacional que asegura que una clase solo tenga una instancia y proporciona un punto de acceso global a esa instancia. En términos más simples, es como asegurar que solo haya una copia única de un objeto particular en tu programa, y puedes acceder a ese objeto desde cualquier parte de tu código.
Tomemos un ejemplo del mundo real: el portapapeles. Imagina múltiples aplicaciones o procesos ejecutándose en una computadora, cada uno intentando acceder al portapapeles simultáneamente. Si cada aplicación creara su propia versión del portapapeles para gestionar las operaciones de copiar y pegar, podrÃa conducir a datos conflictivos.
public class Clipboard {
private String value;
public void copy(String value) {
this.value = value;
}
public String paste() {
return value;
}
}
Code language: JavaScript (javascript)
En el ejemplo anterior, hemos definido una clase Clipboard capaz de copiar y pegar valores. Sin embargo, si creáramos múltiples instancias de Clipboard, cada instancia tendrÃa sus propios datos separados.
public class Main {
public static void main(String[] args) {
Clipboard clipboard1 = new Clipboard();
Clipboard clipboard2 = new Clipboard();
clipboard1.copy("Java");
clipboard2.copy("Patrones de diseño");
System.out.println(clipboard1.paste()); // salida: Java
System.out.println(clipboard2.paste()); // salida: Patrones de diseño
}
}
Code language: JavaScript (javascript)
Claramente, esto no es ideal. Esperamos que ambas instancias del portapapeles muestren el mismo valor. Aquà es donde el patrón Singleton demuestra su valÃa.
public class Clipboard {
private String value;
private static Clipboard clipboard = null;
// Constructor privado para evitar la instanciación desde fuera
private Clipboard() {}
// Método para proporcionar acceso a la instancia singleton
public static Clipboard getInstance() {
if (clipboard == null) {
clipboard = new Clipboard();
}
return clipboard;
}
public void copy(String value) {
this.value = value;
}
public String paste() {
return value;
}
}
Code language: PHP (php)
Al implementar el patrón Singleton, aseguramos que solo exista una instancia de la clase Clipboard durante la ejecución del programa.
public class Main {
public static void main(String[] args) {
Clipboard clipboard1 = Clipboard.getInstance();
Clipboard clipboard2 = Clipboard.getInstance();
clipboard1.copy("Java");
clipboard2.copy("Patrones de diseño");
System.out.println(clipboard1.paste()); // salida: Patrones de diseño
System.out.println(clipboard2.paste()); // salida: Patrones de diseño
}
}
Code language: JavaScript (javascript)
Ahora, tanto clipboard1 como clipboard2 hacen referencia a la misma instancia de la clase Clipboard, asegurando la consistencia en toda la aplicación.
El Patrón de Diseño Factory es un patrón de diseño creacional que proporciona una interfaz para crear objetos en una clase superclase, pero permite a las subclases decidir qué clase instanciar. En otras palabras, proporciona una forma de delegar la lógica de instanciación a las clases hijas.
Imagina que estás construyendo un programa que simula una calculadora simple basada en consola. Tienes diferentes tipos de operaciones como suma, resta, multiplicación, división, etc. Cada operación tiene su propio comportamiento único. Ahora, deseas crear estos objetos de operación en tu programa basándote en la elección del usuario.
El desafÃo es que necesitas una forma de crear estos objetos de operación sin hacer que tu código sea demasiado complejo o esté fuertemente acoplado. Esto significa que no quieres que tu código dependa demasiado de las clases especÃficas de las operaciones directamente. También deseas facilitar la adición de nuevos tipos de operaciones más adelante sin cambiar mucho el código.
El Patrón de Diseño Factory te ayuda a resolver este problema proporcionando una forma de crear objetos sin especificar su clase exacta. En su lugar, delegas el proceso de creación a una clase factory.
public interface Operation {
double calculate(double number1, double number2);
}
Code language: PHP (php)
// Para la suma
public class AddOperation implements Operation {
@Override
public double calculate(double number1, double number2) {
return number1 + number2;
}
}
// Para la resta
public class SubOperation implements Operation {
@Override
public double calculate(double number1, double number2) {
return number1 - number2;
}
}
// Para la multiplicación
public class MulOperation implements Operation {
@Override
public double calculate(double number1, double number2) {
return number1 * number2;
}
}
// Para la división
public class DivOperation implements Operation {
@Override
public double calculate(double number1, double number2) {
if (number2 == 0) throw new ArithmeticException("¡No se puede dividir por cero!");
return number1 / number2;
}
}
// Clase de excepción que se invoca cuando el usuario ingresa una elección inválida para la operación
public class InvalidOperationException extends Exception {
public InvalidOperationException(String message) {
super(message);
}
}
Code language: PHP (php)
public interface OperationFactory {
Operation getInstance(int choice) throws InvalidOperationException;
}
public class OperationFactoryImpl implements OperationFactory {
@Override
public Operation getInstance(int choice) throws InvalidOperationException {
if (choice == 1) return new AddOperation();
else if (choice == 2) return new SubOperation();
else if (choice == 3) return new MulOperation();
else if (choice == 4) return new DivOperation();
throw new InvalidOperationException("¡Operación seleccionada inválida!");
}
}
Code language: PHP (php)
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
try {
System.out.println("\n1. Suma(+)\n2. Resta(-)\n3. Multiplicación(*)\n4. División(/)");
// Obteniendo la elección del usuario
System.out.println("\n\nSelecciona tu operación (1-4): ");
int choice = scan.nextInt();
// Obteniendo 2 operandos del usuario
System.out.println("Ingresa el primer operando: ");
double operand1 = scan.nextDouble();
System.out.println("Ingresa el segundo operando: ");
double operand2 = scan.nextDouble();
// Crear instancia de operación basada en la elección del usuario
OperationFactory operationFactory = new OperationFactoryImpl();
Operation operation = operationFactory.getInstance(choice);
// Imprimiendo el resultado
System.out.println("\nEl resultado es " + operation.calculate(operand1, operand2) + ".");
} catch (InputMismatchException e) {
System.out.println("¡Tipo de entrada inválido!\n");
} catch (InvalidOperationException | ArithmeticException e) {
System.out.println(e.getMessage());
}
scan.close();
}
Code language: JavaScript (javascript)
AquÃ, la clase Main demuestra el uso de la fábrica para crear diferentes objetos de operación sin conocer sus clases de implementación especÃficas (acoplamiento suelto). Solo interactúa con la interfaz de la fábrica. No solo eso, sino que también podemos agregar fácilmente nuevos tipos de operaciones sin cambiar el código del cliente existente. Solo necesitamos crear un nuevo producto concreto y actualizar la fábrica si es necesario.
El Patrón Builder proporciona una forma de construir un objeto permitiéndote establecer sus diversas propiedades (o atributos) de manera paso a paso.
Algunos de los parámetros pueden ser opcionales para un objeto, pero estamos obligados a enviar todos los parámetros y los parámetros opcionales deben enviarse como NULL. Podemos resolver este problema con un gran número de parámetros proporcionando un constructor con parámetros requeridos y luego diferentes métodos setter para establecer los parámetros opcionales.
Este patrón es particularmente útil cuando se trata de objetos que tienen muchos parámetros opcionales o configuraciones.
Imagina que estamos desarrollando una entidad de usuario. Los usuarios
tienen atributos como id, nombre, email, edad y dirección, algunos de los cuales pueden ser opcionales. Usar constructores para establecer todos estos atributos puede volverse confuso y difÃcil de manejar a medida que crece el número de parámetros. Aquà es donde el Patrón Builder entra en juego, permitiéndonos construir el objeto de una manera más legible y manejable.
public class User {
private int id;
private String name;
private String email;
private int age;
private String address;
// Constructor privado para la clase User
private User(UserBuilder builder) {
this.id = builder.id;
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.address = builder.address;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + ", email=" + email + ", age=" + age + ", address=" + address + "]";
}
// Clase estática interna UserBuilder
public static class UserBuilder {
private int id;
private String name;
private String email;
private int age;
private String address;
// Métodos de construcción para UserBuilder
public UserBuilder setId(int id) {
this.id = id;
return this;
}
public UserBuilder setName(String name) {
this.name = name;
return this;
}
public UserBuilder setEmail(String email) {
this.email = email;
return this;
}
public UserBuilder setAge(int age) {
this.age = age;
return this;
}
public UserBuilder setAddress(String address) {
this.address = address;
return this;
}
// Método de construcción final para crear el objeto User
public User build() {
return new User(this);
}
}
}
Code language: JavaScript (javascript)
public static void main(String[] args) {
// Crear un objeto User utilizando el patrón Builder
User user1 = new User.UserBuilder()
.setId(1)
.setName("John Doe")
.setEmail("john.doe@example.com")
.setAge(30)
.setAddress("123 Main St")
.build();
User user2 = new User.UserBuilder()
.setId(2)
.setName("Jane Doe")
.build(); // Solo se configuran id y nombre, los demás atributos son opcionales
// Imprimir los detalles del usuario
System.out.println(user1);
System.out.println(user2);
}
Code language: JavaScript (javascript)
AquÃ, hemos usado el patrón Builder para construir objetos User. Esto nos permite establecer solo los atributos que queremos, y podemos omitir aquellos que no son necesarios. El resultado es un código más limpio y legible.
El patrón Adapter es un patrón de diseño estructural que permite que objetos con interfaces incompatibles trabajen juntos. Actúa como un puente entre dos interfaces incompatibles.
Imagina una situación donde dos clases o componentes realizan tareas similares pero tienen nombres de métodos, tipos de parámetros o estructuras diferentes. El patrón Adapter permite que estas interfaces incompatibles trabajen juntas proporcionando un envoltorio (el adaptador) que traduce la interfaz de una clase en una interfaz que el cliente espera.
// Interfaz Target
interface CellPhone {
void call();
}
// Adaptee (la clase que se va a adaptar)
class FriendCellPhone {
public void ring() {
System.out.println("Ringing");
}
}
// Clase Adapter que implementa la interfaz Target
class CellPhoneAdapter implements CellPhone {
private FriendCellPhone friendCellPhone;
public CellPhoneAdapter(FriendCellPhone friendCellPhone) {
this.friendCellPhone = friendCellPhone;
}
@Override
public void call() {
friendCellPhone.ring();
}
}
// Clase Cliente
public class AdapterMain {
public static void main(String[] args) {
// Usando el adaptador para hacer que Adaptee funcione con la interfaz Target
FriendCellPhone adaptee = new FriendCellPhone();
CellPhone adapter = new CellPhoneAdapter(adaptee);
adapter.call();
}
}
Code language: PHP (php)
En este ejemplo:
El patrón Decorator es un patrón de diseño en programación orientada a objetos que permite añadir comportamiento a objetos individuales, ya sea estáticamente o dinámicamente, sin afectar el comportamiento de otros objetos de la misma clase.
En este patrón, hay una clase base (o interfaz) que define la funcionalidad común y una o más clases decoradoras que añaden comportamiento adicional. Estas clases decoradoras envuelven el objeto original, aumentando su comportamiento de una manera modular y flexible.
// Interfaz Shape
interface Shape {
void draw();
String getName();
}
// Shape concreto: Circle
class Circle implements Shape {
private String name;
public Circle(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void draw() {
System.out.println("Drawing circle, " + getName() + ".");
}
}
// Clase Decorator abstracta
abstract class ShapeDecorator implements Shape {
private Shape decoratedShape;
public ShapeDecorator(Shape decoratedShape) {
this.decoratedShape = decoratedShape;
}
@Override
public void draw() {
decoratedShape.draw();
}
@Override
public String getName() {
return decoratedShape.getName();
}
}
// Decorator concreto: BorderDecorator
class BorderDecorator extends ShapeDecorator {
private String color;
private int widthInPxs;
public BorderDecorator(Shape decoratedShape, String color, int widthInPxs) {
super(decoratedShape);
this.color = color;
this.widthInPxs = widthInPxs;
}
@Override
public void draw() {
super.draw();
System.out.println("Adding " + widthInPxs + "px, " + color + " color border to " + getName() + ".");
}
}
// Decorator concreto: ColorDecorator
class ColorDecorator extends ShapeDecorator {
private String color;
public ColorDecorator(Shape decoratedShape, String color) {
super(decoratedShape);
this.color = color;
}
@Override
public void draw() {
super.draw();
System.out.println("Filling with " + color + " color to " + getName() + ".");
}
}
// Clase Principal
public class DecoratorMain {
public static void main(String[] args) {
// Crear un cÃrculo
Shape circle1 = new Circle("circle1");
// Decorar el cÃrculo con un borde
Shape circle1WithBorder = new BorderDecorator(circle1, "red", 2);
// Decorar el cÃrculo con un color
Shape circle1WithBorderAndColor = new ColorDecorator(circle1WithBorder, "blue");
// Dibujar el cÃrculo decorado
circle1WithBorderAndColor.draw();
}
}
Code language: JavaScript (javascript)
Con la implementación del Patrón Decorator, nuestra aplicación de dibujo gana la notable capacidad de embellecer no solo cÃrculos, sino también una plétora de formas geométricas como rectángulos, triángulos y más. Además, la extensibilidad de este patrón nos permite integrar decoradores adicionales de manera fluida, ofreciendo caracterÃsticas como transparencia, diversos estilos de borde (sólido, punteado) y mucho más. Esta capacidad de mejora dinámica, lograda sin alterar la estructura central de las formas, subraya el poder del patrón para promover la reutilización de código, flexibilidad y escalabilidad.
El Patrón Observer es un patrón de diseño de comportamiento comúnmente utilizado en la programación orientada a objetos para establecer una dependencia de uno a muchos entre objetos. En este patrón, un objeto (llamado sujeto u observable) mantiene una lista de sus dependientes (observadores) y les notifica cualquier cambio de estado, generalmente llamando a uno de sus métodos.
public enum EventType {
NEW_VIDEO,
LIVE_STREAM
}
public class YoutubeEvent {
private EventType eventType;
private String topic;
public YoutubeEvent(EventType eventType, String topic) {
this.eventType = eventType;
this.topic = topic;
}
// getters y setters
@Override
public String toString() {
return eventType.name() + " on " + topic;
}
}
public interface Subject {
void addSubscriber(Observer observer);
void removeSubscriber(Observer observer);
void notifyAllSubscribers(YoutubeEvent event);
}
public interface Observer {
void notifyMe(String youtubeChannelName, YoutubeEvent event);
}
package observer;
import java.util.ArrayList;
import java.util.List;
public class YoutubeChannel implements Subject {
private String name;
private List<Observer> subscribers = new ArrayList<>();
public YoutubeChannel(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void addSubscriber(Observer observer) {
subscribers.add(observer);
}
@Override
public void removeSubscriber(Observer observer) {
subscribers.remove(observer);
}
@Override
public void notifyAllSubscribers(YoutubeEvent event) {
for (Observer observer : subscribers) {
observer.notifyMe(getName(), event);
}
}
}
package observer;
public class YoutubeSubscriber implements Observer {
private String name;
public YoutubeSubscriber(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void notifyMe(String youtubeChannelName, YoutubeEvent event) {
System.out.println("Dear " + getName() + ", Notification from " + youtubeChannelName + ": " + event);
}
}
public class ObserverMain {
public static void main(String[] args) throws InterruptedException {
YoutubeChannel myChannel = new YoutubeChannel("MyChannel");
Observer john = new YoutubeSubscriber("John");
Observer bob = new YoutubeSubscriber("Bob");
Observer tom = new YoutubeSubscriber("Tom");
myChannel.addSubscriber(john);
myChannel.addSubscriber(bob);
myChannel.addSubscriber(tom);
myChannel.notifyAllSubscribers(new YoutubeEvent(EventType.NEW_VIDEO, "Design patterns"));
myChannel.removeSubscriber(tom);
System.out.println();
Thread.sleep(5000);
myChannel.notifyAllSubscribers(new YoutubeEvent(EventType.LIVE_STREAM, "JAVA for beginners"));
}
}
Code language: PHP (php)
Dear John, Notification from MyChannel: NEW_VIDEO on Design patterns
Dear Bob, Notification from MyChannel: NEW_VIDEO on Design patterns
Dear Tom, Notification from MyChannel: NEW_VIDEO on Design patterns
Dear John, Notification from MyChannel: LIVE_STREAM on JAVA for beginners
Dear Bob, Notification from MyChannel: LIVE_STREAM on JAVA for beginners
Code language: JavaScript (javascript)
Usando el patrón de diseño Observer, el canal de YouTube puede notificar fácilmente a todos sus suscriptores cada vez que se sube un nuevo video sin acoplar estrechamente el canal y sus suscriptores. Esto promueve un diseño más flexible y mantenible.
En conclusión, los patrones de diseño son herramientas indispensables para los desarrolladores de Java, ofreciendo soluciones probadas a problemas recurrentes de diseño y promoviendo la reutilización del código, mantenibilidad y escalabilidad. Al comprender e implementar estos patrones de manera efectiva, los desarrolladores pueden crear soluciones de software robustas, flexibles y fácilmente mantenibles. Si bien dominar los patrones de diseño requiere práctica y experiencia, los beneficios que aportan al desarrollo de software son invaluables. Ya sea que estés trabajando en un proyecto pequeño o en una aplicación empresarial a gran escala, aprovechar los patrones de diseño te permite escribir código más limpio y eficiente y, en última instancia, convertirte en un desarrollador de Java más competente.
Espero que hayas encontrado la información útil y hayas obtenido algunas ideas valiosas sobre el tema. Desde entender qué son los patrones de diseño hasta explorar ejemplos del mundo real, hemos cubierto mucho.
¡Sigue aprendiendo, explorando y creando cosas increÃbles con JAVA!
¡Feliz programación!