Quando si utilizza il principio di responsabilità singola, cosa costituisce una "responsabilità?"


198

Sembra abbastanza chiaro che "Principio della singola responsabilità" non significa "fa solo una cosa". Ecco a cosa servono i metodi.

public Interface CustomerCRUD
{
    public void Create(Customer customer);
    public Customer Read(int CustomerID);
    public void Update(Customer customer);
    public void Delete(int CustomerID);
}

Bob Martin afferma che "le lezioni dovrebbero avere solo una ragione per cambiare". Ma è difficile pensarci se sei un programmatore nuovo di SOLID.

Ho scritto una risposta a un'altra domanda , in cui ho suggerito che le responsabilità sono come titoli di lavoro, e ho ballato sull'argomento usando una metafora del ristorante per illustrare il mio punto. Ma ciò non articola ancora un insieme di principi che qualcuno potrebbe usare per definire le responsabilità delle proprie classi.

Quindi come lo fai? Come si determinano le responsabilità che ogni classe dovrebbe avere e come si definisce una responsabilità nel contesto di SRP?


28
Pubblica su Code Review e fatti a pezzi :-D
Jörg W Mittag il

8
@ JörgWMittag Ehi, non spaventare la gente :)
Flambino,

118
Domande come questa da parte di membri veterani dimostrano che le regole e i principi che tentiamo di rispettare non sono affatto chiari o semplici . Sono [una specie di] contraddittorio e mistico ... come dovrebbe essere una buona serie di regole . E mi piacerebbe credere a domande come questa umili e sagge, e dare speranza a coloro che si sentono irrimediabilmente stupidi. Grazie Robert!
svidgen,

41
Mi chiedo se questa domanda sarebbe stata sottoposta a downgrade + contrassegnato duplicato immediatamente se fosse stata inviata da un noob :)
Andrejs

9
@rmunn: o in altre parole - il grande rappresentante attira ancora più rappresentante, perché nessuno ha cancellato i pregiudizi umani di base su stackexchange
Andrejs

Risposte:


117

Un modo per avvolgere la testa è immaginare potenziali cambiamenti nei progetti futuri e chiedersi cosa sarà necessario fare per realizzarli.

Per esempio:

Nuovi requisiti aziendali: gli utenti situati in California ottengono uno sconto speciale.

Esempio di cambiamento "buono": ho bisogno di modificare il codice in una classe che calcola gli sconti.

Esempio di modifiche errate: è necessario modificare il codice nella classe User e tale modifica avrà un effetto a cascata su altre classi che utilizzano la classe User, comprese le classi che non hanno nulla a che fare con gli sconti, ad esempio iscrizione, enumerazione e gestione.

O:

Nuovo requisito non funzionale: inizieremo a utilizzare Oracle anziché SQL Server

Esempio di buona modifica: è sufficiente modificare una singola classe nel livello di accesso ai dati che determina come mantenere i dati nei DTO.

Modifica errata: devo modificare tutte le mie classi di livello aziendale perché contengono una logica specifica di SQL Server.

L'idea è di ridurre al minimo l'impronta di potenziali cambiamenti futuri, limitando le modifiche del codice a un'area di codice per area di cambiamento.

Come minimo, le tue classi dovrebbero separare le preoccupazioni logiche dalle preoccupazioni fisiche. Una grande serie di esempi si possono trovare nel System.IOnamespace: non possiamo trovare un vari tipi di flussi fisici (ad esempio FileStream, MemoryStreamo NetworkStream) e vari lettori e scrittori ( BinaryWriter, TextWriter) che da lavoro a livello logico. Separando loro in questo modo, si evita di esplosione combinatoria: invece di aver bisogno FileStreamTextWriter, FileStreamBinaryWriter, NetworkStreamTextWriter, NetworkStreamBinaryWriter, MemoryStreamTextWriter, e MemoryStreamBinaryWriter, basta collegare lo scrittore e il torrente e si può avere ciò che si desidera. Quindi in seguito possiamo aggiungere, diciamo, an XmlWriter, senza bisogno di implementarlo nuovamente per memoria, file e rete separatamente.


34
Mentre sono d'accordo con il pensare al futuro, ci sono principi come YAGNI e metodologie come TDD thar suggeriscono il contrario.
Robert Harvey,

87
YAGNI ci dice di non costruire cose di cui non abbiamo bisogno oggi. Non dice di non costruire cose in modo estensibile. Vedi anche principio aperto / chiuso , che afferma che "le entità software (classi, moduli, funzioni, ecc.) Dovrebbero essere aperte per l'estensione, ma chiuse per modifica".
John Wu,

18
@JohnW: +1 solo per il tuo commento su YAGNI. Non riesco a credere a quanto devo spiegare alla gente che YAGNI non è una scusa per costruire un sistema rigido e inflessibile che non può reagire al cambiamento - ironicamente, l'opposto di ciò a cui aspirano SRP e le entità Open / Closed.
Greg Burghardt,

36
@JohnWu: Non sono d'accordo, YAGNI ci dice esattamente di non costruire cose di cui non abbiamo bisogno oggi. La leggibilità e i test, per esempio, sono qualcosa di cui un programma ha sempre bisogno "oggi", quindi YAGNI non è mai una scusa per non aggiungere struttura e punti di iniezione. Tuttavia, non appena "l'estensibilità" aggiunge costi significativi per i quali i benefici non sono evidenti "oggi", YAGNI intende evitare questo tipo di estensibilità, poiché quest'ultima porta a un eccesso di ingegnerizzazione.
Doc Brown,

9
@JohnWu Siamo passati da SQL 2008 al 2012. Sono state modificate in totale due query. E da SQL Auth a trusted? Perché sarebbe anche un cambio di codice; modifica della connessione La stringa nel file di configurazione è sufficiente. Ancora una volta, YAGNI. YAGNI e SRP sono a volte preoccupazioni in competizione, ed è necessario giudicare quale ha il miglior rapporto costi / benefici.
Andy,

76

In pratica, le responsabilità sono limitate da quelle cose che potrebbero cambiare. Pertanto, sfortunatamente non esiste un modo scientifico o di formula per arrivare a ciò che costituisce una responsabilità. È una chiamata di giudizio.

Riguarda ciò che, nella tua esperienza , rischia di cambiare.

Tendiamo ad applicare il linguaggio del principio in una rabbia iperbolica, letterale, zelante. Tendiamo a dividere le classi perché potrebbero cambiare, o lungo linee che semplicemente ci aiutano a risolvere i problemi. (Quest'ultima ragione non è intrinsecamente negativa.) Ma l'SRP non esiste per se stesso; è in servizio per la creazione di software gestibile.

Quindi, di nuovo, se le divisioni non sono guidate da probabili cambiamenti, non sono veramente in servizio per l'SRP 1 se YAGNI è più applicabile. Entrambi servono lo stesso obiettivo finale. Ed entrambi sono questioni di giudizio - si spera giudizio condito .

Quando lo zio Bob parla di questo, suggerisce che pensiamo alla "responsabilità" in termini di "chi sta chiedendo il cambiamento". In altre parole, non vogliamo che il Partito A perda il lavoro perché il Partito B ha chiesto un cambiamento.

Quando si scrive un modulo software, si desidera assicurarsi che, quando vengono richieste le modifiche, tali modifiche possono provenire solo da una singola persona, o meglio, da un singolo gruppo strettamente accoppiato di persone che rappresentano una singola funzione aziendale strettamente definita. Volete isolare i vostri moduli dalle complessità dell'organizzazione nel suo complesso e progettare i vostri sistemi in modo tale che ciascun modulo sia responsabile (risponde a) delle esigenze di quella sola funzione aziendale. ( Zio Bob - Il principio di responsabilità singola )

Gli sviluppatori bravi e con esperienza avranno un'idea dei probabili cambiamenti. E quell'elenco mentale varierà leggermente in base al settore e all'organizzazione.

Ciò che costituisce una responsabilità nella tua particolare applicazione, nella tua particolare organizzazione, è in definitiva una questione di giudizio stagionato . Riguarda ciò che probabilmente cambierà. E, in un certo senso, riguarda chi possiede la logica interna del modulo.


1. Per essere chiari, ciò non significa che siano cattive divisioni. Potrebbero essere grandi divisioni che migliorano notevolmente la leggibilità del codice. Significa solo che non sono guidati dall'SRP.


11
La migliore risposta, e in realtà cita i pensieri di zio Bob. Quanto a ciò che è probabile che cambi, tutti fanno un grosso problema con l'I / O, "e se cambiassimo il database?" o "cosa succede se passiamo da XML a JSON?" Penso che questo sia solitamente fuorviato. La vera domanda dovrebbe essere "cosa succede se abbiamo bisogno di cambiare questo int in un float, aggiungere un campo e cambiare questa stringa in un elenco di stringhe?"
user949300

2
Questo è barare. La singola responsabilità da sola è solo un modo proposto di "cambiare isolamento". Spiegare che è necessario isolare le modifiche per mantenere la responsabilità "singola", non suggerisce come farlo, ma solo spiegare l'origine del requisito.
Basilevs,

6
@Basilevs Sto cercando di affinare la mancanza che stai vedendo in questa risposta - per non parlare della risposta di zio Bob! Ma, forse, devo chiarire che SRP non ha lo scopo di garantire che "un cambiamento" avrà un impatto solo su 1 classe. Si tratta di garantire che ogni classe risponderà a "un solo cambiamento". ... Si tratta di provare a disegnare le frecce da ogni classe a un singolo proprietario. Non da ciascun proprietario a una singola classe.
svidgen,

2
Grazie per aver fornito una risposta pragmatica! Anche lo zio Bob mette in guardia dall'adesione zelante ai principi SOLIDI nell'architettura agile . Non ho la citazione a portata di mano, ma fondamentalmente dice che dividere le responsabilità aumenta intrinsecamente il livello di astrazione nel tuo codice e che tutta l'astrazione ha un costo, quindi assicurati che il vantaggio di seguire SRP (o altri principi) superi il costo di aggiungere più astrazione. (seguito dal prossimo commento)
Michael L.

4
Questo è il motivo per cui dovremmo mettere il prodotto di fronte al cliente il prima possibile e con la massima frequenza, in modo che forzino i cambiamenti nel nostro design e possiamo vedere quali aree possono cambiare in quel prodotto. Inoltre, avverte che non possiamo proteggerci da ogni tipo di cambiamento. Per qualsiasi applicazione, sarà difficile apportare alcuni tipi di modifiche. Dobbiamo assicurarci che questi siano i cambiamenti che hanno meno probabilità di verificarsi.
Michael L.

29

Seguo "le lezioni dovrebbero avere solo un motivo per cambiare".

Per me, questo significa pensare a schemi sfrenati che il mio product owner potrebbe escogitare ("Dobbiamo supportare il mobile!", "Dobbiamo andare sul cloud!", "Dobbiamo supportare il cinese!"). Una buona progettazione limiterà l'impatto di questi schemi su aree più piccole e le renderà relativamente facili da realizzare. Disegnare male significa andare su un sacco di codice e fare un sacco di cambiamenti rischiosi.

L'esperienza è l'unica cosa che ho trovato per valutare correttamente la probabilità di quegli schemi folli - perché renderne uno facile potrebbe renderne più difficili altri due - e valutare la bontà di un progetto. I programmatori esperti possono immaginare cosa dovrebbero fare per cambiare il codice, cosa c'è in giro per morderli nel culo e quali trucchi rendono le cose facili. I programmatori esperti hanno una buona impressione di quanto siano fregati quando il proprietario del prodotto chiede cose folli.

Praticamente, trovo che i test unitari aiutino qui. Se il tuo codice non è flessibile, sarà difficile testarlo. Se non puoi iniettare mock o altri dati di test, probabilmente non sarai in grado di iniettare quel SupportChinesecodice.

Un'altra metrica approssimativa è il passo dell'elevatore. Le piazzole tradizionali per ascensori sono "se fossi in un ascensore con un investitore, potresti venderlo su un'idea?". Le start-up devono avere descrizioni brevi e semplici di ciò che stanno facendo - qual è il loro obiettivo. Allo stesso modo, le classi (e le funzioni) dovrebbero avere una semplice descrizione di ciò che fanno . Non "questa classe implementa alcuni fubar in modo tale da poterlo utilizzare in questi scenari specifici". Qualcosa che puoi dire a un altro sviluppatore: "Questa classe crea utenti". Se non è possibile comunicare che ad altri sviluppatori, si sta andando per ottenere i bug.


A volte vai a implementare quello che pensavi sarebbe un cambiamento disordinato, e risulta semplice, o un piccolo refactor lo rende semplice e aggiunge funzionalità utili allo stesso tempo. Ma sì, di solito puoi vedere arrivare problemi.

16
Sono un grande sostenitore dell'idea "ascensore pitch". Se è difficile spiegare cosa fa una classe in una o due frasi, sei in un territorio rischioso.
Ivan

1
Si tocca un punto importante: la probabilità di quegli schemi folli varia drasticamente da un proprietario del progetto all'altro. Devi fare affidamento non solo sulla tua esperienza generale, ma anche su quanto conosci il proprietario del progetto. Ho lavorato per persone che volevano ridurre i nostri sprint a una settimana e ancora non riuscivo a evitare di cambiare direzione a metà sprint.
Kevin Krumwiede,

1
Oltre agli ovvi benefici, documentare il tuo codice usando "pitch dell'ascensore" ti aiuta anche a pensare a cosa sta facendo il tuo codice usando il linguaggio naturale che trovo utile per scoprire molteplici responsabilità.
Alexander,

1
@KevinKrumwiede Ecco a cosa servono le metodologie "Pollo che corre con la testa tagliata" e "Inseguimento dell'oca selvatica"!

26

Nessuno sa. O almeno, non siamo in grado di concordare una definizione. Questo è ciò che rende SPR (e altri principi SOLID) abbastanza controversi.

Direi che riuscire a capire cosa è o non è una responsabilità è una delle abilità che lo sviluppatore di software deve imparare nel corso della sua carriera. Più codice scrivi e recensisci, più esperienza dovrai determinare se qualcosa è responsabilità singola o multipla. O se una singola responsabilità viene fratturata in parti separate del codice.

Direi che lo scopo principale di SRP non è quello di essere una regola difficile. È per ricordarci di essere consapevoli della coesione nel codice e di fare sempre uno sforzo consapevole nel determinare quale codice sia coesivo e cosa no.


20
I nuovi programmatori hanno la tendenza a trattare il SOLID come se fosse un insieme di leggi, cosa che non è. È semplicemente un raggruppamento di buone idee per aiutare le persone a migliorare il design di classe. Purtroppo, le persone tendono a prendere troppo seriamente questi principi; Recentemente ho visto un annuncio di lavoro che citava SOLID come uno dei requisiti del lavoro.
Robert Harvey,

9
+42 per l'ultimo paragrafo. Come dice @RobertHarvey, cose come SPR, SOLID e YAGNI non dovrebbero essere prese come " regole assolute ", ma come principi generali di "buon consiglio". Tra loro (e altri) il consiglio a volte sarà contraddittorio, ma bilanciare quel consiglio (invece di seguire un rigido insieme di regole) (col tempo, man mano che la tua esperienza cresce) ti guiderà a produrre software migliore. Ci dovrebbe essere solo una "regola assoluta" nello sviluppo del software: " Non ci sono regole assolute ".
TripeHound,

Questo è un ottimo chiarimento su un aspetto di SRP. Ma, anche se i principi SOLIDI non sono regole rigide, non sono terribilmente preziosi se nessuno capisce cosa significano - ancor meno se la tua affermazione che "nessuno lo sa" è vera! ... ha senso per loro essere difficili da capire. Come per ogni abilità, c'è qualcosa che distingue il buono dal meno buono! Ma ... "nessuno lo sa" lo rende più un rituale di nonnismo. (E non credo che sia l'intento di SOLID!)
svidgen,

3
Con "Nessuno lo sa", spero che @Euphoric significhi semplicemente che non esiste una definizione precisa che funzioni per ogni caso d'uso. È qualcosa che richiede un certo grado di giudizio. Penso che uno dei modi migliori per determinare dove si trovano le tue responsabilità sia quello di iterare rapidamente e lasciare che il tuo codice ti dica . Cerca "odori" che il tuo codice non sia facilmente gestibile. Ad esempio, quando una modifica a una singola regola aziendale inizia ad avere effetti a cascata attraverso classi apparentemente non correlate, probabilmente si verifica una violazione di SRP.
Michael L.

1
In secondo luogo, @TripeHound e altri che hanno sottolineato che tutte queste "regole" non esistono per definire la vera religione dello sviluppo, ma per aumentare la probabilità di sviluppare software gestibile. Fai molta attenzione a seguire una "best practice" se non riesci a spiegare come promuove il software gestibile, migliora la qualità o aumenta l'efficienza dello sviluppo ..
Michael L.

5

Penso che il termine "responsabilità" sia utile come metafora perché ci consente di utilizzare il software per indagare su come è organizzato il software. In particolare, mi concentrerei su due principi:

  • La responsabilità è commisurata all'autorità.
  • Nessuna entità dovrebbe essere responsabile della stessa cosa.

Questi due principi ci consentono di distribuire la responsabilità in modo significativo perché si giocano a vicenda. Se stai abilitando un pezzo di codice per fare qualcosa per te, deve avere una responsabilità per quello che fa. Ciò comporta la responsabilità che una classe potrebbe dover crescere, espandendo la sua "ragione per cambiare" in ambiti sempre più ampi. Tuttavia, man mano che allarghi le cose, inizi naturalmente a imbatterti in situazioni in cui più entità sono responsabili della stessa cosa. Questo è pieno di problemi nella responsabilità della vita reale, quindi sicuramente è un problema anche nella programmazione. Di conseguenza, questo principio fa restringere gli ambiti, in quanto si suddivide la responsabilità in pacchi non duplicati.

Oltre a questi due, un terzo principio sembra ragionevole:

  • La responsabilità può essere delegata

Considera un programma appena coniato ... una lavagna vuota. All'inizio, hai solo un'entità, che è il programma nel suo insieme. È responsabile di ... tutto. Naturalmente ad un certo punto inizierai a delegare responsabilità a funzioni o classi. A questo punto, entrano in gioco le prime due regole che ti costringono a bilanciare tale responsabilità. Il programma di livello superiore è ancora responsabile dell'output complessivo, proprio come un manager è responsabile della produttività del proprio team, ma a ciascuna entità secondaria è stata delegata la responsabilità e con essa l'autorità di svolgere tale responsabilità.

Come ulteriore vantaggio, questo rende SOLID particolarmente compatibile con qualsiasi sviluppo software aziendale che potrebbe essere necessario fare. Ogni azienda del pianeta ha un'idea di come delegare la responsabilità e non sono tutti d'accordo. Se deleghi la responsabilità all'interno del tuo software in un modo che ricorda la delegazione della tua azienda, sarà molto più facile per i futuri sviluppatori arrivare rapidamente a come fai le cose in questa azienda.


Non sono sicuro al 100% che questo lo spieghi completamente. Ma penso che spiegare "responsabilità" in relazione a "autorità" sia un modo penetrante per esprimerla! (+1)
svidgen,

Pirsig disse: "Tendi a creare i tuoi problemi con la macchina", il che mi fa fermare.

@nocomprende Tendi anche a rafforzare i tuoi punti di forza nella macchina. Direi che quando i tuoi punti di forza e la tua debolezza sono le stesse cose, è allora che diventa interessante.
Cort Ammon,

5

In questa conferenza a Yale, lo zio Bob fa questo divertente esempio:

Inserisci qui la descrizione dell'immagine

Dice che Employeeha tre ragioni per cambiare, tre fonti di requisiti di cambiamento e dà questa spiegazione umoristica e ironica , ma comunque illustrativa:

  • Se il CalcPay()metodo presenta un errore e costa all'azienda milioni di dollari, il CFO ti licenzierà .

  • Se il ReportHours()metodo presenta un errore e costa all'azienda milioni di US $, il COO ti licenzierà .

  • Se il WriteEmmployee(metodo) presenta un errore che causa la cancellazione di molti dati e costa all'azienda milioni di dollari, il CTO ti licenzierà .

Quindi, avere tre diversi dirigenti di livello C che potenzialmente ti licenziano per errori costosi nella stessa classe significa che la classe ha troppe responsabilità.

Fornisce questa soluzione che risolve la violazione di SRP, ma deve comunque risolvere la violazione di DIP che non è mostrata nel video.

Inserisci qui la descrizione dell'immagine


Questo esempio sembra più una classe con responsabilità sbagliate .
Robert Harvey,

4
@RobertHarvey Quando una classe ha troppe responsabilità, significa che le responsabilità extra sono responsabilità sbagliate .
Tulains Córdova,

5
Ho sentito quello che stai dicendo, ma non lo trovo convincente. C'è una differenza tra una classe che ha troppe responsabilità e una classe che fa qualcosa che non ha niente da fare. Può sembrare lo stesso, ma non lo è; contare le arachidi non è lo stesso che chiamarle noci. È il principio di zio Bob e l'esempio di zio Bob, ma se fosse sufficientemente descrittivo, non avremmo affatto bisogno di questa domanda.
Robert Harvey,

@RobertHarvey, qual è la differenza? Queste situazioni mi sembrano isomorfe.
Paul Draper,

3

Penso che un modo migliore di suddividere le cose rispetto alle "ragioni per cambiare" sia iniziare pensando in base al fatto che avrebbe senso richiedere quel codice che deve compiere due (o più) azioni dovrebbe contenere un riferimento a un oggetto separato per ogni azione e se sarebbe utile avere un oggetto pubblico che potrebbe fare un'azione ma non l'altra.

Se le risposte a entrambe le domande sono sì, ciò suggerirebbe che le azioni dovrebbero essere eseguite da classi separate. Se le risposte a entrambe le domande sono no, ciò suggerirebbe che da un punto di vista pubblico ci dovrebbe essere una classe; se il codice per questo sarebbe ingombrante, potrebbe essere suddiviso internamente in classi private. Se la risposta alla prima domanda è no, ma la seconda è sì, dovrebbe esserci una classe separata per ogni azione più una classe composita che include riferimenti alle istanze delle altre.

Se si dispone di classi separate per la tastiera, un segnale acustico, la lettura numerica, la stampante di ricevute e il cassetto di cassa di un registratore di cassa e nessuna classe composita per un registratore di cassa completo, il codice che dovrebbe elaborare una transazione potrebbe finire per essere accidentalmente invocato in un modo che prende input dalla tastiera di una macchina, produce rumore dal cicalino di una seconda macchina, mostra i numeri sul display di una terza macchina, stampa una ricevuta sulla stampante di una quarta macchina e apre il cassetto della cassa di una quinta macchina. Ognuna di queste funzioni secondarie potrebbe essere utilmente gestita da una classe separata, ma dovrebbe esserci anche una classe composita che le unisce. La classe composita dovrebbe delegare quanta più logica possibile alle classi costituenti,

Si potrebbe dire che la "responsabilità" di ogni classe è o incorporare qualche logica reale oppure fornire un punto di attaccamento comune per più altre classi che lo fanno, ma ciò che è importante è concentrarsi innanzitutto sul modo in cui il codice client dovrebbe visualizzare una classe. Se ha senso per il codice client vedere qualcosa come un singolo oggetto, allora il codice client dovrebbe vederlo come un singolo oggetto.


Questo è un buon consiglio. Potrebbe valere la pena sottolineare che le responsabilità vengono suddivise in base a più criteri rispetto al solo SRP.
Jørgen Fogh,

1
Analogia automobilistica: non ho bisogno di sapere quanto gas c'è nel serbatoio di qualcun altro, o non voglio accendere i tergicristalli di qualcun altro. (ma questa è la definizione di Internet) (Shh! rovinerai la storia)

1
@nocomprende - "Non ho bisogno di sapere quanto gas c'è nel serbatoio di qualcun altro", a meno che tu non sia un adolescente che cerca di decidere quale delle auto della famiglia "prendere in prestito" per il tuo prossimo viaggio ...;)
alephzero

3

SRP è difficile da ottenere. Si tratta principalmente di assegnare "lavori" al codice e assicurarsi che ogni parte abbia responsabilità chiare. Come nella vita reale, in alcuni casi dividere il lavoro tra le persone può essere del tutto naturale, ma in altri casi può essere davvero complicato, soprattutto se non le conosci (o il lavoro).

Ti consiglio sempre di scrivere solo un semplice codice che funzioni per primo , quindi di riflettere un po ': dopo un po' tenderai a vedere come si raggruppa il codice in modo naturale. Penso che sia un errore forzare le responsabilità prima di conoscere il codice (o le persone) e il lavoro da svolgere.

Una cosa che noterai è quando il modulo inizia a fare troppo ed è difficile eseguire il debug / mantenere. Questo è il momento di refactoring; quale dovrebbe essere il lavoro principale e quali compiti potrebbero essere assegnati a un altro modulo? Ad esempio, dovrebbe gestire i controlli di sicurezza e gli altri lavori o eseguire prima i controlli di sicurezza altrove o questo renderà il codice più complesso?

Usa troppe indirette e diventa di nuovo un disastro ... come per altri principi, questo sarà in conflitto con altri, come KISS, YAGNI, ecc. Tutto è una questione di equilibrio.


SRP non è solo scritto sulla Costituzione di Costantino?
Nick Keighley,

Troverai naturalmente questi schemi se digiti abbastanza a lungo, ma puoi accelerare l'apprendimento nominandoli e aiuta con la comunicazione ...
Christophe Roussy

@NickKeighley Penso che sia coesione, non tanto scritto in grande, ma guardato da un'altra prospettiva.
sdenham,

3

"Principio di responsabilità unica" è forse un nome confuso. "Un solo motivo per cambiare" è una migliore descrizione del principio, ma è ancora facile fraintendere. Non stiamo parlando di dire cosa fa sì che gli oggetti cambino stato in fase di esecuzione. Stiamo prendendo in considerazione ciò che potrebbe causare agli sviluppatori di modificare il codice in futuro.

A meno che non stiamo risolvendo un bug, la modifica sarà dovuta a un requisito aziendale nuovo o modificato. Dovrai pensare al di fuori del codice stesso e immaginare quali fattori esterni potrebbero causare una modifica indipendente dei requisiti . Dire:

  • Le aliquote fiscali cambiano a causa di una decisione politica.
  • Il marketing decide di cambiare i nomi di tutti i prodotti
  • L'interfaccia utente deve essere riprogettata per essere accessibile
  • Il database è congestionato, quindi è necessario eseguire alcune ottimizzazioni
  • Devi ospitare un'app mobile
  • e così via...

Idealmente, si desidera che fattori indipendenti influenzino classi diverse. Ad esempio, poiché le aliquote fiscali cambiano indipendentemente dai nomi dei prodotti, le modifiche non dovrebbero interessare le stesse classi. Altrimenti si corre il rischio di un'introduzione di una modifica fiscale, un errore nella denominazione del prodotto, che è il tipo di accoppiamento stretto che si desidera evitare con un sistema modulare.

Quindi non limitarti a concentrarti su ciò che potrebbe cambiare: qualsiasi cosa potrebbe in teoria cambiare in futuro. Concentrati su ciò che potrebbe cambiare in modo indipendente . I cambiamenti sono in genere indipendenti se sono causati da attori diversi.

Il tuo esempio con i titoli di lavoro è sulla buona strada, ma dovresti prenderlo più letteralmente! Se il marketing potrebbe causare modifiche al codice e la finanza potrebbe causare altre modifiche, tali modifiche non dovrebbero influire sullo stesso codice, poiché si tratta di titoli di lavoro letteralmente diversi e quindi le modifiche avverranno in modo indipendente.

Per citare lo zio Bob che ha inventato il termine:

Quando si scrive un modulo software, si desidera assicurarsi che, quando vengono richieste le modifiche, tali modifiche possono provenire solo da una singola persona, o meglio, da un singolo gruppo strettamente accoppiato di persone che rappresentano una singola funzione aziendale strettamente definita. Volete isolare i vostri moduli dalle complessità dell'organizzazione nel suo complesso e progettare i vostri sistemi in modo tale che ciascun modulo sia responsabile (risponde a) delle esigenze di quella sola funzione aziendale.

Per riassumere: una "responsabilità" è quella di soddisfare una singola funzione aziendale. Se più di un attore potrebbe farti cambiare classe, probabilmente la classe infrange questo principio.


Secondo il suo libro "Clean Architecture" questo è esattamente giusto. Le regole aziendali dovrebbero provenire da un'unica fonte e una sola fonte. Ciò significa che HR, Operations e IT devono tutti cooperare nella formulazione dei requisiti in una "Responsabilità unica". E questo è il principio. +1
Benny Skogberg,

2

Un buon articolo che spiega i principi di programmazione SOLID e fornisce esempi di codice che segue e non segue questi principi è https://scotch.io/bar-talk/solid-the-first-five-principles-of-object-oriented- design .

Nell'esempio relativo a SRP fornisce un esempio di alcune classi di forma (cerchio e quadrato) e una classe progettata per calcolare l'area totale di più forme.

Nel suo primo esempio crea l'area che calcola la classe e fa restituire il suo output come HTML. Successivamente decide di voler visualizzarlo come JSON e deve cambiare la sua area calcolando la classe.

Il problema con questo esempio è che la sua classe di calcolo dell'area è responsabile del calcolo dell'area delle forme E della visualizzazione di quell'area. Quindi passa attraverso un modo migliore per farlo utilizzando un'altra classe progettata appositamente per visualizzare le aree.

Questo è un semplice esempio (e più facilmente comprensibile leggendo l'articolo in quanto ha frammenti di codice) ma dimostra l'idea di base di SRP.


0

Prima di tutto, quello che hai in realtà sono due problemi separati : il problema di quali metodi mettere nelle tue classi e il problema dell'interfaccia gonfia.

interfacce

Hai questa interfaccia:

public Interface CustomerCRUD
{
  public void Create(Customer customer);
  public Customer Read(int CustomerID);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

Presumibilmente, hai più classi conformi CustomerCRUDall'interfaccia (altrimenti un'interfaccia non è necessaria) e alcune funzioni do_crud(customer: CustomerCRUD)che accettano un oggetto conforme. Ma hai già rotto l'SRP: hai legato insieme queste quattro operazioni distinte.

Diciamo che in seguito dovrai operare su viste del database. Una vista del database ha solo il Readmetodo disponibile per essa. Ma vuoi scrivere una funzione do_query_stuff(customer: ???)che gestisca in modo trasparente su tabelle o viste complete; usa solo il Readmetodo, dopo tutto.

Quindi crea un'interfaccia

public Interface CustomerReader {Public Customer Read (customerID: int)}

e considera la tua CustomerCrudinterfaccia come:

public interface CustomerCRUD extends CustomerReader
{
  public void Create(Customer customer);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

Ma non c'è fine in vista. Potrebbero esserci oggetti che possiamo creare ma non aggiornare, ecc. Questa tana di coniglio è troppo profonda. L'unico modo sano di aderire al principio della responsabilità singola è quello di fare in modo che tutte le tue interfacce contengano esattamente un metodo . Go in realtà segue questa metodologia da quello che ho visto, con la stragrande maggioranza delle interfacce che contengono una singola funzione; se si desidera specificare un'interfaccia che contiene due funzioni, è necessario creare goffamente una nuova interfaccia che combina le due. Presto otterrai un'esplosione combinatoria di interfacce.

La via d'uscita da questo pasticcio è usare il sottotipo strutturale (implementato ad esempio in OCaml) invece delle interfacce (che sono una forma di sottotipo nominale). Non definiamo interfacce; invece, possiamo semplicemente scrivere una funzione

let do_customer_stuff customer = customer.read ... customer.update ...

che chiama qualunque metodo ci piaccia. OCaml utilizzerà l'inferenza del tipo per determinare che possiamo passare qualsiasi oggetto che implementa questi metodi. In questo esempio, sarebbe determinare che customerha tipo <read: int -> unit, update: int -> unit, ...>.

Classi

Questo risolve il disordine dell'interfaccia ; ma dobbiamo ancora implementare classi che contengono più metodi. Ad esempio, dovremmo creare due classi diverse CustomerReadere CustomerWriter? E se volessimo cambiare il modo in cui le tabelle vengono lette (ad es. Ora memorizziamo le nostre risposte in redis prima di recuperare i dati), ma ora come vengono scritte? Se segui questa catena di ragionamenti fino alla sua logica conclusione, sei indissolubilmente guidato alla programmazione funzionale :)


4
"Senza senso" è un po 'forte. Potrei andare dietro "mistico" o "Zen". Ma non completamente privo di significato!
svidgen,

Puoi spiegarci un po 'di più perché il sottotipo strutturale è una soluzione?
Robert Harvey,

@RobertHarvey Ristrutturato in modo significativo la mia risposta
gardenhead

4
Uso le interfacce anche quando ho una sola classe che la implementa. Perché? Derisione in unit test.
Eterno21

0

Nella mia mente, la cosa più vicina a un SRP che mi viene in mente è un flusso di utilizzo. Se non hai un flusso di utilizzo chiaro per una determinata classe, è probabile che la tua classe abbia un odore di design.

Un flusso di utilizzo sarebbe una data successione di chiamata del metodo che ti darebbe un risultato atteso (quindi verificabile). Fondamentalmente si definisce una classe con i casi d'uso che ha ottenuto IMHO, ecco perché tutta la metodologia del programma si concentra sulle interfacce piuttosto che sull'implementazione.


0

Per ottenere più modifiche ai requisiti, non è necessario modificare il componente .

Ma buona fortuna capendolo a prima vista, quando senti parlare per la prima volta di SOLID.


Vedo molti commenti che dicono che SRP e YAGNI possono contraddirsi a vicenda, ma YAGN I rinforzato da TDD (GOOS, London School) mi ha insegnato a pensare e progettare i miei componenti dal punto di vista del cliente. Ho iniziato a progettare le mie interfacce con il minimo che un client vorrebbe che facesse, ovvero quanto poco dovrebbe fare . E quell'esercizio può essere fatto senza alcuna conoscenza del TDD.

Mi piace la tecnica descritta da Zio Bob (non ricordo da dove, purtroppo), che va qualcosa del tipo:

Chiediti, cosa fa questa lezione?

La tua risposta contiene o di And o Or

In tal caso, estrai quella parte della risposta, è una sua responsabilità

Questa tecnica è assoluta, e come diceva @svidgen, SRP è un appello di giudizio, ma quando si impara qualcosa di nuovo, gli assoluti sono i migliori, è più facile fare sempre qualcosa. Assicurati che il motivo per cui non ti separi sia; una stima istruita e non perché non sai come farlo. Questa è l'arte e ci vuole esperienza.


Penso che molte delle risposte sembrano argomentare per il disaccoppiamento quando si parla di SRP .

SRP è non è per assicurarsi che un cambiamento non si propaga verso il basso il grafico delle dipendenze.

Teoricamente, senza SRP , non avresti dipendenze ...

Un cambiamento non dovrebbe causare un cambiamento in molti punti dell'applicazione, ma abbiamo altri principi per questo. SRP , tuttavia, migliora il principio aperto chiuso . Questo principio riguarda più l'astrazione, tuttavia, le astrazioni più piccole sono più facili da reimplementare .

Quindi, quando si insegna a SOLID nel suo insieme, fare attenzione a insegnare che SRP consente di modificare meno codice quando cambiano i requisiti, mentre in realtà consente di scrivere meno nuovo codice.


3
When learning something new, absolutes are the best, it is easier to just always do something.- Nella mia esperienza, i nuovi programmatori sono troppo dogmatici. L'assolutismo porta a sviluppatori non pensanti e alla programmazione cargo-cult. Dire "fai questo" va bene, purché tu capisca che la persona con cui stai parlando dovrà in seguito disimparare ciò che hai insegnato loro.
Robert Harvey,

@RobertHarvey, Completamente vero, crea un comportamento dogmatico e devi disimparare / riapprendere man mano che acquisisci esperienza. Questo è il mio punto però. Se un nuovo programmatore tenta di effettuare chiamate di giudizio senza alcun modo di ragionare sulla propria decisione, sembra al limite inutile, perché non sanno perché ha funzionato, quando ha funzionato. Facendo esagerare con le persone , insegna loro a cercare le eccezioni invece di fare ipotesi non qualificate. Tutto ciò che hai detto sull'assolutismo è corretto, motivo per cui dovrebbe essere solo un punto di partenza.
Chris Wohlert,

@RobertHarvey, Un rapido esempio di vita reale : potresti insegnare ai tuoi figli ad essere sempre onesti, ma man mano che invecchiano, probabilmente realizzeranno alcune eccezioni in cui le persone non vogliono sentire i loro pensieri più onesti. Aspettarsi che un bambino di 5 anni emetta un giudizio corretto sull'essere onesto è nella migliore delle ipotesi ottimista. :)
Chris Wohlert,

0

Non c'è una risposta chiara a questo. Sebbene la domanda sia ristretta, le spiegazioni no.

Per me, è qualcosa come Occam's Razor se vuoi. È un ideale in cui provo a misurare il mio codice attuale. È difficile inchiodarlo con parole semplici e chiare. Un'altra metafora sarebbe »un argomento« che è tanto astratto, cioè difficile da comprendere, quanto »una sola responsabilità«. Un terzo descripton verrebbe a "occuparsi di un livello di astrazione".

Cosa significa praticamente?

Ultimamente uso uno stile di codifica che consiste principalmente in due fasi:

La fase I è meglio descritta come caos creativo. In questa fase scrivo il codice mentre scorrono i pensieri, cioè crudo e brutto.

La fase II è l'esatto contrario. È come pulire dopo un uragano. Questo richiede più lavoro e disciplina. E poi guardo il codice dal punto di vista di un designer.

Sto lavorando principalmente in Python ora, il che mi permette di pensare a oggetti e classi in seguito. Prima fase I : scrivo solo funzioni e le diffondo quasi casualmente in diversi moduli. Nella fase II , dopo che le cose sono andate avanti, ho uno sguardo più da vicino a quale modulo si occupa di quale parte della soluzione. E mentre sfoglio i moduli, gli argomenti mi emergono. Alcune funzioni sono tematicamente correlate. Questi sono buoni candidati per le lezioni . E dopo che ho trasformato le funzioni in classi - il che è quasi finito con il rientro e l'aggiunta selfall'elenco dei parametri in Python;) - Uso SRPcome Occam's Razor per eliminare funzionalità ad altri moduli e classi.

Un esempio attuale potrebbe essere la scrittura di piccole funzionalità di esportazione l'altro giorno.

C'era la necessità di fogli CSV , Excel e Excel combinati in una zip.

La normale funzionalità è stata eseguita in tre viste (= funzioni). Ogni funzione utilizzava un metodo comune per determinare i filtri e un secondo metodo per recuperare i dati. Quindi in ciascuna funzione ha avuto luogo la preparazione dell'esportazione ed è stata consegnata come risposta dal server.

C'erano troppi livelli di astrazione confusi:

I) gestire la richiesta / risposta in entrata / in uscita

II) determinazione dei filtri

III) recupero dei dati

IV) trasformazione dei dati

Il semplice passo era usare un'astrazione ( exporter) per trattare gli strati II-IV in un primo passo.

L'unica cosa rimasta era l'argomento relativo alle richieste / risposte . Allo stesso livello di astrazione sta estraendo i parametri di richiesta, il che va bene. Quindi ho avuto per questo punto di vista una "responsabilità".

In secondo luogo, ho dovuto interrompere l'esportatore, che come abbiamo visto consisteva in almeno altri tre strati di astrazione.

La determinazione dei criteri di filtro e l'effettivo recupero sono quasi allo stesso livello di astrazione (i filtri sono necessari per ottenere il sottoinsieme corretto dei dati). Questi livelli sono stati inseriti in qualcosa di simile a un livello di accesso ai dati .

Nel passaggio successivo ho suddiviso gli attuali meccanismi di esportazione: laddove era necessaria la scrittura su un file temporale, l'ho suddivisa in due "responsabilità": una per la scrittura effettiva dei dati su disco e un'altra parte che si occupava del formato effettivo.

Lungo la formazione delle classi e dei moduli, le cose sono diventate più chiare, ciò che apparteneva a dove. E sempre la domanda latente, se la classe fa troppo .

Come si determinano le responsabilità che ogni classe dovrebbe avere e come si definisce una responsabilità nel contesto di SRP?

È difficile dare una ricetta da seguire. Ovviamente potrei ripetere il criptico "un livello di astrazione" - regola se questo aiuta.

Principalmente per me è una sorta di "intuizione artistica" che porta al design attuale; Modello il codice come un artista può scolpire l'argilla o dipinge.

Immaginami come un programmatore Bob Ross ;)


0

Cosa provo a fare per scrivere il codice che segue l'SRP:

  • Scegli un problema specifico che devi risolvere;
  • Scrivi il codice che lo risolve, scrivi tutto in un metodo (es: main);
  • Analizzare attentamente il codice e, in base all'attività, provare a definire le responsabilità che sono visibili in tutte le operazioni che vengono eseguite (questa è la parte soggettiva che dipende anche dall'azienda / progetto / cliente);
  • Si noti che tutte le funzionalità sono già implementate; la prossima è solo l'organizzazione del codice (d'ora in poi in questo approccio non saranno implementate funzionalità o meccanismi aggiuntivi);
  • In base alle responsabilità definite nei passaggi precedenti (definite in base all'azienda e all'idea "un motivo per cambiare"), estrarre una classe o un metodo separati per ognuno;
  • Si noti che questo approccio si preoccupa solo dell'SPR; idealmente ci dovrebbero essere ulteriori passaggi qui cercando di aderire anche agli altri principi.

Esempio:

Problema: ottenere due numeri dall'utente, calcolare la somma e generare il risultato per l'utente:

//first step: solve the problem right away
static void Main(string[] args)
{
    Console.WriteLine("Number 1: ");
    int firstNumber = Convert.ToInt32(Console.ReadLine());

    Console.WriteLine("Number 2: ");
    int secondNumber = Convert.ToInt32(Console.ReadLine());

    int result = firstNumber + secondNumber;

    Console.WriteLine("Hi there! The result is: {0}", result);

    Console.ReadLine();
}

Quindi, prova a definire le responsabilità in base alle attività che devono essere eseguite. Da questo, estrarre le classi appropriate:

//Responsible for getting two integers from the user
class Input {
    public int FirstNumber { get; set; }
    public int SecondNumber { get; set; }
    public void Read() {
        Console.WriteLine("Number 1: ");
        FirstNumber = Convert.ToInt32(Console.ReadLine());

        Console.WriteLine("Number 2: ");
        SecondNumber = Convert.ToInt32(Console.ReadLine());
    }
}

//Responsible for calculating the sum of two integers
class SumOperation {
    public int Result { get; set; }
    public void Calculate(int a, int b) {
        Result = a + b;
    }
}

//Responsible for the output of some value to the user
class Output {
    public void Write(int result) {
        Console.WriteLine("Hello! The result is: {0}", result);
    }
}

Quindi, il programma refactored diventa:

//Program: responsible for main execution.
//Gets two numbers from user and output their sum.
static void Main(string[] args)
{
    var input = new Input();
    input.Read();

    var operation = new SumOperation();
    operation.Calculate(input.FirstNumber, input.SecondNumber);

    var output = new Output();
    output.Write(operation.Result);

    Console.ReadLine();
}

Nota: questo esempio molto semplice prende in considerazione solo il principio SRP. L'uso degli altri principi (ad esempio: il codice "L" dovrebbe dipendere dalle astrazioni piuttosto che dalle concrezioni) fornirebbe maggiori vantaggi al codice e lo renderebbe più sostenibile per i cambiamenti aziendali.


1
Il tuo esempio è troppo semplice per illustrare adeguatamente SRP. Nessuno lo farebbe nella vita reale.
Robert Harvey,

Sì, in progetti reali scrivo alcuni pseudocodici anziché scrivere il codice esatto come nel mio esempio. Dopo lo pseudocodice, provo a dividere le responsabilità proprio come ho fatto nell'esempio. Ad ogni modo, è così che lo faccio.
Emerson Cardoso,

0

Dal libro di Robert C. Martins Clean Architecture: A Craftsman's Guide to Software Structure and Design , pubblicato il 10 settembre 2017, Robert scrive a pagina 62 quanto segue:

Storicamente, l'SRP è stato descritto in questo modo:

Un modulo dovrebbe avere una, e una sola, ragione per cambiare

I sistemi software sono cambiati per soddisfare utenti e stakeholder; quegli utenti e le parti interessate sono la "ragione per cambiare". di cui parla il principio. In effetti, possiamo riformulare il principio per dire questo:

Un modulo dovrebbe essere responsabile nei confronti di uno, e solo uno, utente o stakeholder

Sfortunatamente, la parola "utente" e "stakeholder" non sono proprio la parola giusta da usare qui. Probabilmente ci saranno più utenti o stakeholder che vogliono cambiare il sistema in modo sano. Invece ci riferiamo davvero a un gruppo: una o più persone che richiedono quel cambiamento. Ci riferiremo a quel gruppo come attore .

Pertanto la versione finale dell'SRP è:

Un modulo dovrebbe essere responsabile nei confronti di un solo attore.

Quindi non si tratta di codice. L'SRP riguarda il controllo del flusso di requisiti e delle esigenze aziendali, che può provenire solo da una sola soluzione.


Non sono sicuro del motivo per cui stai facendo la distinzione che "non si tratta di codice". Ovviamente si tratta di codice; questo è lo sviluppo del software.
Robert Harvey,

@RobertHarvey Il mio punto è che il flusso di requisiti proviene da una fonte, l'attore. Utenti e parti interessate non sono nel codice, ma nelle regole aziendali che ci vengono incontro come requisiti. Quindi l'SRP è un processo per controllare questi requisiti, che per me non è un codice. È lo sviluppo del software (!), Ma non il codice.
Benny Skogberg,
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.