Assicurarsi che ogni classe abbia una sola responsabilità, perché?


37

Secondo la documentazione Microsoft, l'articolo del principio SOLID di Wikipedia o la maggior parte degli architetti IT, dobbiamo garantire che ogni classe abbia una sola responsabilità. Vorrei sapere perché, perché se tutti sembrano essere d'accordo con questa regola nessuno sembra essere d'accordo sui motivi di questa regola.

Alcuni citano una migliore manutenzione, altri dicono che fornisce test facili o rende la classe più robusta o sicura. Cosa è corretto e cosa significa in realtà? Perché migliora la manutenzione, semplifica i test o rende il codice più robusto?



1
Avevo una domanda che potresti trovare anche correlata: qual è la vera responsabilità di una classe?
Pierre Arlaud,

3
Tutti i motivi della singola responsabilità non possono essere corretti? Semplifica la manutenzione. Semplifica i test. Rende la classe più robusta (in generale).
Martin York,

2
Per lo stesso motivo hai delle lezioni TUTTO.
Davor Ždralo

2
Ho sempre avuto un problema con questa affermazione. È molto difficile definire cosa sia una "singola responsabilità". Una singola responsabilità può variare da "verificare che 1 + 1 = 2" a "mantenere un registro accurato di tutto il denaro versato dentro e fuori dai conti bancari aziendali".
Dave Nay,

Risposte:


57

Modularità. Qualsiasi linguaggio decente ti darà i mezzi per incollare pezzi di codice, ma non esiste un modo generale per rimuovere un grosso pezzo di codice senza che il programmatore esegua un intervento chirurgico sulla fonte. Inserendo molte attività in un costrutto di codice, rubi a te stesso e agli altri l'opportunità di combinare i suoi pezzi in altri modi e introducete dipendenze non necessarie che potrebbero causare modifiche a un pezzo per influenzare gli altri.

L'SRP è applicabile tanto alle funzioni quanto alle classi, ma i linguaggi OOP tradizionali sono relativamente scarsi nell'incollare insieme le funzioni.


12
+1 per menzionare la programmazione funzionale come un modo più sicuro per comporre le astrazioni.
logc

> ma le lingue OOP tradizionali sono relativamente scarse nell'incollare le funzioni insieme. <Forse perché le lingue OOP non sono interessate alle funzioni, ma ai messaggi.
Jeff Hubbard,

@JeffHubbard Penso che tu intenda che è perché prendono dopo C / C ++. Non c'è nulla sugli oggetti che li rendono reciprocamente esclusivi con le funzioni. Inferno, un oggetto / interfaccia è solo un record / struttura di funzioni. Fingere che le funzioni non siano importanti è ingiustificato sia in teoria che in pratica. Non possono essere comunque evitati: alla fine devi lanciare "Runnables", "Fabbriche", "Fornitori" e "Azioni" o utilizzare i cosiddetti "modelli" di strategia e metodo dei modelli e finire con un mucchio di inutili scaldabagni per fare lo stesso lavoro.
Doval,

@Doval No, intendo davvero che OOP si occupa dei messaggi. Questo non vuol dire che un determinato linguaggio sia migliore o peggiore di un linguaggio di programmazione funzionale nell'incollare le funzioni insieme - solo che non è la preoccupazione principale del linguaggio .
Jeff Hubbard,

Fondamentalmente, se la tua lingua è di rendere OOP più facile / migliore / più veloce / più forte, allora se le funzioni incollano bene insieme non è ciò su cui ti concentri.
Jeff Hubbard,

30

Una migliore manutenzione, test facili, una correzione più rapida dei bug sono solo (molto piacevoli) risultati dell'applicazione di SRP. Il motivo principale (come dice Robert C. Matin) è:

Una classe dovrebbe avere una, e una sola, ragione per cambiare.

In altre parole, SRP aumenta la località di cambiamento .

SRP promuove anche il codice DRY. Finché avremo classi che hanno una sola responsabilità, potremmo scegliere di usarle dove vogliamo. Se abbiamo una classe che ha due responsabilità, ma ne abbiamo bisogno solo una e la seconda interferisce, abbiamo 2 opzioni:

  1. Copia e incolla la classe in un'altra e magari crea un altro mutante multi-responsabilità (aumenta un po 'il debito tecnico).
  2. Dividi la classe e rendila come dovrebbe essere, in primo luogo, il che può essere costoso a causa dell'uso estensivo della classe originale.

1
+1 Per collegare SRP a DRY.
Mahdi

21

È facile creare codice per risolvere un problema particolare. È più complicato creare codice che risolva quel problema, consentendo nel contempo di apportare modifiche successive in modo sicuro. SOLID fornisce una serie di pratiche che migliorano il codice.

Per quanto riguarda quello corretto: Tutti e tre. Sono tutti vantaggi dell'utilizzo della responsabilità singola e del motivo per cui è necessario utilizzarla.

Quanto a ciò che significano:

  • Una migliore manutenzione significa che è più facile da cambiare e non cambia così spesso. Perché c'è meno codice e quel codice è focalizzato su qualcosa di specifico, se devi cambiare qualcosa che non è correlato alla classe, la classe non ha bisogno di essere cambiata. Inoltre, quando è necessario modificare la classe, purché non sia necessario modificare l'interfaccia pubblica, è sufficiente preoccuparsi di quella classe e nient'altro.
  • Test facili significa meno test, meno installazione. Non ci sono tante parti mobili sulla classe, quindi il numero di possibili guasti sulla classe è più piccolo, e quindi meno casi che è necessario testare. Ci saranno meno membri / campi privati ​​da configurare.
  • A causa delle due sopra ottenete una classe che cambia di meno e fallisce di meno, e quindi è più robusta.

Prova a creare il codice per un po 'seguendo il principio e rivedi quel codice in seguito per apportare alcune modifiche. Vedrai l'enorme vantaggio che offre.

Fai lo stesso per ogni classe e finisci con più classi, tutte conformi a SRP. Rende la struttura più complessa dal punto di vista delle connessioni, ma la semplicità di ogni classe la giustifica.


Non credo che i punti qui esposti siano giustificati. In particolare, come evitare i giochi a somma zero in cui la semplificazione di una classe rende le altre classi più complicate, senza effetti netti sulla base di codice nel suo insieme? Credo che la risposta di @ Doval risolva questo problema.

2
Fai lo stesso per tutte le classi. Si termina con più classi, tutte conformi a SRP. Rende la struttura più complessa dal punto di vista delle connessioni. Ma la semplicità di ogni classe lo giustifica. Mi piace la risposta di @ Doval, però.
Miyamoto Akira

Penso che dovresti aggiungerlo alla tua risposta.

4

Ecco gli argomenti che, a mio avviso, supportano l'affermazione secondo cui il principio della responsabilità singola è una buona pratica. Fornisco anche collegamenti ad ulteriore letteratura, dove puoi leggere ragionamenti ancora più dettagliati - e più eloquenti dei miei:

  • Migliore manutenzione : idealmente, ogni volta che una funzionalità del sistema deve cambiare, ci sarà una e una sola classe che deve essere cambiata. Una chiara mappatura tra classi e responsabilità significa che qualsiasi sviluppatore coinvolto nel progetto può identificare di quale classe si tratta. (Come ha notato @MaciejChałapuk, vedi Robert C. Martin, "Codice pulito" .)

  • Test più semplici : idealmente, le classi dovrebbero avere un'interfaccia pubblica minima, e i test dovrebbero riguardare solo questa interfaccia pubblica. Se non riesci a testare con chiarezza, poiché molte parti della tua classe sono private, questo è un chiaro segno che la tua classe ha troppe responsabilità e che dovresti dividerla in sottoclassi più piccole. Si noti che ciò vale anche per le lingue in cui non vi sono membri della classe "pubblici" o "privati"; avere una piccola interfaccia pubblica significa che è molto chiaro al codice client quali parti della classe dovrebbe usare. (Vedi Kent Beck, "Test-Driven Development" per maggiori dettagli.)

  • Codice robusto : il tuo codice non fallirà più o meno spesso perché è ben scritto; ma, come tutto il codice, il suo obiettivo finale non è comunicare con la macchina , ma con altri sviluppatori (vedi Kent Beck, "Schemi di implementazione" , Capitolo 1.) È più facile ragionare su una base di codice chiara, quindi verranno introdotti meno bug e passerà meno tempo tra scoprire un bug e risolverlo.


Se qualcosa deve cambiare per motivi di lavoro, credimi con SRP, dovrai modificare più di una classe. Dire che se un cambio di classe è solo per un motivo non è lo stesso che se c'è un cambiamento avrà un impatto solo su una classe.
Bastien Vandamme,

@ B413: non intendevo dire che qualsiasi modifica implicherà una singola modifica in una singola classe. Molte parti del sistema potrebbero aver bisogno di modifiche per conformarsi a un singolo cambiamento aziendale. Ma forse hai ragione e avrei dovuto scrivere "ogni volta che una funzionalità del sistema deve cambiare".
logc,

3

Ci sono una serie di ragioni, ma quella che mi piace è l'approccio utilizzato da molti dei primi programmi UNIX: fare bene una cosa. È abbastanza difficile farlo con una cosa, e aumentando le difficoltà, più cose provi a fare.

Un altro motivo è limitare e controllare gli effetti collaterali. Ho adorato il mio apriporta combinato per caffettiera. Sfortunatamente, il caffè di solito traboccava quando avevo visitatori. Ho dimenticato di chiudere la porta dopo aver fatto il caffè l'altro giorno e qualcuno l'ha rubato.

Dal punto di vista psicologico, puoi solo tenere traccia di alcune cose alla volta. Le stime generali sono sette più o meno due. Se una classe fa più cose, devi tenerne traccia tutte in una volta. Ciò riduce la tua capacità di tracciare ciò che stai facendo. Se una classe fa tre cose e ne vuoi solo una, puoi esaurire la tua capacità di tenere traccia delle cose prima di fare qualsiasi cosa con la classe.

Fare più cose aumenta la complessità del codice. Oltre il codice più semplice, la crescente complessità aumenta la probabilità di bug. Da questo punto di vista vuoi che le classi siano il più semplici possibile.

Testare una classe che fa una cosa è molto più semplice. Non è necessario verificare che la seconda cosa che la classe ha fatto o meno per ogni test. Inoltre, non è necessario correggere le condizioni interrotte e ripetere il test quando uno di questi test ha esito negativo.


2

Perché il software è organico. I requisiti cambiano costantemente, quindi è necessario manipolare i componenti con il minor mal di testa possibile. Non seguendo i principi SOLID, si potrebbe finire con una base di codice che è impostata in concreto.

Immagina una casa con un muro di cemento portante. Cosa succede quando togli questo muro senza alcun supporto? La casa probabilmente crollerà. Nel software non lo vogliamo, quindi strutturiamo le applicazioni in modo tale da poter spostare / sostituire / modificare facilmente i componenti senza causare molti danni.


1

Seguo il pensiero: 1 Class = 1 Job.

Usando un'analogia di fisiologia: motore (sistema neurale), respirazione (polmoni), digestivo (stomaco), olfattivo (osservazione), ecc. Ognuno di questi avrà un sottoinsieme di controller, ma ognuno di essi ha solo 1 responsabilità, sia che sia da gestire il modo in cui ciascuno dei rispettivi sottosistemi funziona o se si tratta di un sottosistema endpoint e svolgono solo 1 attività, come sollevare un dito o far crescere un follicolo pilifero.

Non confondere il fatto che potrebbe agire come manager anziché come lavoratore. Alcuni lavoratori alla fine vengono promossi a Manager, quando il lavoro che stavano eseguendo è diventato troppo complicato per un processo da gestire da solo.

La parte più complicata di ciò che ho sperimentato è sapere quando designare una Classe come processo Operatore, Supervisore o Manager. Indipendentemente da ciò, dovrai osservare e denotare la sua funzionalità per 1 responsabilità (Operatore, Supervisore o Manager).

Quando una Classe / Oggetto svolge più di 1 di questi ruoli di tipo, scoprirai che il processo generale inizierà ad avere problemi di prestazioni o elaborerà il collo di bottiglia.


Anche se l'implementazione dell'elaborazione dell'ossigeno atmosferico in emoglobina dovesse essere gestita come una Lungclasse, direi che un'istanza Persondovrebbe ancora Breathe, e quindi la Personclasse dovrebbe contenere abbastanza logica per delegare almeno le responsabilità associate a quel metodo. Suggerirei inoltre che una foresta di entità interconnesse accessibili solo attraverso un proprietario comune è spesso più facile da ragionare rispetto a una foresta che ha più punti di accesso indipendenti.
supercat

Sì, in quel caso il Polmone è Manager, sebbene il Villi sarebbe un Supervisore, e il processo di Respirazione sarebbe la classe Operaio per trasferire le particelle di Aria nel flusso sanguigno.
GoldBishop

@GoldBishop: Forse la giusta opinione sarebbe quella di dire che una Personclasse ha come compito la delega di tutte le funzionalità associate a un'entità mostruosamente "complicata" del mondo reale-umano-reale, inclusa l'associazione delle sue varie parti . Avere un'entità che fa 500 cose è brutto, ma se è così che funziona il sistema del mondo reale modellato, avere una classe che delega 500 funzioni mantenendo un'identità può essere meglio che dover fare tutto con pezzi disgiunti.
supercat

@supercat Oppure potresti semplicemente compartimentare il tutto e avere 1 Classe con il Supervisore necessario sui sottoprocessi. In questo modo, si potrebbe (in teoria) avere ciascun processo separato dalla classe base Personma riportando comunque la catena su successo / fallimento ma non necessariamente influenzando l'altro. 500 funzioni (anche se impostate come esempio, sarebbero esagerate e non supportabili) cerco di mantenere quel tipo di funzionalità derivata e modulare.
GoldBishop

@GoldBishop: Vorrei che ci fosse un modo per dichiarare un tipo "effimero", in modo tale che il codice esterno potesse invocare membri su di esso o passarlo come parametro ai metodi propri del tipo, ma nient'altro. Dal punto di vista dell'organizzazione del codice, la suddivisione delle classi ha senso e anche avere un codice esterno invoca i metodi di un livello inferiore (ad esempio, Fred.vision.lookAt(George)ha senso, ma consentire al codice di dire someEyes = Fred.vision; someTubes = Fred.digestionsembra malvagio poiché oscura la relazione tra someEyese someTubes.
supercat

1

Soprattutto con un principio così importante come la responsabilità singola, mi aspetterei personalmente che ci siano molte ragioni per cui le persone adottano questo principio.

Alcuni di questi motivi potrebbero essere:

  • Manutenzione: SRP garantisce che la modifica della responsabilità in una classe non influisca su altre responsabilità, semplificando la manutenzione. Questo perché se ogni classe ha una sola responsabilità, le modifiche apportate a una responsabilità sono isolate dalle altre responsabilità.
  • Test - Se una classe ha una responsabilità, è molto più facile capire come testare questa responsabilità. Se una classe ha più responsabilità, devi assicurarti di testare quella corretta e che il test non sia influenzato da altre responsabilità della classe.

Si noti inoltre che SRP viene fornito con un pacchetto di principi SOLID. Rispettare SRP e ignorare il resto è altrettanto male che non fare SRP in primo luogo. Quindi non dovresti valutare SRP da solo, ma nel contesto di tutti i principi SOLID.


... test is not affected by other responsibilities the class has, potresti per favore approfondire questo?
Mahdi,

1

Il modo migliore per comprendere l'importanza di questi principi è di averne bisogno.

Quando ero un programmatore alle prime armi, non pensavo molto al design, infatti, non sapevo nemmeno che esistessero modelli di design. Man mano che i miei programmi crescevano, cambiare una cosa significava cambiare molte altre cose. È stato difficile rintracciare i bug, il codice era enorme e ripetitivo. Non c'era molto di una gerarchia di oggetti, le cose erano dappertutto. L'aggiunta di qualcosa di nuovo o la rimozione di qualcosa di vecchio porterebbe errori nelle altre parti del programma. Vai a capire.

Sui piccoli progetti, potrebbe non avere importanza, ma nei grandi progetti, le cose possono essere molto da incubo. Più tardi, quando mi sono imbattuto nei concetti di schemi di design, mi sono detto: "oh sì, fare questo avrebbe reso le cose molto più facili allora".

Non puoi davvero capire l'importanza dei modelli di progettazione fino a quando non sorge la necessità. Rispetto i modelli perché per esperienza posso dire che rendono la manutenzione del codice semplice e robusta.

Tuttavia, proprio come te, sono ancora incerto sul "test facile", perché non ho ancora avuto la necessità di entrare in test unitari.


I motivi sono in qualche modo collegati a SRP, ma SRP non è richiesto, quando si applicano motivi di disegno. È possibile utilizzare i pattern e ignorare completamente SRP. Utilizziamo i pattern per soddisfare i requisiti non funzionali, ma l'utilizzo dei pattern non è richiesto in nessun programma. I principi sono essenziali per rendere lo sviluppo meno doloroso e (IMO) dovrebbe essere un'abitudine.
Maciej Chałapuk,

Sono completamente d'accordo con te. SRP, in una certa misura, rafforza la progettazione e la gerarchia di oggetti puliti. È una buona mentalità per uno sviluppatore entrare, in quanto induce lo sviluppatore a iniziare a pensare in termini di oggetti e come dovrebbero essere. Personalmente, ha avuto un impatto davvero positivo anche sul mio sviluppo.
harsimranb,

1

La risposta è, come altri hanno sottolineato, tutti sono corretti e si alimentano a vicenda, più facile da testare rende più facile la manutenzione rende il codice più robusto rende la manutenzione più semplice e così via ...

Tutto ciò si riduce a un principio chiave: il codice dovrebbe essere piccolo e fare il minimo necessario per portare a termine il lavoro. Questo vale per un'applicazione, un file o una classe tanto quanto una funzione. Più cose fa un pezzo di codice, più difficile è capire, mantenere, estendere o testare.

Penso che si possa riassumere in una parola: portata. Presta molta attenzione all'ambito dei manufatti, minore è il campo di applicazione in un determinato punto in un'applicazione, meglio è.

Portata estesa = più complessità = più modi in cui le cose vanno male.


1

Se una classe è troppo grande, diventa difficile mantenere, testare e capire, altre risposte hanno coperto questa volontà.

È possibile che una classe abbia più di una responsabilità senza problemi, ma presto si verificano problemi con classi troppo complesse.

Tuttavia, avere una semplice regola di "una sola responsabilità" rende più semplice sapere quando è necessaria una nuova classe .

Tuttavia, definire la "responsabilità" è difficile, non significa "fare tutto quello che dice la specifica dell'applicazione", la vera abilità è sapere come scomporre il problema in piccole unità di "responsabilità".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.