Aikido

Perché le classi dovrebbero seguire il principio della responsabilità unica

Leggibilità

Regola
Classi dovrebbero avere una sola responsabilità.
Le classi che gestiscono più aspetti viola
il Principio Responsabilità .

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

Introduzione

Le classi che svolgono troppe funzioni diventano dei colli di bottiglia. Una classe che gestisce l'autenticazione, le e-mail e la convalida richiede modifiche ogni volta che uno di questi aspetti subisce un'evoluzione, con il rischio di compromettere funzionalità non correlate. Il testing richiede di simulare l'intera classe anche quando si testa un solo aspetto. Il principio della responsabilità unica stabilisce che una classe debba avere un solo motivo per cambiare.

Perché è importante

Manutenibilità del codice: le classi con responsabilità multiple vengono modificate più spesso, poiché l'evoluzione di qualsiasi aspetto influisce sull'intera classe.

Complessità dei test: per testare classi con più responsabilità è necessario simulare tutte le dipendenze, anche solo per testare una singola funzionalità.

Riutilizzabilità: non è possibile estrarre una singola responsabilità senza portare con sé tutte le dipendenze. Gli sviluppatori preferiscono duplicare il codice piuttosto che districare le classi con responsabilità multiple.

Coordinamento del team: quando più sviluppatori lavorano sulla stessa classe per implementare funzionalità diverse, si verificano frequenti conflitti di unione. Le classi a responsabilità singola consentono lo sviluppo 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 le operazioni sul database, l'invio delle e-mail, la registrazione degli eventi e la memorizzazione nella cache. Qualsiasi modifica ai modelli di e-mail, ai formati di registrazione degli eventi o alla strategia di memorizzazione nella cache richiede la modifica di questa classe. Testare la creazione di un utente comporta la simulazione dei servizi di posta elettronica, di analisi dei dati e della 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é è importante: Ogni classe ha una responsabilità ben definita: la persistenza dei dati, l'invio delle e-mail, la registrazione degli eventi o l'orchestrazione. Le modifiche ai modelli di e-mail influiscono solo su Servizio di notifica via e-mail. Per testare la creazione degli utenti è possibile utilizzare semplici stub per le dipendenze. Le classi possono essere riutilizzate in modo indipendente in diverse funzionalità.

Conclusione

Il principio della responsabilità unica non consiste nel rendere le classi il più piccole possibile, ma nel garantire che ogni classe abbia un unico motivo chiaro per essere modificata. Quando una classe inizia a gestire più aspetti, è opportuno rifattorizzarla estraendo ciascuna responsabilità in una classe a sé stante con un'interfaccia mirata. Ciò rende il codice più facile da testare, mantenere e far evolvere senza che le modifiche si ripercuotano a cascata su funzionalità non correlate.

Domande frequenti

Hai delle domande?

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

Cerca classi che presentino molteplici motivi di modifica. Se la modifica della logica delle e-mail, del formato di registrazione e dello schema del database richiede tutte di modificare la stessa classe, significa che questa ha troppe responsabilità. Controlla i nomi dei metodi: se nella stessa classe sono presenti verbi non correlati tra loro come sendEmail(), logEvent() e validateData(), è un segnale d’allarme. Le classi con più di 300-400 righe spesso indicano responsabilità multiple, anche se la dimensione da sola non è un criterio definitivo.

La suddivisione delle classi non comporta forse un aumento del numero di file e della complessità?

Un numero maggiore di file non equivale a una maggiore complessità. Dieci classi mirate da 50 righe ciascuna sono più facili da comprendere rispetto a un’unica classe da 500 righe che gestisce tutto. La chiave sta nel fatto che ogni classe sia semplice e abbia uno scopo chiaro. La navigazione negli IDE moderni rende irrilevante il numero di file. La riduzione della complessità deriva dalla possibilità di ragionare su ciascuna classe in modo indipendente, senza considerare aspetti non correlati.

E che dire delle classi che, per loro natura, devono coordinare più operazioni?

Il coordinamento è di per sé una responsabilità. Una classe UserService può orchestrare le chiamate a UserRepository, EmailService ed EventLogger senza implementare direttamente tali funzionalità. Si tratta del pattern “orchestrator” o “facade”. La differenza sta nel fatto che l’orchestrator delega a classi specializzate anziché implementare direttamente più funzionalità. Si tratta di un codice di collegamento leggero, non di logica di business.

In che modo questo principio si applica alle classi di utilità con metodi statici?

Le classi di utilità sono particolarmente soggette a violare il principio della responsabilità unica, poiché è facile continuare ad aggiungere metodi statici non correlati. Una classe StringUtils potrebbe nascere con metodi di formattazione, ma finire per includere anche funzioni di convalida, analisi sintattica, crittografia e codifica. È opportuno suddividerle in classi di utilità specializzate, come StringFormatter, StringValidator e StringEncoder. Ciascuna di esse presenta un insieme coeso di operazioni correlate.

Come posso rifattorizzare le classi esistenti che violano questo principio?

Inizia individuando le diverse responsabilità all’interno della classe. Estrai prima quella più semplice in una nuova classe, aggiorna i test e verifica che tutto funzioni. Ripeti il processo in modo iterativo, anziché tentare un rifattorizzazione su larga scala. Utilizza il pattern “strangler fig”: crea nuove classi a responsabilità singola e sposta gradualmente il codice dalla vecchia classe. Una volta che la vecchia classe è vuota o ridotta al minimo, rendila deprecata. Ogni passo dovrebbe rappresentare un incremento funzionante e testabile.

"Responsabilità unica" significa "metodo unico"?

No. Una classe può avere più metodi, purché siano tutti correlati alla stessa responsabilità. Una classe UserRepository potrebbe avere i metodi create(), update(), delete() e findById(), poiché tutti servono alla singola responsabilità della persistenza dei dati degli utenti. I metodi sono variazioni coesive dello stesso ambito, non ambiti 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.