Python e programmazione a oggetti: composizione e collaborazione tra classi

by theArchitect
SHARE
Python e programmazione a oggetti: composizione e collaborazione tra classi
© Guida-HTML5.it

Introduzione

Quando si inizia a studiare la programmazione a oggetti in Python, è facile concentrarsi subito su classi, oggetti, costruttori ed ereditarietà. Tuttavia, c’è un concetto altrettanto importante e molto pratico: la composizione. Invece di costruire una gerarchia di classi “padre-figlio”, la composizione permette di creare oggetti complessi combinando oggetti più semplici al loro interno.

Questo approccio è estremamente utile nei progetti reali perché rende il codice più modulare, più facile da testare e spesso più flessibile dell’ereditarietà. In altre parole, se vuoi modellare un sistema in modo chiaro, la domanda da porti non è solo “chi eredita da chi?”, ma anche “quali oggetti collaborano tra loro?”.

In questo tutorial vedremo come usare la composizione in Python per progettare classi che lavorano insieme in modo pulito. Useremo un esempio concreto: un sistema di gestione ordini con clienti, prodotti e carrello. È un caso realistico, semplice da capire e molto vicino a problemi che si incontrano nello sviluppo software.

Codice completo

from dataclasses import dataclass
from typing import List


@dataclass
class Product:
    name: str
    price: float

    def __post_init__(self):
        if self.price < 0:
            raise ValueError("Il prezzo non può essere negativo.")


class Cart:
    def __init__(self):
        self.items: List[Product] = []

    def add_product(self, product: Product) -> None:
        self.items.append(product)

    def total(self) -> float:
        return sum(product.price for product in self.items)

    def list_items(self) -> None:
        if not self.items:
            print("Il carrello è vuoto.")
            return

        for product in self.items:
            print(f"- {product.name}: €{product.price:.2f}")


class Customer:
    def __init__(self, name: str):
        self.name = name
        self.cart = Cart()  # composizione: il cliente possiede un carrello

    def add_to_cart(self, product: Product) -> None:
        self.cart.add_product(product)

    def show_summary(self) -> None:
        print(f"Cliente: {self.name}")
        self.cart.list_items()
        print(f"Totale: €{self.cart.total():.2f}")


class Order:
    def __init__(self, customer: Customer):
        self.customer = customer
        self.status = "in preparazione"

    def confirm(self) -> None:
        if not self.customer.cart.items:
            print("Impossibile confermare l´ordine: carrello vuoto.")
            return

        self.status = "confermato"
        print(f"Ordine confermato per {self.customer.name}.")

    def summary(self) -> None:
        print(f"Stato ordine: {self.status}")
        self.customer.show_summary()


# --- Esempio di utilizzo ---
p1 = Product("Tastiera meccanica", 89.90)
p2 = Product("Mouse wireless", 34.50)
p3 = Product("Monitor 27 pollici", 229.99)

customer = Customer("Luca")
customer.add_to_cart(p1)
customer.add_to_cart(p2)
customer.add_to_cart(p3)

order = Order(customer)
order.confirm()
order.summary()

Spiegazione

Nel codice abbiamo definito quattro classi: Product, Cart, Customer e Order. Ognuna ha una responsabilità precisa, e questo è uno degli obiettivi principali della programmazione a oggetti ben progettata.

1. Product: un oggetto semplice e riutilizzabile

La classe Product rappresenta un prodotto con nome e prezzo. Abbiamo usato @dataclass per ridurre il boilerplate e rendere la classe più leggibile. Il metodo __post_init__ ci permette di controllare che il prezzo non sia negativo.

Questo è un ottimo esempio di oggetto “atomico”: non fa molto, ma è chiaro, stabile e facile da riutilizzare in altri contesti.

2. Cart: una classe che contiene altri oggetti

La classe Cart è il cuore della composizione. Al suo interno mantiene una lista di oggetti Product. Invece di far ereditare il carrello da un’altra classe, lo costruiamo usando oggetti già esistenti.

Il metodo add_product aggiunge un prodotto, total calcola il totale del carrello e list_items mostra il contenuto. Questa classe ha una responsabilità molto chiara: gestire l’elenco dei prodotti e il loro totale.

3. Customer: un oggetto che possiede un carrello

La classe Customer contiene un attributo cart creato dentro il costruttore. Questo significa che ogni cliente ha il proprio carrello. È un esempio perfetto di composizione: il cliente non è un carrello, ma ha un carrello.

Il metodo add_to_cart non aggiunge direttamente il prodotto a una lista interna del cliente, ma delega il lavoro al carrello. Questo è importante: la classe cliente non deve conoscere i dettagli di implementazione del carrello, ma solo usarlo.

4. Order: collaborazione tra oggetti

La classe Order riceve un oggetto Customer e utilizza il suo carrello per verificare se l’ordine può essere confermato. Anche qui vediamo una relazione di collaborazione: l’ordine non gestisce prodotti direttamente, ma si appoggia al cliente e al suo carrello.

Il metodo confirm controlla che il carrello non sia vuoto e aggiorna lo stato dell’ordine. Il metodo summary mostra un riepilogo completo usando la logica già presente nel cliente.

Perché questo approccio è utile?

La composizione è utile perché riduce l’accoppiamento tra le classi. Se domani volessi cambiare il comportamento del carrello, potrei farlo senza riscrivere tutta la logica del cliente o dell’ordine. Questo rende il sistema più facile da mantenere.

Inoltre, la composizione favorisce il riuso: la stessa classe Cart potrebbe essere usata in un altro progetto, oppure affiancata a una classe diversa da Customer.

Best practice

  • Preferisci la composizione quando un oggetto “ha” un altro oggetto: ad esempio, un cliente ha un carrello, un ordine ha una lista di righe, una biblioteca ha libri.
  • Assegna a ogni classe una sola responsabilità: classi piccole e focalizzate sono più facili da testare e da modificare.
  • Evita di mettere troppa logica in una sola classe: se una classe fa troppo, diventa difficile da capire e da estendere.
  • Delegare è spesso meglio che duplicare: se una classe già sa fare qualcosa, usa quella logica invece di riscriverla.
  • Usa type hint per rendere chiaro il contratto tra gli oggetti: aiuta te, il tuo team e gli strumenti di analisi statica.
  • Valida i dati in ingresso: nel nostro esempio controlliamo che il prezzo non sia negativo, evitando stati incoerenti.
  • Progetta pensando ai test: classi piccole e ben separate sono molto più semplici da verificare con test automatici.

Riepilogo

In questo tutorial abbiamo visto un aspetto fondamentale della programmazione a oggetti in Python: la composizione. Abbiamo costruito un piccolo sistema di gestione ordini usando oggetti che collaborano tra loro in modo chiaro e modulare.

I punti chiave da ricordare sono:

  • la composizione è l’idea di costruire oggetti complessi usando oggetti più semplici;
  • è spesso più flessibile dell’ereditarietà per modellare relazioni reali;
  • aiuta a separare le responsabilità tra le classi;
  • rende il codice più facile da mantenere, riusare e testare.

Se stai muovendo i primi passi nella programmazione a oggetti, imparare a riconoscere quando usare la composizione è un salto di qualità importante. Ti permette di scrivere codice più pulito e di progettare sistemi più vicini al mondo reale.

Approfondisci con risorse ufficiali

SHARE