Aikido

Perché le classi dovrebbero seguire il principio di responsabilità singola

Leggibilità

Regola
Le classi dovrebbero avere una responsabilità singola.
Le classi che gestiscono molteplici aspetti violano
il Principio di Responsabilità Singola.

Linguaggi supportati: JS, TS, PY, JAVA, C/C++,
C#, Swift/Objective C, Ruby. PHP, Kotlin, 
Scala, Rust, Haskell, Groovy, Dart. Julia,
Elixir, Clojure, OCaml, Delphi

Introduzione

Le classi che fanno troppo diventano dei colli di bottiglia. Una classe che gestisce autenticazione, email e validazione richiede modifiche ogni volta che evolve uno qualsiasi di questi aspetti, rischiando di compromettere funzionalità non correlate. I test richiedono il mocking dell'intera classe anche quando si testa un solo aspetto. Il Principio di Responsabilità Singola stabilisce che una classe dovrebbe avere un solo motivo per cambiare.

Perché è importante

Manutenibilità del codice: Le classi con molteplici responsabilità cambiano più spesso perché l'evoluzione di qualsiasi aspetto influisce sull'intera classe.

Complessità dei test: Testare classi con responsabilità multiple richiede il mocking di tutte le dipendenze, anche per testare una singola funzionalità.

Riutilizzabilità: Non è possibile estrarre una responsabilità senza portarsi dietro tutte le dipendenze. Gli sviluppatori duplicano il codice anziché districare classi con responsabilità multiple.

Coordinamento del team: Più sviluppatori che lavorano sulla stessa classe per funzionalità diverse creano frequenti conflitti di merge. Le classi a responsabilità singola consentono lo sviluppo in parallelo senza conflitti.

Esempi di codice

❌ Non conforme:

class UserManager {
    async createUser(userData) {
        const user = await db.users.insert(userData);
        await this.sendWelcomeEmail(user.email);
        await this.logEvent('user_created', user.id);
        await cache.set(`user:${user.id}`, user);
        return user;
    }

    async sendWelcomeEmail(email) {
        const template = this.loadEmailTemplate('welcome');
        await emailService.send(email, template);
    }

    async logEvent(event, userId) {
        await analytics.track(event, { userId, timestamp: Date.now() });
    }
}

Perché è sbagliato: Questa classe gestisce operazioni di database, invio di email, logging e caching. Modifiche ai template delle email, ai formati di log o alla strategia di cache richiedono la modifica di questa classe. Testare la creazione di un utente significa effettuare il mock dei servizi email, di analisi e di cache, rendendo i test lenti e fragili.

✅ Conforme:

class UserRepository {
    async create(userData) {
        return await db.users.insert(userData);
    }
}

class EmailNotificationService {
    async sendWelcomeEmail(email) {
        const template = await this.templateLoader.load('welcome');
        return await this.emailSender.send(email, template);
    }
}

class UserEventLogger {
    async logCreation(userId) {
        return await this.analytics.track('user_created', {
            userId,
            timestamp: Date.now()
        });
    }
}

class UserService {
    constructor(repository, emailService, eventLogger, cache) {
        this.repository = repository;
        this.emailService = emailService;
        this.eventLogger = eventLogger;
        this.cache = cache;
    }

    async createUser(userData) {
        const user = await this.repository.create(userData);
        await Promise.all([
            this.emailService.sendWelcomeEmail(user.email),
            this.eventLogger.logCreation(user.id),
            this.cache.set(`user:${user.id}`, user)
        ]);
        return user;
    }
}

Perché questo è importante: Ciascuna classe ha una sola responsabilità chiara: persistenza dei dati, invio di email, logging degli eventi o orchestrazione. Le modifiche ai template delle email influiscono solo su EmailNotificationService. Testare la creazione di utenti può utilizzare semplici stub per le dipendenze. Le classi possono essere riutilizzate in modo indipendente per diverse funzionalità.

Conclusione

Il Principio di Responsabilità Singola non consiste nel rendere le classi il più piccole possibile, ma nel garantire che ciascuna classe abbia un solo motivo chiaro per cambiare. Quando una classe inizia a gestire più aspetti, procedi alla rifattorizzazione estraendo ciascuna responsabilità nella propria classe con un'interfaccia mirata. Questo rende il codice più facile da testare, mantenere ed evolvere senza modifiche a cascata su funzionalità non correlate.

FAQ

Hai domande?

Come faccio a identificare quando una classe ha troppe responsabilità?

Cerca classi con molteplici motivi per cambiare. Se la modifica della logica delle email, del formato dei log e dello schema del database richiede la modifica della stessa classe, quest'ultima ha troppe responsabilità. Controlla i nomi dei metodi: se coprono verbi non correlati come sendEmail(), logEvent() e validateData() all'interno della stessa classe, questo è un campanello d'allarme. Le classi con più di 300-400 righe spesso indicano responsabilità multiple, sebbene le dimensioni da sole non siano definitive.

La suddivisione delle classi non crea più file e complessità?

Più file non significa maggiore complessità. Dieci classi mirate di 50 righe ciascuna sono più facili da comprendere rispetto a una classe di 500 righe che gestisce tutto. La chiave è che ogni classe sia semplice e abbia uno scopo chiaro. La navigazione nei moderni IDE rende irrilevante il numero di file. La riduzione della complessità deriva dalla capacità di ragionare su ciascuna classe in modo indipendente, senza dover considerare aspetti non correlati.

E per quanto riguarda le classi che hanno naturalmente bisogno di coordinare più operazioni?

Il coordinamento è di per sé una responsabilità. Una classe UserService può orchestrare le chiamate a UserRepository, EmailService e EventLogger senza implementare direttamente tali aspetti. Questo è il pattern orchestrator o facade. La differenza sta nel fatto che l'orchestratore delega a classi specializzate anziché implementare direttamente molteplici aspetti. Si tratta di codice di raccordo (glue code) leggero, non di logica di business.

Come si applica questo principio alle classi di utilità con metodi statici?

Le classi di utilità sono particolarmente soggette alla violazione del principio di responsabilità singola, poiché è facile continuare ad aggiungere metodi statici non correlati. Una classe StringUtils potrebbe iniziare con helper di formattazione per poi estendersi includendo validazione, parsing, crittografia e codifica. È opportuno suddividerle in classi di utilità mirate come StringFormatter, StringValidator e StringEncoder. Ognuna possiede un insieme coeso di operazioni correlate.

‍

Come posso effettuare il refactoring delle classi esistenti che violano questo principio?

Inizia identificando le singole responsabilità all'interno della classe. Estrai per prima quella più semplice in una nuova classe, aggiorna i test e verifica che tutto funzioni. Procedi in modo iterativo anziché tentare un refactoring massiccio. Usa il pattern strangler fig: crea nuove classi a responsabilità singola e sposta gradualmente il codice dalla vecchia classe. Una volta che la vecchia classe risulta vuota o ridotta al minimo, rimuovila (deprecandola). Ogni passaggio dovrebbe rappresentare un incremento funzionante e testabile.

‍

Il principio di responsabilità singola significa un solo metodo?

No. Una classe può avere più metodi purché siano tutti correlati alla stessa responsabilità. Una classe UserRepository potrebbe disporre dei metodi create(), update(), delete() e findById() perché servono tutti all'unico scopo della persistenza dei dati utente. I metodi sono variazioni coese della stessa logica, non aspetti distinti raggruppati insieme.

‍

Metti in sicurezza ora

Metti in sicurezza il tuo codice, il cloud e il runtime in un unico sistema centralizzato.
Trova e risolvi le vulnerabilità rapidamente e automaticamente.

Nessuna carta di credito richiesta | Risultati della scansione in 32 secondi.