I design pattern in Python

Oggi parleremo di design pattern in Python. I design pattern sono soluzioni tipiche a problemi comuni nella progettazione del software. Sono come dei modelli collaudati che puoi riutilizzare per risolvere problemi ricorrenti in modo efficiente tramite delle soluzioni standardizzate.

Il concetto di design pattern venne introdotto nel 1995  dala Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) con la pubblicazione del libro "Design Patterns: Elements of Reusable Object-Oriented Software" dove un "pattern" descrive un problema ricorrente e una soluzione applicabile in molti contesti.

Ogni pattern è generalmente composto da:

  • un nome
  • una descrizione del problema
  • una soluzione
  • un'analisi degli effetti che produce.

Oltre ai pattern ci sono anche degli anti-pattern che, invece, identificano le soluzioni ai problemi da evitare perché producono conseguenze negative.

Ad esempio, la pratica diffusa di riutilizzare parti di codice tramite il copia e incolla è un tipico anti-pattern. Questa pratica va evitata perché rende il codice più difficile da mantenere e aumenta il rischio di errori nel tempo.

Ecco qualche esempio pratico di design pattern.

Singleton

Il pattern Singleton assicura che una classe abbia una sola istanza e fornisce un punto di accesso globale a quell'istanza.

È utile quando si ha bisogno di una sola istanza di una classe, come per un logger o una connessione a un database.

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
        return cls._instance

In questo esmepio, la classe `Singleton` ha un attributo di classe `_instance` che viene inizialmente impostato a `None`.

Il metodo speciale `__new__`, controlla se `_instance` è `None` e, in tal caso, crea una nuova istanza della classe; altrimenti, restituisce l'istanza già creata.

Questo assicura che, indipendentemente da quante volte si istanzi la classe, verrà sempre restituita la stessa istanza.

Ad esempio, crea due istanze `s1` e `s2` della classe

s1 = Singleton()
s2 = Singleton()

Entrambe le variabili puntano alla stessa istanza della classe `Singleton`.

print(s1 is s2) 

True

Factory

Il pattern Factory è usato per creare oggetti senza specificare l'esatta classe dell'oggetto che verrà creato. È utile quando il processo di creazione è complesso o quando la classe esatta degli oggetti non è nota fino a runtime.

class Car:
    def drive(self):
        return "Driving a car!"

class Bike:
    def drive(self):
        return "Riding a bike!"

class VehicleFactory:
    def create_vehicle(self, vehicle_type):
        if vehicle_type == "car":
            return Car()
        elif vehicle_type == "bike":
            return Bike()
        else:
            raise ValueError("Unknown vehicle type")

Questo codice definisce due classi, `Car` e `Bike`, entrambe con un metodo `drive` che ritorna una stringa descrittiva dell'azione.

La classe `VehicleFactory` dispone di un metodo `create_vehicle` che accetta un parametro `vehicle_type` e ritorna un'istanza della classe corrispondente (`Car` o `Bike`), oppure lancia un'eccezione se il tipo di veicolo è sconosciuto.

In questo modo puoi utilizzare il `VehicleFactory` per creare e usare oggetti `Car` e `Bike` in base al tipo richiesto.

factory = VehicleFactory()

Ad esempio, puoi creare un oggetto `Car`

car = factory.create_vehicle("car")
print(car.drive()) 

Driving a car!

Con lo stesso metodo puoi anche creare un oggetto `Bike`

bike = factory.create_vehicle("bike")
print(bike.drive()) 

Riding a bike!

In breve, il codice dimostra come utilizzare un Factory Pattern per istanziare oggetti di diverse classi in modo flessibile e centralizzato.

Observer

Il pattern Observer definisce una dipendenza uno-a-molti tra oggetti, in modo tale che quando un oggetto cambia stato, tutti i suoi dipendenti vengono notificati e aggiornati automaticamente. È utile per implementare sistemi di notifiche.

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, message):
        for observer in self._observers:
            observer.update(message)

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

class ConcreteObserver(Observer):
    def update(self, message):
        print(f"Received message: {message}")

Ad esempio, crea un oggetto Subject, che sarà l'oggetto da osservare. Questo oggetto mantiene una lista di osservatori che saranno notificati quando si verifica un cambiamento.

subject = Subject()

Crea due istanze di ConcreteObserver, observer1 e observer2. Questi sono gli osservatori che saranno notificati dei cambiamenti nel Subject.

observer1 = ConcreteObserver()
observer2 = ConcreteObserver()

Aggiungi observer1 e observer2 alla lista di osservatori del Subject.

Ora, subject sa che deve notificare questi osservatori quando c'è un cambiamento.< /p>

subject.attach(observer1)
subject.attach(observer2)

Chiama il metodo notify del Subject, che itera attraverso tutti gli osservatori registrati (in questo caso observer1 e observer2) e chiama il loro metodo update passando il messaggio "Hello, observers!".

subject.notify("Hello, observers!")

Ciascun osservatore (observer1 e observer2) riceve il messaggio e stampa "Received message: Hello, observers!".

Quindi, l'output di questo codice sarà:

Received message: Hello, observers!
Received message: Hello, observers!

Questo esempio mostra come un Subject può notificare più osservatori di un cambiamento, e gli osservatori reagiscono al cambiamento ricevendo e stampando il messaggio.

Decorator

Il pattern Decorator permette di aggiungere comportamento a un oggetto in modo dinamico. È utile per estendere le funzionalità delle classi senza doverle modificare direttamente.

class Coffee:
    def cost(self):
        return 5

class MilkDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost() + 1

class SugarDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def cost(self):
        return self._coffee.cost() + 0.5

Questo codice definisce tre classi:

  • La classe Coffee rappresenta un semplice caffè con un costo fisso di 5.
  • La classe MilkDecorator è un decoratore che aggiunge latte al caffè. Prende un oggetto Coffee (o un decoratore di Coffee) e aggiunge 1 al suo costo.
  • La classe SugarDecorator è un altro decoratore che aggiunge zucchero al caffè. Prende un oggetto Coffee (o un decoratore di Coffee) e aggiunge 0.5 al suo costo.

Ad esempio crea un oggetto della classe "Coffee"

coffee = Coffee()

Il costo base del caffè 5

print(coffee.cost()) 

5

Ora aggiungi il latte al caffè tramite la classe MilkDecorator.

milk_coffee = MilkDecorator(coffee)

L'aggiunta del latte aumenta il costo del caffè di +1

print(milk_coffee.cost()) 

6

Adesso aggiungi anche lo zucchero al caffè con latte usando SugarDecorator

milk_sugar_coffee = SugarDecorator(milk_coffee)

Il costo del caffè aumenta ulteriormente di 0.5

print(milk_sugar_coffee.cost())

6.5

In sintesi, il pattern Decorator ti permette di aggiungere funzionalità (come latte o zucchero) a un oggetto Coffee in modo flessibile e incrementale, senza modificare direttamente la classe Coffee.

Strategy

Il pattern Strategy permette di definire una famiglia di algoritmi, di incapsularli e di renderli intercambiabili. È utile per selezionare l'algoritmo appropriato a runtime.

class Strategy:
    def execute(self, a, b):
        pass

class AddStrategy(Strategy):
    def execute(self, a, b):
        return a + b

class SubtractStrategy(Strategy):
    def execute(self, a, b):
        return a - b

class Context:
    def __init__(self, strategy):
        self._strategy = strategy

    def set_strategy(self, strategy):
        self._strategy = strategy

    def execute_strategy(self, a, b):
        return self._strategy.execute(a, b)

Questo esempio implementa il pattern Strategy, che consente di definire una famiglia di algoritmi, incapsularli e renderli intercambiabili. Vediamo il codice in dettaglio:

Ad esempio, crea un oggetto "Context" con AddStrategy come strategia iniziale.

context = Context(AddStrategy())

Se ora chiami il metodo context.execute_strategy(5, 3), questo esegue il metodo execute di AddStrategy restituendo la somma di 5 e 3.

print(context.execute_strategy(5, 3)) 

8

Adesso, cambia la strategia a SubtractStrategy tramite set_strategy.

context.set_strategy(SubtractStrategy())

Se ora chiami il metodo context.execute_strategy(5, 3), questo esegue il metodo execute di SubtractStrategy, restituendo la differenza tra 5 e 3

print(context.execute_strategy(5, 3)) 

2

In sintesi, il pattern Strategy permette di cambiare l'algoritmo utilizzato da un oggetto Context in modo dinamico, senza modificare il contesto stesso. Questo rende il codice più flessibile e manutenibile.

Questi sono solo alcuni dei design pattern più comuni e utili in Python. Ogni pattern ha i suoi casi d'uso ideali e può aiutare a scrivere codice più manutenibile, riutilizzabile e robusto.

Spero che questa guida ti sia stata utile e che ti aiuti a capire meglio come e quando utilizzare i design pattern!

 




Se qualcosa non ti è chiaro, scrivi la tua domanda nei commenti.




FacebookTwitterLinkedinLinkedin