En esta sección, exploraremos ejemplos prácticos de cómo aplicar algunos de los patrones de diseño más comunes en situaciones reales de desarrollo de software. Cada ejemplo incluirá una explicación detallada, un bloque de código y ejercicios prácticos para reforzar los conceptos aprendidos.

Ejemplo 1: Singleton

Descripción

El patrón Singleton asegura que una clase tenga solo una instancia y proporciona un punto de acceso global a ella. Es útil en situaciones donde se necesita un control centralizado, como en el caso de un registro de configuración o una conexión a una base de datos.

Código de Ejemplo

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

# Uso del Singleton
singleton1 = Singleton()
singleton2 = Singleton()

print(singleton1 is singleton2)  # True

Explicación

  1. Clase Singleton: La clase Singleton tiene un atributo de clase _instance que almacena la única instancia de la clase.
  2. Método __new__: El método __new__ se sobrescribe para controlar la creación de nuevas instancias. Si _instance es None, se crea una nueva instancia; de lo contrario, se devuelve la instancia existente.
  3. Uso: Al crear dos objetos singleton1 y singleton2, ambos apuntan a la misma instancia, lo que se verifica con singleton1 is singleton2.

Ejercicio Práctico

Implementa un Singleton para una clase Logger que solo permite una instancia para registrar mensajes.

class Logger:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Logger, cls).__new__(cls)
        return cls._instance

    def log(self, message):
        print(f"Log: {message}")

# Uso del Logger
logger1 = Logger()
logger2 = Logger()

logger1.log("Este es un mensaje de prueba.")
logger2.log("Este es otro mensaje de prueba.")

print(logger1 is logger2)  # True

Solución

class Logger:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Logger, cls).__new__(cls)
        return cls._instance

    def log(self, message):
        print(f"Log: {message}")

# Uso del Logger
logger1 = Logger()
logger2 = Logger()

logger1.log("Este es un mensaje de prueba.")
logger2.log("Este es otro mensaje de prueba.")

print(logger1 is logger2)  # True

Ejemplo 2: Factory Method

Descripción

El patrón Factory Method define una interfaz para crear un objeto, pero permite que las subclases alteren el tipo de objetos que se crearán. Es útil cuando una clase no puede anticipar el tipo de objetos que debe crear.

Código de Ejemplo

from abc import ABC, abstractmethod

class Product(ABC):
    @abstractmethod
    def operation(self):
        pass

class ConcreteProductA(Product):
    def operation(self):
        return "Resultado de ConcreteProductA"

class ConcreteProductB(Product):
    def operation(self):
        return "Resultado de ConcreteProductB"

class Creator(ABC):
    @abstractmethod
    def factory_method(self):
        pass

    def some_operation(self):
        product = self.factory_method()
        return f"Creator: El mismo código del creador ha trabajado con {product.operation()}"

class ConcreteCreatorA(Creator):
    def factory_method(self):
        return ConcreteProductA()

class ConcreteCreatorB(Creator):
    def factory_method(self):
        return ConcreteProductB()

# Uso del Factory Method
creator_a = ConcreteCreatorA()
print(creator_a.some_operation())

creator_b = ConcreteCreatorB()
print(creator_b.some_operation())

Explicación

  1. Clase Abstracta Product: Define la interfaz operation que deben implementar los productos concretos.
  2. Clases Concretas ConcreteProductA y ConcreteProductB: Implementan la interfaz operation.
  3. Clase Abstracta Creator: Define el método factory_method que debe ser implementado por las subclases y un método some_operation que utiliza el producto creado.
  4. Clases Concretas ConcreteCreatorA y ConcreteCreatorB: Implementan el método factory_method para crear instancias de ConcreteProductA y ConcreteProductB, respectivamente.

Ejercicio Práctico

Implementa un Factory Method para una clase Transport con subclases Car y Bike.

from abc import ABC, abstractmethod

class Transport(ABC):
    @abstractmethod
    def deliver(self):
        pass

class Car(Transport):
    def deliver(self):
        return "Entrega por Carro"

class Bike(Transport):
    def deliver(self):
        return "Entrega por Bicicleta"

class TransportCreator(ABC):
    @abstractmethod
    def factory_method(self):
        pass

    def plan_delivery(self):
        transport = self.factory_method()
        return f"Plan de entrega: {transport.deliver()}"

class CarCreator(TransportCreator):
    def factory_method(self):
        return Car()

class BikeCreator(TransportCreator):
    def factory_method(self):
        return Bike()

# Uso del Factory Method
car_creator = CarCreator()
print(car_creator.plan_delivery())

bike_creator = BikeCreator()
print(bike_creator.plan_delivery())

Solución

from abc import ABC, abstractmethod

class Transport(ABC):
    @abstractmethod
    def deliver(self):
        pass

class Car(Transport):
    def deliver(self):
        return "Entrega por Carro"

class Bike(Transport):
    def deliver(self):
        return "Entrega por Bicicleta"

class TransportCreator(ABC):
    @abstractmethod
    def factory_method(self):
        pass

    def plan_delivery(self):
        transport = self.factory_method()
        return f"Plan de entrega: {transport.deliver()}"

class CarCreator(TransportCreator):
    def factory_method(self):
        return Car()

class BikeCreator(TransportCreator):
    def factory_method(self):
        return Bike()

# Uso del Factory Method
car_creator = CarCreator()
print(car_creator.plan_delivery())

bike_creator = BikeCreator()
print(bike_creator.plan_delivery())

Ejemplo 3: Observer

Descripción

El patrón Observer define una dependencia de uno a muchos entre objetos, de manera que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente. Es útil en situaciones donde un cambio en un objeto debe reflejarse en otros objetos.

Código de Ejemplo

class Subject:
    def __init__(self):
        self._observers = []

    def attach(self, observer):
        self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self)

class Observer:
    def update(self, subject):
        pass

class ConcreteObserverA(Observer):
    def update(self, subject):
        print("ConcreteObserverA: Reacted to the event")

class ConcreteObserverB(Observer):
    def update(self, subject):
        print("ConcreteObserverB: Reacted to the event")

# Uso del Observer
subject = Subject()

observer_a = ConcreteObserverA()
observer_b = ConcreteObserverB()

subject.attach(observer_a)
subject.attach(observer_b)

subject.notify()

Explicación

  1. Clase Subject: Mantiene una lista de observadores y proporciona métodos para agregar (attach), eliminar (detach) y notificar (notify) a los observadores.
  2. Clase Observer: Define la interfaz update que deben implementar los observadores concretos.
  3. Clases Concretas ConcreteObserverA y ConcreteObserverB: Implementan la interfaz update para reaccionar a las notificaciones del sujeto.

Ejercicio Práctico

Implementa un Observer para una clase Stock que notifica a los observadores cuando el precio cambia.

class Stock:
    def __init__(self):
        self._observers = []
        self._price = 0

    def attach(self, observer):
        self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self)

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        self._price = value
        self.notify()

class StockObserver:
    def update(self, stock):
        pass

class ConcreteStockObserver(StockObserver):
    def update(self, stock):
        print(f"El precio del stock ha cambiado a {stock.price}")

# Uso del Observer
stock = Stock()

observer = ConcreteStockObserver()
stock.attach(observer)

stock.price = 100
stock.price = 200

Solución

class Stock:
    def __init__(self):
        self._observers = []
        self._price = 0

    def attach(self, observer):
        self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self)

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        self._price = value
        self.notify()

class StockObserver:
    def update(self, stock):
        pass

class ConcreteStockObserver(StockObserver):
    def update(self, stock):
        print(f"El precio del stock ha cambiado a {stock.price}")

# Uso del Observer
stock = Stock()

observer = ConcreteStockObserver()
stock.attach(observer)

stock.price = 100
stock.price = 200

Conclusión

En esta sección, hemos explorado ejemplos prácticos de algunos patrones de diseño comunes: Singleton, Factory Method y Observer. Cada ejemplo incluyó una explicación detallada, un bloque de código y ejercicios prácticos para reforzar los conceptos aprendidos. Estos patrones son herramientas poderosas que pueden ayudar a resolver problemas comunes en el desarrollo de software de manera eficiente y reutilizable.

© Copyright 2024. Todos los derechos reservados