Table of Contents

I modelli di progettazione rappresentano soluzioni collaudate nel tempo per rispondere alle sfide dello sviluppo del software. In Python engineering, queste soluzioni riutilizzabili aiutano gli sviluppatori a creare codice più manutenbile, flessibile e scalabile. Capire e implementare i modelli di progettazione in modo efficace può trasformare come si avvicina l'architettura del software, consentendo di scrivere codice che non è solo funzionale ma anche elegante, riutilizzabile e più facile da mantenere nel tempo.

I modelli di design servono come un vocabolario che permette agli ingegneri di comunicare concisamente le decisioni strutturali. Quando uno sviluppatore senior suggerisce di utilizzare un modello specifico, stanno trasmettendo un intero approccio architettonico in poche parole. Questo linguaggio condiviso accelera la collaborazione del team e assicura a tutti di capire la struttura sottostante del codebase.

Comprendere i modelli di progettazione in Python Context

Python è un linguaggio di programmazione di alto livello con digitazione dinamica e legame dinamico, che lo rende un linguaggio dinamico potente e di alto livello. Questa flessibilità offre vantaggi unici di Python quando si implementano modelli di design, ma significa anche che alcuni modelli devono essere adattati per adattarsi agli idiomi e alle funzionalità di Python.

Tutto in Python è un oggetto, comprese le funzioni, che sono oggetti di prima classe, che influenzano il modo in cui i modelli di design vengono implementati in Python rispetto a linguaggi più rigidi e staticamente di tipo. La dinamica di Python permette di implementare più concise implementazioni di molti modelli, introducendo anche la necessità di pratiche di codifica disciplinate.

Poiché Python è così potente e flessibile, abbiamo bisogno di alcune regole o modelli quando si programma in esso. Senza questi principi guida, codebases può diventare rapidamente inflessibile e difficile da mantenere.

Le tre categorie di modelli di design

I modelli di progettazione sono tradizionalmente organizzati in tre categorie principali, ciascuno affrontando diversi aspetti della progettazione del software. Capire queste categorie aiuta gli sviluppatori a selezionare il modello appropriato per le loro sfide specifiche.

Modelli di creazione

I modelli di creazione si concentrano sul processo di creazione di oggetti, fornendo meccanismi che aumentano la flessibilità e il riutilizzo del codice esistente. Questi modelli astraggono il processo di istanza, rendendo i sistemi indipendenti da come gli oggetti vengono creati, composti e rappresentati.

Invece di creare oggetti direttamente utilizzando un costruttore, i modelli creati offrono maggiore controllo e flessibilità sul processo di creazione. Questo approccio è particolarmente prezioso quando la logica di creazione è complessa, quando è necessario controllare quale classe viene istantaneo, o quando si desidera gestire l'allocazione delle risorse in modo più efficiente.

I modelli di creazione comuni in Python includono Singleton, Metodo di fabbrica, Fabbrica astratta, Costruttore e Prototipo. Ognuno di essi serve uno scopo specifico nella gestione della complessità della creazione di oggetti.

Modelli strutturali

I modelli di progettazione strutturale si concentrano sulla composizione di classi o oggetti per formare strutture più grandi e complesse, aiutando ad organizzare e gestire le relazioni tra gli oggetti per ottenere una maggiore flessibilità, riutilizzabilità e manutenbilità, che si occupano di come classi e oggetti siano composti per formare strutture più grandi, mantenendo queste strutture flessibili ed efficienti.

I modelli strutturali aiutano a garantire che quando una parte di un sistema cambia, l'intera struttura non deve essere modificata, facilitando la progettazione di sistemi in cui i componenti possono essere facilmente sostituiti o estesi senza compromettere altre parti dell'applicazione.

Modelli comportamentali

I modelli comportamentali sono preoccupati di algoritmi e l'assegnazione di responsabilità tra oggetti, che non descrivono solo modelli di oggetti o classi, ma anche i modelli di comunicazione tra di loro, che caratterizzano il flusso di controllo complesso che è difficile da seguire in tempo di esecuzione.

L'osservatore affronta la comunicazione, permettendo a più parti di un sistema di reagire automaticamente agli eventi o alle modifiche dello stato. Altri modelli comportamentali includono Strategia, Comando, Iterator, Mediator, Memento, Stato, Metodo dei modelli, Visitatore e Catena di Responsabilità.

Il modello Singleton: Un grado per regolarli tutti

Il Singleton Pattern assicura che una classe abbia un'unica istanza in tutto il programma e fornisce un punto di accesso globale, comunemente usato per gestire risorse condivise come database, sistemi di registrazione o file manager.

Quando usare Singleton

Il modello Singleton è particolarmente utile quando è necessario un'istanza di classe per controllare le risorse come connessioni di database, sistemi di configurazione o di registrazione. Il modello garantisce che tutti i codici che utilizzano l'istanza di classe funzionino con lo stesso oggetto, garantendo coerenza nell'applicazione.

I casi di uso legittimo per singleton in Python includono:

  • Interfacce hardware che rappresentano risorse fisiche uniche, come una fotocamera, una stampante o un'interfaccia GPIO, dove un singolo modello questo esattamente
  • Strati di cache in cui si desidera una singola cache condivisa attraverso l'applicazione
  • Piscine di filettatura o pool di connessione dove si desidera limitare e condividere risorse costose, con la piscina stessa essendo un singleton, anche se le risorse che gestisce non sono
  • Sistemi di gestione della configurazione che devono mantenere impostazioni costanti durante l'applicazione
  • Sistemi di registrazione dove la gestione centralizzata del registro è essenziale

Implementare Singleton in Python

La classe Singleton può essere implementata in modi diversi in Python, tra cui la classe base, l'arredatore e gli approcci di metaclass, con metaclass che è più adatto a questo scopo.

L'implementazione classica con il metodo new [] sembra questo:

class DatabaseConnection:
 _instance = None

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

 def _initialize(self):
 self.connection = None
 self.host = "localhost"
 self.port = 5432

 def connect(self):
 if not self.connection:
 self.connection = f"Connected to {self.host}:{self.port}"
 return self.connection

Un'implementazione sicura per thread utilizza un oggetto di blocco che sincronizza i thread durante il primo accesso al Singleton.Questo è fondamentale nelle applicazioni multi-threaded in cui le condizioni di gara potrebbero portare a molteplici istanze in fase di creazione:

from threading import Lock

class ThreadSafeSingleton:
 _instance = None
 _lock = Lock()

 def __new__(cls):
 if cls._instance is None:
 with cls._lock:
 if cls._instance is None:
 cls._instance = super().__new__(cls)
 return cls._instance

L'alternativa Python: Modulo-Level Singletons

Il sistema di moduli di Python è di per sé un meccanismo singleton: quando si importa un modulo, Python lo esegue una volta e memorizza il risultato in sys.modules, con ogni importazione successiva che ritorna l'oggetto del modulo cache, non un nuovo.

Python importa un modulo solo una volta, rendendo tutto ciò che definito all'interno di un modulo effettivamente un singoloton. Questo approccio è spesso più semplice e più manutenbile che implementare una classe formale Singleton:

# config.py
class _Config:
 def __init__(self):
 self.database_url = "postgresql://localhost/mydb"
 self.api_key = "secret_key"
 self.debug_mode = True

 def update_setting(self, key, value):
 setattr(self, key, value)

# Create the singleton instance
config = _Config()

# In other files, simply import:
# from config import config

Il modello singleton di solito non ha senso in Python nella sua forma più pura; invece, di solito ha più senso fare un'istanza di classe e assegnare quell'istanza a una variabile globale in un modulo.

Sinton: svantaggi e considerazioni

Molti sviluppatori considerano il modello Singleton un antipattern, motivo per cui il suo utilizzo è sul declino del codice Python.

  • Grazie al suo stato globale e all'accoppiamento stretto con altre parti della base di codice, il modello Singleton può rendere difficile il test di unità, con l'inumidimento o la sostituzione dell'istanza singleton essendo ingombrante
  • In ambienti multithreaded, il Singleton Pattern può introdurre problemi di concurrency se non implementati con attenzione, con fili multipli potenzialmente creando più istanze senza una corretta sincronizzazione
  • Sintonizzanti creano dipendenze nascoste che rendono il codice più difficile da capire e mantenere
  • Violano il principio di responsabilità unica gestendo sia la loro logica aziendale che la loro istanza
  • Sintonizzanti rendono difficile estendere o modificare il comportamento attraverso l'eredità

Considerate invece l'utilizzo dell'iniezione di dipendenza, in quanto potrebbe essere più pulito o utilizzare un'istanza di livello modulo, che spesso offre gli stessi vantaggi senza i inconvenienti.

Modello di fabbrica: Creazione di oggetti flessibili

Il modello Factory offre un'interfaccia per creare oggetti senza specificare le loro classi esatte, promuovendo l'accoppiamento sciolto e rendendo il codice più flessibile ai cambiamenti. Questo modello è prezioso quando è necessario creare oggetti, ma vuole decouplare la logica di creazione dal codice che utilizza quegli oggetti.

La fabbrica si concentra su come vengono creati gli oggetti, nascondendo la logica di istanza e riducendo l'accoppiamento stretto tra i componenti.

Semplice applicazione di fabbrica

Il modello Simple Factory incapsula la creazione di oggetti in una funzione o classe dedicata, un esempio pratico per creare diversi tipi di connessioni di database:

from abc import ABC, abstractmethod

class Database(ABC):
 @abstractmethod
 def connect(self):
 pass

 @abstractmethod
 def query(self, sql):
 pass

class PostgreSQLDatabase(Database):
 def connect(self):
 return "Connected to PostgreSQL"

 def query(self, sql):
 return f"PostgreSQL executing: {sql}"

class MySQLDatabase(Database):
 def connect(self):
 return "Connected to MySQL"

 def query(self, sql):
 return f"MySQL executing: {sql}"

class MongoDBDatabase(Database):
 def connect(self):
 return "Connected to MongoDB"

 def query(self, sql):
 return f"MongoDB executing: {sql}"

class DatabaseFactory:
 @staticmethod
 def create_database(db_type):
 databases = {
 'postgresql': PostgreSQLDatabase,
 'mysql': MySQLDatabase,
 'mongodb': MongoDBDatabase
 }

 db_class = databases.get(db_type.lower())
 if not db_class:
 raise ValueError(f"Unknown database type: {db_type}")

 return db_class()

# Usage
db = DatabaseFactory.create_database('postgresql')
print(db.connect())
print(db.query("SELECT * FROM users"))

Modello del metodo di fabbrica

Il Metodo di Fabbrica fornisce un'interfaccia per creare oggetti in una superclasse ma permette alle sottoclassi di alterare il tipo di oggetti che verranno creati. Questa variazione dà ancora più flessibilità delegando l'istantanea alle sottoclassi:

from abc import ABC, abstractmethod

class DocumentProcessor(ABC):
 @abstractmethod
 def create_parser(self):
 """Factory method to be implemented by subclasses"""
 pass

 def process_document(self, content):
 parser = self.create_parser()
 return parser.parse(content)

class Parser(ABC):
 @abstractmethod
 def parse(self, content):
 pass

class JSONParser(Parser):
 def parse(self, content):
 return f"Parsing JSON: {content}"

class XMLParser(Parser):
 def parse(self, content):
 return f"Parsing XML: {content}"

class CSVParser(Parser):
 def parse(self, content):
 return f"Parsing CSV: {content}"

class JSONDocumentProcessor(DocumentProcessor):
 def create_parser(self):
 return JSONParser()

class XMLDocumentProcessor(DocumentProcessor):
 def create_parser(self):
 return XMLParser()

# Usage
json_processor = JSONDocumentProcessor()
result = json_processor.process_document('{"name": "John"}')
print(result)

Vantaggi del modello di fabbrica

Il modello di fabbrica offre diversi vantaggi interessanti:

  • Loose Coupling[[]: Il codice client non ha bisogno di sapere che le classi di cemento sono istantanee
  • Responsabilità del personale[[]: La logica della creazione di oggetti è centralizzata in un unico luogo
  • Principio aperto/permesso[[]: Nuovi tipi possono essere aggiunti senza modificare il codice esistente
  • Flexibility[]: Facile da passare tra diverse implementazioni a runtime
  • Testability[]: Gli oggetti Mock possono essere facilmente iniettati per scopi di test

Modello di Osservatore: Architettura a conduzione Evento

Il modello Observer definisce un meccanismo di abbonamento per informare più oggetti su qualsiasi evento che accada all'oggetto che stanno osservando. Questo modello è fondamentale per la programmazione a eventi ed è ampiamente utilizzato nei framework GUI, nei sistemi in tempo reale e nelle applicazioni reattive.

Comprendere il modello Osservatore

Quando il soggetto (osservabile) cambia lo stato, tutti i suoi dipendenti (osservatori) vengono notificati e aggiornati automaticamente, e questo decouplisce il soggetto dai suoi osservatori, permettendo loro di variare in modo indipendente.

Ecco una completa implementazione del modello Osservatore:

from abc import ABC, abstractmethod
from typing import List

class Observer(ABC):
 @abstractmethod
 def update(self, subject):
 """Receive update from subject"""
 pass

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

 def attach(self, observer: Observer):
 """Attach an observer to the subject"""
 if observer not in self._observers:
 self._observers.append(observer)

 def detach(self, observer: Observer):
 """Detach an observer from the subject"""
 try:
 self._observers.remove(observer)
 except ValueError:
 pass

 def notify(self):
 """Notify all observers about an event"""
 for observer in self._observers:
 observer.update(self)

class StockPrice(Subject):
 def __init__(self, symbol: str, price: float):
 super().__init__()
 self._symbol = symbol
 self._price = price

 @property
 def symbol(self):
 return self._symbol

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

 @price.setter
 def price(self, new_price: float):
 if new_price != self._price:
 self._price = new_price
 self.notify()

class StockDisplay(Observer):
 def __init__(self, name: str):
 self._name = name

 def update(self, subject: StockPrice):
 print(f"{self._name}: {subject.symbol} is now ${subject.price:.2f}")

class StockAlert(Observer):
 def __init__(self, threshold: float):
 self._threshold = threshold

 def update(self, subject: StockPrice):
 if subject.price > self._threshold:
 print(f"ALERT: {subject.symbol} exceeded ${self._threshold}! Current: ${subject.price:.2f}")

# Usage
apple_stock = StockPrice("AAPL", 150.00)

display1 = StockDisplay("Display 1")
display2 = StockDisplay("Display 2")
alert = StockAlert(160.00)

apple_stock.attach(display1)
apple_stock.attach(display2)
apple_stock.attach(alert)

apple_stock.price = 155.50
apple_stock.price = 162.00

Applicazioni reali nel mondo

Il modello Observer è ampiamente utilizzato nello sviluppo moderno del software:

  • GGUI Event Handling[]: Pulsante click, movimenti del mouse e eventi della tastiera
  • Model-View-Controller (MVC)[: Visualizza i modelli per le modifiche dei dati
  • Aggiungimenti di dati a tempo reale[: Prezzi di stock, aggiornamenti meteo, notifiche di social media
  • Sistemi di registrazione[]: Multiplici manici di log che osservano gli eventi delle applicazioni
  • Sistemi di pubblicazione [: code di messaggi e bus di eventi

Modello Decoratore: Extending Functionality Dinamicamente

Il modello Decorator consente di aggiungere comportamenti a un oggetto, registrazione, caching, autenticazione, riprova, senza modificare la classe dell'oggetto e senza sottoclassificarlo, offrendo un'alternativa flessibile alla sottoclassificazione per estendere la funzionalità.

Python Decoratori vs Decorator Pattern

La sintassi @decorator di Python e il modello di design Decorator sono concettualmente lo stesso—entrambi avvolgere il comportamento intorno a una chiamata esistente senza modificarlo, con la sintassi di Python che rende il modello una funzione linguistica-nativa, rendendo Python particolarmente adatto per l'implementazione della funzionalità decoratore.

Ecco un esempio usando la sintassi dell'arredatore di Python:

import time
import functools

def timing_decorator(func):
 @functools.wraps(func)
 def wrapper(*args, **kwargs):
 start_time = time.time()
 result = func(*args, **kwargs)
 end_time = time.time()
 print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
 return result
 return wrapper

def cache_decorator(func):
 cache = {}

 @functools.wraps(func)
 def wrapper(*args):
 if args in cache:
 print(f"Returning cached result for {args}")
 return cache[args]
 result = func(*args)
 cache[args] = result
 return result
 return wrapper

def retry_decorator(max_attempts=3):
 def decorator(func):
 @functools.wraps(func)
 def wrapper(*args, **kwargs):
 for attempt in range(max_attempts):
 try:
 return func(*args, **kwargs)
 except Exception as e:
 if attempt == max_attempts - 1:
 raise
 print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
 return None
 return wrapper
 return decorator

@timing_decorator
@cache_decorator
def fibonacci(n):
 if n < 2:
 return n
 return fibonacci(n-1) + fibonacci(n-2)

@retry_decorator(max_attempts=3)
def unreliable_api_call():
 import random
 if random.random() < 0.7:
 raise ConnectionError("API temporarily unavailable")
 return "Success!"

# Usage
print(fibonacci(10))
print(fibonacci(10)) # This will use cached result

Modello decorativo a base di classe

Il modello tradizionale Decoratore può essere implementato anche utilizzando classi, che è utile per scenari più complessi:

from abc import ABC, abstractmethod

class Coffee(ABC):
 @abstractmethod
 def cost(self):
 pass

 @abstractmethod
 def description(self):
 pass

class SimpleCoffee(Coffee):
 def cost(self):
 return 2.00

 def description(self):
 return "Simple coffee"

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

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

 def description(self):
 return self._coffee.description()

class MilkDecorator(CoffeeDecorator):
 def cost(self):
 return self._coffee.cost() + 0.50

 def description(self):
 return self._coffee.description() + ", milk"

class SugarDecorator(CoffeeDecorator):
 def cost(self):
 return self._coffee.cost() + 0.25

 def description(self):
 return self._coffee.description() + ", sugar"

class WhippedCreamDecorator(CoffeeDecorator):
 def cost(self):
 return self._coffee.cost() + 0.75

 def description(self):
 return self._coffee.description() + ", whipped cream"

# Usage
coffee = SimpleCoffee()
print(f"{coffee.description()}: ${coffee.cost():.2f}")

coffee_with_milk = MilkDecorator(coffee)
print(f"{coffee_with_milk.description()}: ${coffee_with_milk.cost():.2f}")

fancy_coffee = WhippedCreamDecorator(SugarDecorator(MilkDecorator(SimpleCoffee())))
print(f"{fancy_coffee.description()}: ${fancy_coffee.cost():.2f}")

Modello di strategia: Algoritmi intercambiabili

Il modello di strategia consente di definire una famiglia di algoritmi, incapsulare ciascuno, e renderli intercambiabili a runtime, delegare il comportamento a classi di strategia separate o funzioni invece di scrivere grandi blocchi condizionali.

Il modello Strategy definisce una famiglia di algoritmi, li mette in una classe separata e rende i loro oggetti intercambiabili, consentendo di selezionare algoritmi a runtime in base al contesto o alla configurazione.

Attuazione del modello di strategia

Ecco un esempio completo che dimostra il modello di strategia per il trattamento dei pagamenti:

from abc import ABC, abstractmethod
from typing import Protocol

class PaymentStrategy(Protocol):
 def pay(self, amount: float) -> str:
 """Process payment and return confirmation"""
 ...

class CreditCardPayment:
 def __init__(self, card_number: str, cvv: str, expiry: str):
 self.card_number = card_number
 self.cvv = cvv
 self.expiry = expiry

 def pay(self, amount: float) -> str:
 # Simulate payment processing
 masked_card = f"****-****-****-{self.card_number[-4:]}"
 return f"Paid ${amount:.2f} using Credit Card {masked_card}"

class PayPalPayment:
 def __init__(self, email: str):
 self.email = email

 def pay(self, amount: float) -> str:
 return f"Paid ${amount:.2f} using PayPal account {self.email}"

class CryptocurrencyPayment:
 def __init__(self, wallet_address: str, currency: str = "BTC"):
 self.wallet_address = wallet_address
 self.currency = currency

 def pay(self, amount: float) -> str:
 return f"Paid ${amount:.2f} using {self.currency} to wallet {self.wallet_address[:10]}..."

class BankTransferPayment:
 def __init__(self, account_number: str, routing_number: str):
 self.account_number = account_number
 self.routing_number = routing_number

 def pay(self, amount: float) -> str:
 masked_account = f"****{self.account_number[-4:]}"
 return f"Paid ${amount:.2f} via bank transfer from account {masked_account}"

class ShoppingCart:
 def __init__(self):
 self.items = []
 self.payment_strategy = None

 def add_item(self, item: str, price: float):
 self.items.append({'item': item, 'price': price})

 def set_payment_strategy(self, strategy: PaymentStrategy):
 self.payment_strategy = strategy

 def calculate_total(self) -> float:
 return sum(item['price'] for item in self.items)

 def checkout(self) -> str:
 if not self.payment_strategy:
 raise ValueError("Payment strategy not set")

 total = self.calculate_total()
 return self.payment_strategy.pay(total)

# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)
cart.add_item("Keyboard", 79.99)

# Pay with credit card
cart.set_payment_strategy(CreditCardPayment("1234567890123456", "123", "12/25"))
print(cart.checkout())

# Change strategy to PayPal
cart.set_payment_strategy(PayPalPayment("[email protected]"))
print(cart.checkout())

# Change strategy to Cryptocurrency
cart.set_payment_strategy(CryptocurrencyPayment("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"))
print(cart.checkout())

Strategia Python con funzioni

Le funzioni di prima classe di Python permettono una più concisa implementazione del modello Strategy:

def discount_none(total):
 return total

def discount_percentage(percentage):
 def apply_discount(total):
 return total * (1 - percentage / 100)
 return apply_discount

def discount_fixed_amount(amount):
 def apply_discount(total):
 return max(0, total - amount)
 return apply_discount

def discount_bulk(threshold, discount_percentage):
 def apply_discount(total):
 if total >= threshold:
 return total * (1 - discount_percentage / 100)
 return total
 return apply_discount

class Order:
 def __init__(self, total: float):
 self.total = total
 self.discount_strategy = discount_none

 def set_discount_strategy(self, strategy):
 self.discount_strategy = strategy

 def calculate_final_price(self):
 return self.discount_strategy(self.total)

# Usage
order = Order(100.00)
print(f"No discount: ${order.calculate_final_price():.2f}")

order.set_discount_strategy(discount_percentage(10))
print(f"10% discount: ${order.calculate_final_price():.2f}")

order.set_discount_strategy(discount_fixed_amount(15))
print(f"$15 off: ${order.calculate_final_price():.2f}")

order.set_discount_strategy(discount_bulk(50, 20))
print(f"Bulk discount: ${order.calculate_final_price():.2f}")

Modello del costruttore: costruzione di oggetti complessi

Il modello Builder consente di costruire oggetti complessi passo dopo passo, permettendo di produrre diversi tipi e rappresentazioni di un oggetto utilizzando lo stesso codice di costruzione. Questo modello è particolarmente utile quando un oggetto richiede numerose opzioni di configurazione o quando il processo di costruzione comporta più passaggi.

Quando utilizzare il modello di Costruttore

Il modello Builder brilla negli scenari in cui:

  • La costruzione di oggetti richiede molti parametri opzionali
  • Il processo di costruzione deve seguire una sequenza specifica
  • È necessario creare rappresentazioni diverse dello stesso oggetto
  • Parametri costruttori creerebbero un "costruttore di telescoping" antipattern
  • La creazione di oggetti implica una logica complessa che dovrebbe essere separata dall'oggetto stesso

Implementazione del modello di costruttore in Python

Ecco una pratica implementazione per la costruzione di oggetti di query del database:

from typing import List, Optional
from dataclasses import dataclass, field

@dataclass
class Query:
 table: str
 columns: List[str]
 where_clauses: List[str]
 joins: List[str]
 order_by: Optional[str]
 limit: Optional[int]
 offset: Optional[int]

 def to_sql(self) -> str:
 # Select clause
 cols = ", ".join(self.columns) if self.columns else "*"
 sql = f"SELECT {cols} FROM {self.table}"

 # Joins
 if self.joins:
 sql += " " + " ".join(self.joins)

 # Where clause
 if self.where_clauses:
 sql += " WHERE " + " AND ".join(self.where_clauses)

 # Order by
 if self.order_by:
 sql += f" ORDER BY {self.order_by}"

 # Limit and offset
 if self.limit:
 sql += f" LIMIT {self.limit}"
 if self.offset:
 sql += f" OFFSET {self.offset}"

 return sql

class QueryBuilder:
 def __init__(self):
 self._table: Optional[str] = None
 self._columns: List[str] = []
 self._where_clauses: List[str] = []
 self._joins: List[str] = []
 self._order_by: Optional[str] = None
 self._limit: Optional[int] = None
 self._offset: Optional[int] = None

 def table(self, table_name: str) -> 'QueryBuilder':
 self._table = table_name
 return self

 def select(self, *columns: str) -> 'QueryBuilder':
 self._columns.extend(columns)
 return self

 def where(self, condition: str) -> 'QueryBuilder':
 self._where_clauses.append(condition)
 return self

 def join(self, join_clause: str) -> 'QueryBuilder':
 self._joins.append(join_clause)
 return self

 def order_by(self, column: str, direction: str = "ASC") -> 'QueryBuilder':
 self._order_by = f"{column} {direction}"
 return self

 def limit(self, limit: int) -> 'QueryBuilder':
 self._limit = limit
 return self

 def offset(self, offset: int) -> 'QueryBuilder':
 self._offset = offset
 return self

 def build(self) -> Query:
 if not self._table:
 raise ValueError("Table name is required")

 return Query(
 table=self._table,
 columns=self._columns,
 where_clauses=self._where_clauses,
 joins=self._joins,
 order_by=self._order_by,
 limit=self._limit,
 offset=self._offset
 )

 def reset(self) -> 'QueryBuilder':
 self.__init__()
 return self

# Usage
builder = QueryBuilder()

# Build a simple query
query1 = (builder
 .table("users")
 .select("id", "name", "email")
 .where("age > 18")
 .where("status = 'active'")
 .order_by("name", "ASC")
 .limit(10)
 .build())

print(query1.to_sql())

# Build a complex query with joins
builder.reset()
query2 = (builder
 .table("orders")
 .select("orders.id", "users.name", "products.title", "orders.total")
 .join("INNER JOIN users ON orders.user_id = users.id")
 .join("INNER JOIN products ON orders.product_id = products.id")
 .where("orders.status = 'completed'")
 .where("orders.total > 100")
 .order_by("orders.created_at", "DESC")
 .limit(20)
 .offset(10)
 .build())

print(query2.to_sql())

Vantaggi dell'interfaccia fluida

Il modello Builder spesso implementa un'interfaccia fluente (catenazione del metallo), che fornisce diversi vantaggi:

  • Leggibilità[]: Il codice legge come il linguaggio naturale
  • Flexibility[]: Facile da aggiungere o rimuovere i passaggi di configurazione
  • Immutabilità[[]: L'oggetto finale può essere immutabile mentre il costruttore è mutabile
  • Validation[: La logica della costruzione può convalidare l'oggetto prima della creazione
  • Riusabilità[]: I costruttori possono essere riutilizzati per creare oggetti simili multipli

Modello adattatore: Rendere interfacce incompatibili lavorare insieme

Il modello Adattatore permette di collaborare con oggetti con interfacce incompatibili, che funge da ponte tra due interfacce incompatibili, consentendo alle classi di lavorare insieme che non potrebbero altrimenti a causa di interfacce incompatibili.

Il metodo adattatore è un modello di progettazione strutturale che consente di realizzare due interfacce incompatibili, creando un ponte tra loro, particolarmente prezioso quando si integrano librerie di terze parti, codice legacy o API esterne nella vostra applicazione.

Realizzazione adattatore mondiale

Considera uno scenario in cui è necessario integrare più gateway di pagamento con diverse interfacce:

from abc import ABC, abstractmethod

# Target interface that our application expects
class PaymentProcessor(ABC):
 @abstractmethod
 def process_payment(self, amount: float, currency: str) -> dict:
 pass

# Adaptee 1: Stripe API (incompatible interface)
class StripeAPI:
 def create_charge(self, amount_cents: int, currency_code: str, source: str):
 return {
 'id': 'ch_stripe_123',
 'amount': amount_cents,
 'currency': currency_code,
 'status': 'succeeded'
 }

# Adaptee 2: PayPal API (different incompatible interface)
class PayPalAPI:
 def make_payment(self, total: float, currency_type: str, account: str):
 return {
 'transaction_id': 'pp_456',
 'total_amount': total,
 'currency': currency_type,
 'state': 'approved'
 }

# Adaptee 3: Square API (yet another incompatible interface)
class SquareAPI:
 def charge_card(self, money_amount: dict, card_token: str):
 return {
 'payment_id': 'sq_789',
 'amount_money': money_amount,
 'status': 'COMPLETED'
 }

# Adapter for Stripe
class StripeAdapter(PaymentProcessor):
 def __init__(self, stripe_api: StripeAPI):
 self.stripe = stripe_api

 def process_payment(self, amount: float, currency: str) -> dict:
 # Convert dollars to cents for Stripe
 amount_cents = int(amount * 100)

 # Call Stripe API with adapted parameters
 result = self.stripe.create_charge(
 amount_cents=amount_cents,
 currency_code=currency.upper(),
 source='tok_visa'
 )

 # Adapt the response to our standard format
 return {
 'success': result['status'] == 'succeeded',
 'transaction_id': result['id'],
 'amount': amount,
 'currency': currency,
 'provider': 'Stripe'
 }

# Adapter for PayPal
class PayPalAdapter(PaymentProcessor):
 def __init__(self, paypal_api: PayPalAPI):
 self.paypal = paypal_api

 def process_payment(self, amount: float, currency: str) -> dict:
 result = self.paypal.make_payment(
 total=amount,
 currency_type=currency.upper(),
 account='[email protected]'
 )

 return {
 'success': result['state'] == 'approved',
 'transaction_id': result['transaction_id'],
 'amount': amount,
 'currency': currency,
 'provider': 'PayPal'
 }

# Adapter for Square
class SquareAdapter(PaymentProcessor):
 def __init__(self, square_api: SquareAPI):
 self.square = square_api

 def process_payment(self, amount: float, currency: str) -> dict:
 money_amount = {
 'amount': int(amount * 100),
 'currency': currency.upper()
 }

 result = self.square.charge_card(
 money_amount=money_amount,
 card_token='cnon:card-token'
 )

 return {
 'success': result['status'] == 'COMPLETED',
 'transaction_id': result['payment_id'],
 'amount': amount,
 'currency': currency,
 'provider': 'Square'
 }

# Client code that works with the unified interface
class PaymentService:
 def __init__(self, processor: PaymentProcessor):
 self.processor = processor

 def charge_customer(self, amount: float, currency: str = 'USD'):
 result = self.processor.process_payment(amount, currency)

 if result['success']:
 print(f"✓ Payment successful via {result['provider']}")
 print(f" Transaction ID: {result['transaction_id']}")
 print(f" Amount: {result['amount']} {result['currency']}")
 else:
 print(f"✗ Payment failed")

 return result

# Usage - all payment gateways work through the same interface
stripe_processor = StripeAdapter(StripeAPI())
paypal_processor = PayPalAdapter(PayPalAPI())
square_processor = SquareAdapter(SquareAPI())

# Process payments using different providers
service = PaymentService(stripe_processor)
service.charge_customer(99.99)

service = PaymentService(paypal_processor)
service.charge_customer(149.99)

service = PaymentService(square_processor)
service.charge_customer(199.99)

Modello Metodo Pattern: Definire gli schelettrici dell'algoritmo

Il Metodo Template definisce lo scheletro di un algoritmo nella superclasse ma permette alle sottoclassi di superare i passaggi specifici dell'algoritmo senza cambiare la sua struttura. Questo modello comportamentale è eccellente per rafforzare un processo coerente, consentendo la personalizzazione a punti specifici.

Metodo di modello Attuazione

from abc import ABC, abstractmethod
import time

class DataProcessor(ABC):
 """Template class defining the data processing algorithm"""

 def process(self, data):
 """Template method defining the algorithm structure"""
 print("Starting data processing pipeline...")

 # Step 1: Validate
 if not self.validate(data):
 raise ValueError("Data validation failed")

 # Step 2: Extract
 extracted = self.extract(data)

 # Step 3: Transform
 transformed = self.transform(extracted)

 # Step 4: Load
 result = self.load(transformed)

 # Step 5: Cleanup (optional hook)
 self.cleanup()

 print("Data processing completed successfully")
 return result

 @abstractmethod
 def validate(self, data) -> bool:
 """Validate input data - must be implemented by subclasses"""
 pass

 @abstractmethod
 def extract(self, data):
 """Extract relevant data - must be implemented by subclasses"""
 pass

 @abstractmethod
 def transform(self, data):
 """Transform data - must be implemented by subclasses"""
 pass

 @abstractmethod
 def load(self, data):
 """Load processed data - must be implemented by subclasses"""
 pass

 def cleanup(self):
 """Optional hook method - can be overridden by subclasses"""
 print("Performing default cleanup...")

class CSVDataProcessor(DataProcessor):
 def validate(self, data) -> bool:
 print("Validating CSV data...")
 return isinstance(data, str) and len(data) > 0

 def extract(self, data):
 print("Extracting CSV data...")
 lines = data.strip().split('n')
 headers = lines[0].split(',')
 rows = [line.split(',') for line in lines[1:]]
 return {'headers': headers, 'rows': rows}

 def transform(self, data):
 print("Transforming CSV data...")
 headers = data['headers']
 rows = data['rows']
 return [dict(zip(headers, row)) for row in rows]

 def load(self, data):
 print(f"Loading {len(data)} CSV records...")
 return data

 def cleanup(self):
 print("Cleaning up CSV processing resources...")

class JSONDataProcessor(DataProcessor):
 def validate(self, data) -> bool:
 print("Validating JSON data...")
 import json
 try:
 json.loads(data)
 return True
 except:
 return False

 def extract(self, data):
 print("Extracting JSON data...")
 import json
 return json.loads(data)

 def transform(self, data):
 print("Transforming JSON data...")
 if isinstance(data, list):
 return [self._flatten_dict(item) for item in data]
 return [self._flatten_dict(data)]

 def _flatten_dict(self, d, parent_key='', sep='_'):
 items = []
 for k, v in d.items():
 new_key = f"{parent_key}{sep}{k}" if parent_key else k
 if isinstance(v, dict):
 items.extend(self._flatten_dict(v, new_key, sep=sep).items())
 else:
 items.append((new_key, v))
 return dict(items)

 def load(self, data):
 print(f"Loading {len(data)} JSON records...")
 return data

class XMLDataProcessor(DataProcessor):
 def validate(self, data) -> bool:
 print("Validating XML data...")
 return data.strip().startswith('')

 def extract(self, data):
 print("Extracting XML data...")
 # Simplified XML parsing
 return {'xml_content': data}

 def transform(self, data):
 print("Transforming XML data...")
 return {'processed_xml': data['xml_content']}

 def load(self, data):
 print("Loading XML data...")
 return data

 def cleanup(self):
 print("Cleaning up XML processing resources...")
 print("Closing XML parser...")

# Usage
csv_data = """name,age,city
John,30,New York
Jane,25,Los Angeles
Bob,35,Chicago"""

json_data = '''[
 {"name": "John", "age": 30, "address": {"city": "New York"}},
 {"name": "Jane", "age": 25, "address": {"city": "Los Angeles"}}
]'''

xml_data = "John30"

# Process CSV
csv_processor = CSVDataProcessor()
csv_result = csv_processor.process(csv_data)
print(f"CSV Result: {csv_result}n")

# Process JSON
json_processor = JSONDataProcessor()
json_result = json_processor.process(json_data)
print(f"JSON Result: {json_result}n")

# Process XML
xml_processor = XMLDataProcessor()
xml_result = xml_processor.process(xml_data)
print(f"XML Result: {xml_result}")

Quando usare modelli di progettazione

Sapere quando utilizzare i modelli di progettazione è fondamentale per un'efficace progettazione software, in particolare quando si incontrano problemi di progettazione ricorrenti che hanno soluzioni consolidate, come i modelli di progettazione forniscono approcci testati e collaudati alle sfide di progettazione del software comune.

Casi di utilizzo appropriati

Utilizzare modelli di progettazione per promuovere la riutilizzabilità del codice, la flessibilità e la manutenbilità, in quanto aiutano a strutturare il codice in modo che rende più facile modificare ed estendere come i requisiti si evolvono.

  • Hai un problema che corrisponde all'intento di un modello conosciuto
  • Il modello offre vantaggi chiari su una soluzione più semplice
  • Il vostro team comprende il modello e può mantenerlo
  • La complessità aggiunta è giustificata da una maggiore flessibilità o manutenbilità
  • Si desidera migliorare la comunicazione tra i membri del team

Quando NON usare i modelli

Il codice migliore è il codice più semplice che risolve correttamente il problema, a volte è un modello di design ben posizionato, ma spesso è solo una funzione e una ditta.

  • Una soluzione più semplice funzionerebbe anche
  • Stai applicando dei modelli per usare i modelli
  • Il modello aggiunge complessità inutile al codice semplice
  • Il vostro team non conosce il modello e la documentazione manca
  • Il problema non corrisponde all'intento del modello.

Ogni modello ha i suoi trade-off, e è necessario prestare attenzione più al perché si sta scegliendo un certo modello che a come implementarlo. La decisione di utilizzare un modello dovrebbe essere guidata dal problema a portata di mano, non da un desiderio di dimostrare la conoscenza dei modelli.

Anti-Patterns da evitare in Python

I moduli Python sono già singolitoni, ogni modulo viene importato solo una volta, così le classi singleton esplicite aggiungono complessità inutili, con alternative migliori che sono variabili di livello modulo o iniezione di dipendenza.

Modelli che non si adattano Python Well

  • Dio oggetto[: Centralizza troppa logica in una singola classe, rende il codice più difficile da testare e mantenere, con la migliore alternativa essere di divisione della funzionalità in classi più piccole e coessive
  • Deep Inheritance Gerarchies[[: Gli alberi di eredità profondi rendono il codice fragile, quindi preferiscono la composizione e la delegazione
  • Astrazioni non necessarie[[]: La digitazione dell'anatra di Python elimina spesso la necessità di gerarchie complesse dell'interfaccia

Considerazioni di pattern Python moderne

Nel moderno Python (3.8+), preferisca il Protocollo per la sottotipazione strutturale in quanto non richiede un'eredità esplicita, con classi che soddisfano un Protocollo solo avendo i metodi giusti, mentre si utilizza ABC quando si desidera imporre l'eredità e fornire implementazioni di default.

Utilizzo dei protocolli per la digitazione di un ponte

from typing import Protocol

class Drawable(Protocol):
 def draw(self) -> str:
 ...

class Circle:
 def draw(self) -> str:
 return "Drawing a circle"

class Square:
 def draw(self) -> str:
 return "Drawing a square"

def render(shape: Drawable) -> None:
 print(shape.draw())

# Both Circle and Square satisfy the Drawable protocol
# without explicit inheritance
render(Circle())
render(Square())

Modelli di progettazione in applicazioni Python di produzione

Nel 2026, Python si trova all'incrocio tra AI e machine learning, sviluppo scalabile di prodotti web e ingegneria dei dati aziendali, con la sua posizione dominante che riflette un vantaggio strutturale che i team di ingegneria hanno scoperto anni fa: Python ti permette di muoversi più velocemente, integrare più facilmente e costruire sistemi che sono mantenuti da team, non solo dall'autore originale.

Modelli quadro-Specifici

Django rimane il più completo framework Python per team che costruiscono prodotti con accesso multi-utente, modelli di dati complessi, interfacce di amministrazione e sistemi di autenticazione.

  • Model-View-Template (MVT): Variazione di Django di MVC
  • Active Record: modelli ORM di Django
  • Metodo di temperatura[]: Vista a base di classe
  • Catena di mezzo[]: Richiesta/risponsabilità di elaborazione

FastAPI sfrutta le moderne caratteristiche e modelli Python:

  • Iniezione di dipendenza[: Sistema DI integrato
  • Decorator Pattern: Decoratori per la strada
  • Strategy Pattern[]: Autenticazione Pluggable
  • Schema di fabbrica: Creazione di modelli di risposta

Modelli architettonici

Nel 2026, il consenso tra i team di ingegneri Python esperti è più chiaro: iniziare con un monolite ben strutturato, decomporre in servizi quando i confini specifici emergono dall'uso reale, in quanto un monolite non è una modalità di fallimento.

Instagram ha eseguito su un monolite Django ben oltre 100 milioni di utenti prima di decomposing specifiche funzioni ad alto carico in servizi, con la chiave di costruzione del monolite con decomposizione di servizio in mente dall'inizio attraverso i confini del modulo chiaro, l'accoppiamento trasversale minimo e modelli di comunicazione orientati agli eventi.

Provare modelli di progettazione

I modelli di progettazione dovrebbero rendere il codice più testabile, non meno. Ecco le strategie per testare le implementazioni dei modelli:

Provare il modello di strategia

import unittest
from unittest.mock import Mock

class TestPaymentStrategies(unittest.TestCase):
 def test_credit_card_payment(self):
 strategy = CreditCardPayment("1234567890123456", "123", "12/25")
 result = strategy.pay(100.00)
 self.assertIn("Credit Card", result)
 self.assertIn("100.00", result)

 def test_paypal_payment(self):
 strategy = PayPalPayment("[email protected]")
 result = strategy.pay(50.00)
 self.assertIn("PayPal", result)
 self.assertIn("[email protected]", result)

 def test_shopping_cart_with_different_strategies(self):
 cart = ShoppingCart()
 cart.add_item("Item 1", 50.00)

 # Test with credit card
 cart.set_payment_strategy(CreditCardPayment("1234", "123", "12/25"))
 result1 = cart.checkout()

 # Test with PayPal
 cart.set_payment_strategy(PayPalPayment("[email protected]"))
 result2 = cart.checkout()

 self.assertIsNotNone(result1)
 self.assertIsNotNone(result2)

Provare il modello di Osservatore

class TestObserverPattern(unittest.TestCase):
 def test_observer_notification(self):
 stock = StockPrice("AAPL", 150.00)
 observer = Mock(spec=Observer)

 stock.attach(observer)
 stock.price = 155.00

 observer.update.assert_called_once_with(stock)

 def test_multiple_observers(self):
 stock = StockPrice("GOOGL", 2800.00)
 observer1 = Mock(spec=Observer)
 observer2 = Mock(spec=Observer)

 stock.attach(observer1)
 stock.attach(observer2)
 stock.price = 2850.00

 observer1.update.assert_called_once()
 observer2.update.assert_called_once()

 def test_detach_observer(self):
 stock = StockPrice("MSFT", 300.00)
 observer = Mock(spec=Observer)

 stock.attach(observer)
 stock.detach(observer)
 stock.price = 310.00

 observer.update.assert_not_called()

Considerazioni sulle prestazioni

Mentre i modelli di progettazione migliorano l'organizzazione del codice e la manutenbilità, possono introdurre la sovraccarica delle prestazioni se non implementata con attenzione.

Inizializzazione pigro

L'istanza viene creata solo quando il metodo get instance() viene richiesto per la prima volta, assicurando che le risorse vengano assegnate solo quando necessario.

class DatabaseConnection:
 _instance = None
 _initialized = False

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

 def __init__(self):
 if not DatabaseConnection._initialized:
 self._connect()
 DatabaseConnection._initialized = True

 def _connect(self):
 # Expensive connection operation
 print("Establishing database connection...")
 self.connection = "Connected"

Risultati di Decorazione di Caching

from functools import lru_cache

@lru_cache(maxsize=128)
def expensive_computation(n):
 """Cached using built-in LRU cache"""
 print(f"Computing for {n}...")
 return sum(range(n))

# First call computes
result1 = expensive_computation(1000)

# Second call returns cached result
result2 = expensive_computation(1000)

Migliori Pratiche per Modelli Di Design in Python

I modelli di progettazione dovrebbero semplificare, non complicare, poiché i modelli di design Python non sono circa la copia di diagrammi di libri di testo—si tratta di risolvere problemi reali elegantemente.

1. Composizione preferita sopra l'eritanza

La natura dinamica di Python rende la composizione particolarmente potente, invece di gerarchie profonde di eredità, compone oggetti di componenti più piccoli e concentrati:

# Instead of inheritance
class EmailNotifier:
 def send(self, message):
 print(f"Sending email: {message}")

class SMSNotifier:
 def send(self, message):
 print(f"Sending SMS: {message}")

# Use composition
class NotificationService:
 def __init__(self):
 self.notifiers = []

 def add_notifier(self, notifier):
 self.notifiers.append(notifier)

 def notify(self, message):
 for notifier in self.notifiers:
 notifier.send(message)

# Usage
service = NotificationService()
service.add_notifier(EmailNotifier())
service.add_notifier(SMSNotifier())
service.notify("Important update!")

2. Utilizzare Tipo di suggerimenti per la precisione

I suggerimenti di tipo rendono le implementazioni di modelli più esplicite e consentono un migliore supporto IDE:

from typing import Protocol, List
from abc import abstractmethod

class PaymentMethod(Protocol):
 def process(self, amount: float) -> bool:
 ...

class PaymentProcessor:
 def __init__(self, methods: List[PaymentMethod]):
 self.methods = methods

 def charge(self, amount: float) -> bool:
 for method in self.methods:
 if method.process(amount):
 return True
 return False

3. Uso del modello del documento

Documentare sempre quale modello si sta utilizzando e perché:

class DataExporter:
 """
 Uses the Strategy pattern to support multiple export formats.

 This allows adding new export formats without modifying existing code,
 following the Open/Closed Principle.

 Example:
 exporter = DataExporter(JSONExportStrategy())
 exporter.export(data)
 """

 def __init__(self, strategy: ExportStrategy):
 self._strategy = strategy

 def export(self, data):
 return self._strategy.export(data)

4. Tenere Semplice

Non esagerare, inizia a fare semplice e rifattore a modelli quando la complessità lo giustifica:

# Simple is often better
def calculate_discount(price, customer_type):
 discounts = {
 'regular': 0,
 'premium': 0.10,
 'vip': 0.20
 }
 return price * (1 - discounts.get(customer_type, 0))

# Only introduce Strategy pattern when you need:
# - Runtime strategy switching
# - Complex discount logic
# - Multiple discount calculation methods

Risorse per i modelli di progettazione di apprendimento

Per approfondire la vostra comprensione dei modelli di design in Python, esplora queste preziose risorse:

Conclusioni

I modelli di progettazione sono strumenti potenti in un toolkit di Python Engine, ma dovrebbero essere applicati in modo giudiziario. I modelli di strategia, fabbrica e Osservatore spesso appaiono insieme in sistemi Python ben strutturati, con la strategia che si concentra sulla selezione e la swapping degli algoritmi, la Factory che si concentra su come gli oggetti sono creati e l'osservatore affronta la comunicazione, con la comprensione di queste distinzioni che aiutano a scegliere il modello giusto in base se la vostra sfida è sul comportamento, la creazione o la comunicazione.

La chiave per utilizzare con successo i modelli di design in Python è la comprensione sia dei modelli stessi che delle caratteristiche uniche di Python. La digitazione dinamica di Python, le funzioni di prima classe e la potente libreria standard spesso permettono di implementazioni più semplici che in linguaggi staticamente di tipo.

Ricorda che i modelli sono mezzi per una fine, non finisce in se stessi. L'obiettivo è quello di scrivere codice che è facile da capire, testare e modificare. Quando un modello aiuta a raggiungere tale obiettivo, usarlo. Quando una soluzione più semplice basta, abbraccia la semplicità. Come si guadagna l'esperienza, si svilupperà un'intuizione per quando i modelli aggiungono valore e quando aggiungono complessità inutili.

Padroneggiando i modelli di progettazione e la comprensione quando applicarli, sarete meglio attrezzati per costruire applicazioni Python robuste e scalabili che si basano sulla prova del tempo e dei requisiti in evoluzione.