CodeGym /Corsi /JAVA 25 SELF /Semplificare sistemi complessi tramite astrazioni

Semplificare sistemi complessi tramite astrazioni

JAVA 25 SELF
Livello 19 , Lezione 4
Disponibile

1. Astrazione multilivello

Quando inizi a programmare, tutto sembra semplice: scrivi una classe, chiami un metodo, ottieni un risultato. Ma nei progetti reali tutto si complica: decine di classi, centinaia di metodi, migliaia di righe di codice... E se sul progetto lavora un team — la faccenda si complica ancora di più! Come non affogare in questo caos?

La risposta è — scomporre il complesso nel semplice, e ancora meglio — in livelli di astrazione.

Che cosa sono i livelli di astrazione?

Immaginali come i piani di un grande edificio: a ogni piano c’è la sua vita, ma tutti i piani sono collegati tra loro. In programmazione si distinguono tipicamente questi layer (livelli):

  • Interfaccia utente (UI) — ciò che vede l’utente.
  • Logica di business — le regole e i processi che realizzano l’essenza dell’applicazione.
  • Accesso ai dati (DAO, Repository) — il lavoro con database o file.

Ogni livello lavora con astrazioni senza conoscere i dettagli degli altri. Per esempio, alla logica di business non importa come sia implementata l’interfaccia utente o come siano memorizzati i dati — le importa che esistano metodi come saveOrder() o findUserById().

Un’analogia dalla vita reale

Immagina un ristorante. Gli ospiti (UI) fanno l’ordine tramite il cameriere (astrazione dell’interfaccia), lo chef (logica di business) prepara il piatto, mentre il magazziniere (accesso ai dati) controlla la disponibilità degli ingredienti in magazzino. Gli ospiti non sanno come esattamente lo chef prepari il piatto, e lo chef non si interessa a dove sia riposta la patata — l’importante è che sia a portata di mano.

2. Esempio: architettura multilivello nella pratica

Sviluppiamo il nostro progetto didattico — per esempio, un’app per la gestione delle attività (task manager). Sappiamo già creare classi per le attività; ora complichiamo il quadro e separiamo l’applicazione in layer.

Individuiamo le astrazioni

  • Task — descrizione astratta dell’attività: un’attività ha un titolo, uno stato, metodi per l’esecuzione.
  • TaskRepository — astrazione per la persistenza delle attività (non importa dove — in memoria, file, database).
  • TaskService — logica di business: aggiunta, ricerca, esecuzione delle attività.

Classi astratte e interfacce

// Livello della logica di business
public abstract class Task {
    private String title;
    private boolean completed;

    public Task(String title) {
        this.title = title;
        this.completed = false;
    }

    public abstract void complete();

    public String getTitle() { return title; }
    public boolean isCompleted() { return completed; }

    protected void setCompleted(boolean completed) { this.completed = completed; }
}

// Livello di persistenza dei dati (astrazione)
public interface TaskRepository {
    void save(Task task);
    Task findByTitle(String title);
    List<Task> findAll();
}

Implementazione dei layer

Implementazione di Task

public class WorkTask extends Task {
    private String deadline;

    public WorkTask(String title, String deadline) {
        super(title);
        this.deadline = deadline;
    }

    @Override
    public void complete() {
        setCompleted(true);
        System.out.println("Attività lavorativa '" + getTitle() + "' completata entro la scadenza " + deadline);
    }
}

Implementazione di TaskRepository

public class InMemoryTaskRepository implements TaskRepository {
    private List<Task> tasks = new ArrayList<>();

    @Override
    public void save(Task task) {
        tasks.add(task);
    }

    @Override
    public Task findByTitle(String title) {
        for (Task task : tasks) {
            if (task.getTitle().equals(title)) {
                return task;
            }
        }
        return null;
    }

    @Override
    public List<Task> findAll() {
        return new ArrayList<>(tasks);
    }
}

Implementazione di TaskService

public class TaskService {
    private TaskRepository repository;

    public TaskService(TaskRepository repository) {
        this.repository = repository;
    }

    public void addTask(Task task) {
        repository.save(task);
    }

    public void completeTask(String title) {
        Task task = repository.findByTitle(title);
        if (task != null) {
            task.complete();
        } else {
            System.out.println("Attività non trovata: " + title);
        }
    }

    public void showAllTasks() {
        for (Task task : repository.findAll()) {
            System.out.println(task.getTitle() + " — " + (task.isCompleted() ? "completata" : "non completata"));
        }
    }
}

Uso nella classe principale

public class Main {
    public static void main(String[] args) {
        TaskRepository repo = new InMemoryTaskRepository();
        TaskService service = new TaskService(repo);

        service.addTask(new WorkTask("Redigere il report", "2025-07-15"));
        service.addTask(new WorkTask("Preparare la presentazione", "2025-07-16"));

        service.showAllTasks();

        service.completeTask("Redigere il report");
        service.showAllTasks();
    }
}

Cosa abbiamo ottenuto?

  • La classe principale (Main) non sa com’è fatto lo storage delle attività — lavora con l’astrazione TaskRepository.
  • TaskService non sa quali tipi di attività esistano — lavora con la classe astratta Task.
  • Se domani vorremo memorizzare le attività in un database invece che in memoria — implementeremo semplicemente una nuova classe DatabaseTaskRepository, senza riscrivere la logica di business e l’UI.
  • Se comparirà un nuovo tipo di attività, per esempio HomeTask, — basterà aggiungere una nuova sottoclasse.

3. Vantaggi per il lavoro di squadra

Nei progetti di grandi dimensioni è raro che una sola persona scriva tutto. Di solito il team si divide in “frontend developer”, “backend developer”, “sviluppatori del data layer” ecc. In che modo le astrazioni li aiutano a non pestarsi i piedi?

Separazione delle responsabilità

Ognuno lavora sul proprio livello di astrazione.

  • Uno sviluppatore scrive l’implementazione di TaskRepository per lavorare con il database.
  • Un altro si occupa della logica di business (TaskService).
  • Un terzo realizza l’interfaccia utente.

Il contratto tra i layer è definito dalle astrazioni.

Finché tutti concordano sul fatto che in TaskRepository esistano i metodi save, findByTitle, findAll, i dettagli di implementazione non sono importanti.

Facilità di test e sostituzione dei componenti

  • È possibile sostituire facilmente un’implementazione con un’altra (per esempio, nei test usare InMemoryTaskRepository, e in produzione — lavorare con un database).
  • Il tester può sostituire il data layer con uno “stub” (mock), per testare la logica di business in isolamento.

Evoluzione indipendente

  • Se qualcuno decide di aggiungere un nuovo tipo di attività, non rompe il codice esistente — implementa semplicemente una nuova sottoclasse di Task.
  • Se compare un nuovo modo di memorizzare i dati, cambia solo l’implementazione dell’interfaccia, mentre il resto del codice rimane intatto.

4. Best practice: come non esagerare con le astrazioni

L’astrazione è come il sale in un piatto: senza, tutto è insipido, ma se esageri — rovini tutto. Ecco alcuni consigli:

Usa le astrazioni dove semplificano davvero il sistema.
Non ha senso creare una classe astratta solo per il gusto di farlo. Se hai un solo tipo di attività, forse l’astrazione non è necessaria.

Documenta classi e metodi astratti.
Una buona documentazione aiuta a capire che cosa debba implementare il discendente e perché.

Cerca di mantenere le astrazioni significative.
Una classe astratta dovrebbe esprimere un comportamento e/o uno stato realmente comuni.

Non mescolare le responsabilità.
Non aggiungere in una classe astratta metodi che servono solo a uno dei discendenti.

5. Astrazione nei grandi sistemi: un esempio reale

Vediamo come l’astrazione funzioni in un progetto davvero grande — per esempio, in un e‑commerce.

Layer del sistema

  • Controller (UI): ricevono le richieste dell’utente (ad esempio, “effettuare un ordine”).
  • Servizi (logica di business): verificano la disponibilità del prodotto, calcolano gli sconti, effettuano l’ordine.
  • Repository (accesso ai dati): salvano ordini, prodotti e utenti nel database.

Esempio di astrazioni

// Astrazione per il servizio di gestione degli ordini
public interface OrderService {
    void createOrder(Order order);
    Order findOrderById(String id);
}

// Astrazione per il repository degli ordini
public interface OrderRepository {
    void save(Order order);
    Order findById(String id);
}

Ogni layer conosce solo la propria astrazione. Se domani si decidesse di salvare gli ordini nel cloud — cambierebbe solo l’implementazione di OrderRepository.

Interazione tra i layer — schema

[UI/Controller] <--> [OrderService (astrazione)] <--> [OrderRepository (astrazione)] <--> [Database]
  • Ogni layer lavora con un’astrazione, senza conoscere i dettagli del layer sottostante.
  • Questo permette di sviluppare, testare e migliorare ogni layer in modo indipendente.

Astrazione e manutenzione del codice

  • Facile aggiungere nuove funzionalità (nuovi tipi di attività, pagamenti, trasporti).
  • Facile correggere i bug (corretto un bug in un punto — tutti i discendenti ricevono l’aggiornamento).
  • Facile testare (si possono sostituire i layer con “stub” per i test unitari).

6. Errori tipici nella progettazione delle astrazioni

Errore n. 1: Astrazione eccessiva. A volte si vorrebbe creare una classe astratta per ogni minima cosa. Ma se hai un solo tipo di entità, non farlo solo per moda — complica soltanto il codice.

Errore n. 2: Astrazione troppo vaga. Se la tua classe astratta descrive troppe cose e non ha un’area di responsabilità chiara, i suoi discendenti saranno costretti a implementare metodi inutili o a conservare campi “morti”.

Errore n. 3: Violazione del principio di responsabilità singola. Una classe astratta dovrebbe occuparsi di una sola area di comportamento. Non mescolare, per esempio, metodi per la persistenza e metodi di business logic nella stessa classe astratta.

Errore n. 4: Accoppiamento rigido tra i layer. Se il layer della logica di business dipende direttamente da una specifica implementazione dello storage (per esempio, usa new InMemoryTaskRepository() al suo interno), allora al cambio dello storage dovrai riscrivere tutto il codice. Usa astrazioni (interfacce, classi astratte) per allentare i legami.

Errore n. 5: Documentazione insufficiente. L’astrazione è un contratto e va descritta chiaramente. Se non scrivi che cosa debba fare il discendente, è facile ottenere errori imprevisti o “iniziative creative” dei colleghi.

1
Compito
JAVA 25 SELF, livello 19, lezione 4
Bloccato
Creiamo la base per un gestore di attività ✍️
Creiamo la base per un gestore di attività ✍️
1
Compito
JAVA 25 SELF, livello 19, lezione 4
Bloccato
Servizio intelligente per le attività 🧠
Servizio intelligente per le attività 🧠
1
Compito
JAVA 25 SELF, livello 19, lezione 4
Bloccato
Costruiamo un task manager modulare 🏗️
Costruiamo un task manager modulare 🏗️
1
Compito
JAVA 25 SELF, livello 19, lezione 4
Bloccato
Estendere il task manager per diversi tipi di attività 🚀
Estendere il task manager per diversi tipi di attività 🚀
1
Sondaggio/quiz
Classi astratte, livello 19, lezione 4
Non disponibile
Classi astratte
Astrazione e classi astratte
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION