Parliamo

project-cherry.dev

Intermedio14 min

Classi e OOP

Modella clienti, pratiche e fatture dello studio legale con classi Python: ereditarietà, metodi speciali e composizione.

Classi e OOP

Lo studio legale dell'Avv. Marco Rossi gestisce clienti, pratiche e fatture. Finora abbiamo usato dizionari per rappresentare questi dati, ma quando le relazioni diventano più complesse serve uno strumento migliore. Le classi permettono di raggruppare dati e comportamenti in un unico oggetto, rendendo il codice più organizzato e manutenibile.

La prima classe: Cliente

Partiamo dal dizionario cliente che già conosciamo e trasformiamolo in una classe con gli stessi campi: nome, cognome, codice_fiscale e tipo.

python
class Cliente:
    def __init__(self, nome, cognome, codice_fiscale, tipo="privato"):
        self.nome = nome
        self.cognome = cognome
        self.codice_fiscale = codice_fiscale
        self.tipo = tipo  # "privato" o "aziendale"

    def nome_completo(self):
        return f"{self.nome} {self.cognome}"

cliente = Cliente("Laura", "Verdi", "VRDLRA85M41H501Z")
print(cliente.nome_completo())  # Laura Verdi
print(cliente.tipo)             # privato

Il metodo __init__ è il costruttore: viene eseguito quando crei un nuovo oggetto. self è il riferimento all'istanza corrente, simile a this in altri linguaggi.

Ereditarietà: ClientePrivato e ClienteAziendale

Un cliente privato e uno aziendale condividono i campi base, ma hanno informazioni aggiuntive diverse. L'ereditarietà evita di duplicare codice.

python
class ClientePrivato(Cliente):
    def __init__(self, nome, cognome, codice_fiscale, telefono):
        super().__init__(nome, cognome, codice_fiscale, tipo="privato")
        self.telefono = telefono

class ClienteAziendale(Cliente):
    def __init__(self, nome, cognome, codice_fiscale, partita_iva, ragione_sociale):
        super().__init__(nome, cognome, codice_fiscale, tipo="aziendale")
        self.partita_iva = partita_iva
        self.ragione_sociale = ragione_sociale

privato = ClientePrivato("Marco", "Rossi", "RSSMRC80A01H501X", "333-1234567")
azienda = ClienteAziendale("Sara", "Conti", "CNTSAR75B42H501Y", "IT01234567890", "Conti & Associati")

print(privato.nome_completo())         # Marco Rossi
print(azienda.ragione_sociale)         # Conti & Associati

super().__init__(...) chiama il costruttore della classe genitore, così non ripetiamo l'inizializzazione dei campi comuni.

Metodi speciali: __str__ e __repr__

I dunder methods (double underscore) personalizzano come Python tratta i tuoi oggetti. __str__ definisce la rappresentazione leggibile, __repr__ quella tecnica utile per il debug.

python
class Cliente:
    def __init__(self, nome, cognome, codice_fiscale, tipo="privato"):
        self.nome = nome
        self.cognome = cognome
        self.codice_fiscale = codice_fiscale
        self.tipo = tipo

    def __str__(self):
        return f"{self.nome} {self.cognome} ({self.tipo})"

    def __repr__(self):
        return f"Cliente(nome='{self.nome}', cognome='{self.cognome}', cf='{self.codice_fiscale}')"

cliente = Cliente("Laura", "Verdi", "VRDLRA85M41H501Z")
print(cliente)        # Laura Verdi (privato)
print(repr(cliente))  # Cliente(nome='Laura', cognome='Verdi', cf='VRDLRA85M41H501Z')

Composizione: una Pratica contiene un Cliente

La composizione è il pattern dove un oggetto contiene un altro oggetto. Una pratica legale è legata a un cliente specifico: non ha senso senza.

python
class Pratica:
    def __init__(self, numero, cliente, descrizione, tariffa_oraria=150.0):
        self.numero = numero
        self.cliente = cliente  # oggetto Cliente
        self.descrizione = descrizione
        self.tariffa_oraria = tariffa_oraria
        self.ore_lavorate = 0
        self.stato = "aperta"

    def registra_ore(self, ore):
        self.ore_lavorate += ore

    def calcola_importo(self):
        return self.ore_lavorate * self.tariffa_oraria

    def __str__(self):
        return f"Pratica {self.numero}: {self.descrizione} - {self.cliente.nome_completo()}"

# Creiamo cliente e pratica collegati
cliente = Cliente("Laura", "Verdi", "VRDLRA85M41H501Z")
pratica = Pratica("2024-001", cliente, "Contenzioso civile")
pratica.registra_ore(12)

print(pratica)                              # Pratica 2024-001: Contenzioso civile - Laura Verdi
print(f"Importo: {pratica.calcola_importo():.2f} EUR")  # Importo: 1800.00 EUR

Esempio completo: sistema di fatturazione

Ora colleghiamo tutto. Una Fattura nasce da una Pratica, che a sua volta è collegata a un Cliente. Questo è il vantaggio della OOP: ogni oggetto ha la propria responsabilità.

python
class Fattura:
    _contatore = 0  # variabile di classe, condivisa tra tutte le istanze

    def __init__(self, pratica, aliquota_iva=22):
        Fattura._contatore += 1
        self.numero = f"FT-{Fattura._contatore:04d}"
        self.pratica = pratica  # oggetto Pratica
        self.aliquota_iva = aliquota_iva
        self.importo_netto = pratica.calcola_importo()
        self.importo_iva = self.importo_netto * aliquota_iva / 100
        self.importo_totale = self.importo_netto + self.importo_iva

    def __str__(self):
        return (
            f"Fattura {self.numero}\n"
            f"Cliente: {self.pratica.cliente.nome_completo()}\n"
            f"Pratica: {self.pratica.descrizione}\n"
            f"Netto: {self.importo_netto:.2f} EUR\n"
            f"IVA ({self.aliquota_iva}%): {self.importo_iva:.2f} EUR\n"
            f"Totale: {self.importo_totale:.2f} EUR"
        )

# Pipeline completa: Cliente -> Pratica -> Fattura
cliente = Cliente("Laura", "Verdi", "VRDLRA85M41H501Z")
pratica = Pratica("2024-001", cliente, "Contenzioso civile")
pratica.registra_ore(12)

fattura = Fattura(pratica)
print(fattura)
# Fattura FT-0001
# Cliente: Laura Verdi
# Pratica: Contenzioso civile
# Netto: 1800.00 EUR
# IVA (22%): 396.00 EUR
# Totale: 2196.00 EUR

Da ricordare

  • Una classe raggruppa dati (attributi) e comportamenti (metodi) in un unico oggetto
  • Usa l'ereditarietà quando due classi condividono una base comune, ma non esagerare con la profondità
  • I metodi speciali come __str__ e __repr__ rendono gli oggetti più facili da usare e debuggare
  • Preferisci la composizione (un oggetto contiene un altro) all'ereditarietà profonda
  • Il pattern Cliente -> Pratica -> Fattura mostra come le classi modellano relazioni reali tra entità