Chiarire il principio della responsabilità unica


64

Il principio di responsabilità singola afferma che una classe dovrebbe fare una sola cosa. Alcuni casi sono piuttosto chiari. Altri, tuttavia, sono difficili perché ciò che assomiglia a "una cosa" se vista a un dato livello di astrazione può essere più cose se vista a un livello inferiore. Temo anche che se ai Principi inferiori viene rispettato il principio della responsabilità singola, si può ottenere un codice dei ravioli verboso eccessivamente disaccoppiato, in cui vengono spese più linee per creare minuscole classi per tutto e per idraulizzare le informazioni piuttosto che risolvere effettivamente il problema.

Come descriveresti cosa significa "una cosa"? Quali sono alcuni segni concreti che una classe fa davvero più di "una cosa"?


6
+1 per il "ravioli code" superiore. All'inizio della mia carriera ero una di quelle persone che la spingevano troppo in là. Non solo con le classi, ma anche con la modularizzazione dei metodi. Il mio codice è stato disseminato di tonnellate di piccoli metodi che hanno fatto qualcosa di semplice, solo per il gusto di spezzare un problema in piccoli pezzi che potevano adattarsi allo schermo senza scorrere. Ovviamente, questo andava spesso troppo lontano.
Tavoli Bobby il

Risposte:


50

Mi piace molto il modo in cui Robert C. Martin (zio Bob) riafferma il principio della responsabilità singola (collegato al PDF) :

Non ci dovrebbe mai essere più di un motivo per cambiare una classe

È leggermente diverso dalla tradizionale definizione "dovresti fare solo una cosa", e mi piace perché ti costringe a cambiare il modo in cui pensi alla tua classe. Invece di pensare a "sta facendo una cosa?", Pensi invece a cosa può cambiare e al modo in cui tali cambiamenti influenzano la tua classe. Ad esempio, se il database cambia, la tua classe deve cambiare? Che dire se il dispositivo di output cambia (ad esempio una schermata, un dispositivo mobile o una stampante)? Se la tua classe deve cambiare a causa di cambiamenti da molte altre direzioni, allora questo è un segno che la tua classe ha troppe responsabilità.

Nell'articolo collegato, lo zio Bob conclude:

L'SRP è uno dei principi più semplici e uno dei più difficili da ottenere. Le responsabilità congiunte sono qualcosa che facciamo naturalmente. Trovare e separare queste responsabilità l'una dall'altra è molto importante per la progettazione del software.


2
Mi piace il modo in cui lo afferma anche lui, sembra più facile da controllare quando è collegato al cambiamento piuttosto che alla "responsabilità" astratta.
Matthieu M.,

Questo è in realtà un modo eccellente per dirlo. Lo amo. Come nota, di solito tendo a pensare all'SRP come ad applicare molto più fortemente ai metodi. A volte una classe deve solo fare due cose (forse è la classe che collega due domini), ma un metodo non dovrebbe quasi mai fare più di quello che puoi descrivere in modo succinto con la sua firma del tipo.
CodexArcanum,

1
Ho appena mostrato questo al mio Grad - ottima lettura e un dannatamente buono promemoria per me stesso.
Martijn Verburg,

1
Ok, questo ha molto senso quando lo si combina con l'idea che il codice non troppo ingegnerizzato dovrebbe solo pianificare un cambiamento che è probabile nel prossimo futuro, non per ogni possibile cambiamento. Ribadirei questo, quindi, leggermente come "ci dovrebbe essere solo una ragione per cambiare una classe che probabilmente si verificherà nel prossimo futuro". Ciò incoraggia a favorire la semplicità in alcune parti del progetto che è improbabile che cambi e disaccoppi in parti che potrebbero cambiare.
dsimcha,

18

Continuo a chiedermi quale problema sta cercando di risolvere SRP? Quando mi aiuta SRP? Ecco cosa mi è venuto in mente:

È necessario refactoring di responsabilità / funzionalità fuori da una classe quando:

1) Hai funzionalità duplicata (DRY)

2) Scopri che il tuo codice ha bisogno di un altro livello di astrazione per aiutarti a capirlo (KISS)

3) Ritieni che gli esperti di dominio comprendano parti di funzionalità come parte di un componente diverso (Ubiquitous Language)

NON DOVREBBE rifattuare la responsabilità fuori da una classe quando:

1) Non ci sono funzionalità duplicate.

2) La funzionalità non ha senso al di fuori del contesto della tua classe. Detto in altro modo, la tua classe fornisce un contesto in cui è più facile capire la funzionalità.

3) I tuoi esperti di dominio non hanno un concetto di tale responsabilità.

Mi viene in mente che se l'SRP viene applicato in senso lato, scambiamo un tipo di complessità (cercando di creare la testa o la coda di una classe con troppe cose all'interno) con un altro tipo (cercando di mantenere tutti i collaboratori / livelli di astrazione diretta per capire cosa fanno effettivamente queste classi).

In caso di dubbio, lascialo fuori! Puoi sempre refactoring più tardi, quando c'è un caso chiaro per questo.

Cosa ne pensi?


Sebbene queste linee guida possano essere utili o meno, in realtà non hanno nulla a che fare con l'SRP come definito in SOLID, vero?
Sara

Grazie. Non riesco a credere a una parte della follia coinvolta nei cosiddetti principi SOLID, in cui rendono il codice molto semplice cento volte più complesso senza una buona ragione . I punti forniti descrivono le ragioni del mondo reale per implementare SRP. Penso che i principi che hai dato sopra dovrebbero diventare il loro acronimo, e gettare "SOLID", fa più male che bene. "Architecture Astronauts" in effetti, come hai sottolineato nel thread che mi conduce qui.
Nicholas Petersen,

4

Non so se ci sia una scala oggettiva, ma ciò che darebbe via sarebbe i metodi - non tanto il numero di essi, ma la varietà della loro funzione. Sono d'accordo che puoi prendere la decomposizione troppo lontano ma non seguirei regole rigide e veloci a riguardo.


4

La risposta sta nella definizione

Ciò che definisci la responsabilità di essere, alla fine ti dà il confine.

Esempio:

Ho un componente che ha la responsabilità di visualizzare le fatture -> in questo caso, se ho iniziato ad aggiungere qualcosa di più, sto infrangendo il principio.

Se d'altra parte, se avessi detto la responsabilità della gestione delle fatture -> l'aggiunta di più funzioni più piccole (ad es. Stampa di fatture , aggiornamento di fatture ) sono tutte all'interno di tale limite.

Tuttavia, se quel modulo avesse iniziato a gestire qualsiasi funzionalità al di fuori delle fatture , sarebbe al di fuori di tale limite.


Immagino che il problema principale qui sia la parola "gestione". È troppo generico per definire una singola responsabilità. Penso che sarebbe meglio avere un componente responsabile della fattura di stampa e un altro per la fattura di aggiornamento anziché un singolo componente che gestisce {stampa, aggiorna e - perché no? - visualizza, qualunque sia} fattura .
Machado,

1
L'OP chiede essenzialmente "Come si definisce una responsabilità?" quindi quando dici che la responsabilità è come la definisci, sembra che stia solo ripetendo la domanda.
Despertar,

2

Lo vedo sempre su due livelli:

  • Mi assicuro che i miei metodi facciano solo una cosa e lo facciano bene
  • Vedo una classe come un raggruppamento logico (OO) di quei metodi che rappresenta bene una cosa

Quindi qualcosa come un oggetto di dominio chiamato Dog:

Dogè la mia classe, ma i cani possono fare molte cose! Potrei avere metodi come walk(), run()e bite(DotNetDeveloper spawnOfBill)(scusa, non ho resistito; p).

Se Dogdiventa ingombrante, allora penserei a come gruppi di quei metodi possono essere modellati insieme in un'altra classe, come una Movementclasse, che potrebbe contenere il mio walk()e i run()metodi.

Non ci sono regole rigide e veloci, il tuo design OO si evolverà nel tempo. Cerco di scegliere un'interfaccia / API pubblica chiara e metodi semplici che facciano una cosa e una cosa bene.


Bite dovrebbe davvero prendere un'istanza di Object e un DotNetDeveloper dovrebbe essere una sottoclasse di Person (di solito, comunque!)
Alan Pearce,

@Alan - Ecco - risolto questo problema per te :-)
Martijn Verburg,

1

Lo guardo più lungo le linee di una classe dovrebbe rappresentare solo una cosa. Per appropriarsi dell'esempio di @ Karianna, ho la mia Dogclasse, che ha metodi per walk(), run()e bark(). Non ho intenzione di aggiungere metodi per meaow(), squeak(), slither()o fly()perché quelli non sono le cose che i cani fanno. Sono cose che fanno altri animali e quegli altri animali avrebbero le loro classi per rappresentarli.

(A proposito, se il vostro cane non volare, allora probabilmente si dovrebbe smettere di gettandolo fuori dalla finestra).


+1 per "se il tuo cane vola, probabilmente dovresti smettere di buttarlo fuori dalla finestra". :)
Tavoli Bobby il

Al di là della domanda su cosa dovrebbe rappresentare la classe , cosa rappresenta un'istanza ? Se uno considera SeesFooduna caratteristica di DogEyes, Barkcome qualcosa fatto da un DogVoice, e Eatcome qualcosa fatto da un DogMouth, allora if (dog.SeesFood) dog.Eat(); else dog.Bark();diventerà una logica come if (eyes.SeesFood) mouth.Eat(); else voice.Bark();, perdendo ogni senso di identità di cui occhi, bocca e voce sono tutti collegati a una singola entità.
supercat

@supercat è un punto giusto, sebbene il contesto sia importante. Se il codice che menzioni rientra nella Dogclasse, probabilmente è Dogcorrelato. Altrimenti, probabilmente finiresti con qualcosa di simile myDog.Eyes.SeesFoodpiuttosto che semplicemente eyes.SeesFood. Un'altra possibilità è che Dogespone l' ISeeinterfaccia che richiede la Dog.Eyesproprietà e il SeesFoodmetodo.
Giovanni

@JohnL: Se la meccanica reale del vedere è gestita dagli occhi di un cane, essenzialmente allo stesso modo di quella di un gatto o di una zebra, allora potrebbe avere senso avere la meccanica gestita da una Eyeclasse, ma un cane dovrebbe "vedere" usando i suoi occhi, piuttosto che semplicemente avere gli occhi che possono vedere. Un cane non è un occhio, ma non è semplicemente un porta-occhi. È una "cosa che [almeno può provare a] vedere" e dovrebbe essere descritta come tale tramite l'interfaccia. Anche a un cane cieco può essere chiesto se vede cibo; non sarà molto utile, poiché il cane dirà sempre "no", ma non c'è nulla di male nel chiedere.
supercat

Quindi useresti l'interfaccia ISee come descrivo nel mio commento.
JohnL

1

Una classe dovrebbe fare una cosa se vista al proprio livello di astrazione. Farà senza dubbio molte cose a un livello meno astratto. Ecco come funzionano le classi per rendere i programmi più gestibili: nascondono i dettagli di implementazione se non è necessario esaminarli da vicino.

Uso i nomi delle classi come test per questo. Se non riesco a dare a una classe un nome descrittivo abbastanza breve, o se quel nome contiene una parola come "E", probabilmente sto violando il principio di responsabilità singola.

Nella mia esperienza, è più facile mantenere questo principio ai livelli più bassi, dove le cose sono più concrete.


0

Si tratta di avere un ruolo unico .

Ogni classe dovrebbe essere ripresa da un nome di ruolo. Un ruolo è in effetti un (set di) verbo (i) associato a un contesto.

Per esempio :

Il file fornisce l'accesso a un file. FileManager gestisce gli oggetti File.

Dati di conservazione delle risorse per una risorsa da un file. ResourceManager conserva e fornisce tutte le risorse.

Qui puoi vedere che alcuni verbi come "gestisci" implicano una serie di altri verbi. I verbi da soli sono pensati meglio come funzioni rispetto alle classi, il più delle volte. Se il verbo implica troppe azioni che hanno il loro contesto comune, allora dovrebbe essere una classe in sé.

Quindi, l'idea è solo quella di farti avere un'idea semplice di cosa fa la classe definendo un ruolo unico, che potrebbe essere il raggruppamento di diversi sotto-ruoli (eseguiti da oggetti membri o altri oggetti).

Costruisco spesso classi Manager che contengono diverse altre classi. Come una fabbrica, un registro, ecc. Vedi una classe Manager come una specie di capo gruppo, un capo orchestra che guida altre persone a lavorare insieme per realizzare un'idea di alto livello. Ha un ruolo, ma implica lavorare con altri ruoli unici all'interno. Puoi anche vederlo come è organizzata un'azienda: un CEO non è produttivo sul puro livello di produttività, ma se non è lì, nulla può funzionare correttamente insieme. Questo è il suo ruolo.

Durante la progettazione, identificare ruoli univoci. E per ogni ruolo, vedi di nuovo se non può essere tagliato in molti altri ruoli. In questo modo, se devi semplicemente cambiare il modo in cui il tuo Manager costruisce oggetti, cambia semplicemente la Fabbrica e pensa con tranquillità.


-1

L'SRP non riguarda solo la suddivisione delle classi, ma anche la delega delle funzionalità.

Nell'esempio Dog usato sopra, non usare SRP come giustificazione per avere 3 classi separate come DogBarker, DogWalker, ecc. (Bassa coesione). Invece, guarda l'implementazione dei metodi di una classe e decidi se "sanno troppo". Puoi ancora avere dog.walk (), ma probabilmente il metodo walk () dovrebbe delegare a un'altra classe i dettagli di come si cammina.

In effetti stiamo permettendo alla classe Dog di avere un motivo per cambiare: perché i Dog cambiano. Ovviamente, combinando questo con altri principi SOLID, estenderesti Dog per nuove funzionalità piuttosto che cambiare Dog (aperto / chiuso). E inietteresti le tue dipendenze come IMove e IEat. E ovviamente creeresti queste interfacce separate (segregazione dell'interfaccia). Il cane cambierebbe solo se trovassimo un bug o se i cani cambiassero radicalmente (Liskov Sub, non estendere e quindi rimuovere il comportamento).

L'effetto netto di SOLID è che possiamo scrivere un nuovo codice più spesso di quanto dobbiamo modificare il codice esistente, e questa è una grande vittoria.


-1

Tutto dipende dalla definizione di responsabilità e dal modo in cui tale definizione influirà sul mantenimento del codice. Tutto si riduce a una cosa ed è così che il tuo design ti aiuterà a mantenere il tuo codice.

E come qualcuno ha detto "Camminare sull'acqua e progettare software da specifiche specifiche è facile, a condizione che entrambi siano congelati".

Quindi, se stiamo definendo la responsabilità in modo più concreto, non abbiamo bisogno di cambiarla.

A volte le responsabilità saranno palesemente ovvie, ma a volte potrebbe essere sottile e dobbiamo decidere giudiziosamente.

Supponiamo di aggiungere un'altra responsabilità alla classe Dog denominata catchThief (). Ora potrebbe portare a un'ulteriore diversa responsabilità. Domani, se il modo in cui il cane sta catturando un ladro deve essere cambiato dal dipartimento di polizia, allora la classe del cane dovrà essere cambiata. In questo caso sarebbe meglio creare un'altra sottoclasse e denominarla ThiefCathcerDog. Ma in una prospettiva diversa, se siamo sicuri che non cambierà in nessuna circostanza, o il modo in cui catchThief è stato implementato dipende da alcuni parametri esterni, allora è perfettamente ok avere questa responsabilità. Se le responsabilità non sono sorprendentemente strane, allora dobbiamo decidere giudiziosamente in base al caso d'uso.


-1

"Un motivo per cambiare" dipende da chi sta usando il sistema. Assicurati di utilizzare i casi per ciascun attore e fai un elenco delle modifiche più probabili, per ogni possibile modifica del caso d'uso assicurati che solo una classe sia interessata da quella modifica. Se stai aggiungendo un caso d'uso completamente nuovo, assicurati di dover solo estendere una classe per farlo.


1
Un motivo per cambiare non ha nulla a che fare con il numero di casi d'uso, attori o la probabilità di un cambiamento. Non dovresti fare un elenco di possibili modifiche. È corretto che una modifica abbia un impatto solo su una classe. Essere in grado di estendere una classe per far fronte a quel cambiamento è buono, ma questo è il principio aperto chiuso, non SRP.
candied_orange

Perché non dovremmo fare un elenco delle modifiche più probabili? Design speculativo?
kiwicomb123,

perché non sarà d'aiuto, non sarà completo e ci sono modi più efficaci per gestire il cambiamento che tentare di prevederlo. Aspettalo e basta. Isolare le decisioni e l'impatto sarà minimo.
candied_orange,

Va bene, ho capito, viola il principio "non ne hai bisogno".
kiwicomb123,

In realtà anche Yagni può essere spinto troppo lontano. Ha davvero lo scopo di impedirti di implementare casi d'uso speculativi. Non impedirti di isolare un caso d'uso implementato.
candied_orange,
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.