Il principio di responsabilità singola può / dovrebbe essere applicato al nuovo codice?


20

Il principio è definito come moduli che hanno un motivo per cambiare . La mia domanda è, sicuramente questi motivi per cambiare non sono noti fino a quando il codice non inizia effettivamente a cambiare ?? Praticamente ogni pezzo di codice ha numerose ragioni per cui potrebbe cambiare, ma sicuramente tentare di anticipare tutto questo e progettare il tuo codice con questo in mente finirebbe con un codice molto scarso. Non è una buona idea iniziare davvero ad applicare SRP solo quando iniziano le richieste di modifica del codice? Più specificamente, quando un pezzo di codice è cambiato più di una volta per più di una ragione, dimostrando così che ha più di una ragione per cambiare. Sembra molto anti-Agile tentare di indovinare le ragioni del cambiamento.

Un esempio potrebbe essere un pezzo di codice che stampa un documento. Una richiesta arriva per cambiarlo per stampare in PDF e quindi viene fatta una seconda richiesta per cambiarlo per applicare una formattazione diversa al documento. A questo punto hai la prova di più di un singolo motivo per cambiare (e violazione di SRP) e dovresti effettuare il refactoring appropriato.


6
@Frank - In realtà è comunemente definito così - vedi ad esempio en.wikipedia.org/wiki/Single_responsibility_principle
Joris Timmermans

1
Il modo in cui lo stai formulando non è il modo in cui capisco la definizione di SRP.
Pieter B,

2
Ogni riga di codice ha (almeno) due ragioni per essere cambiata: contribuisce a un bug o interferisce con un nuovo requisito.
Bart van Ingen Schenau,

1
@BartvanIngenSchenau: LOL ;-) Se lo vedi in questo modo, l'SRP non può essere applicato da nessuna parte.
Doc Brown,

1
@DocBrown: è possibile se non si accoppia SRP con la modifica del codice sorgente. Ad esempio, se si interpreta SRP come essere in grado di fornire un resoconto completo di ciò che una classe / funzione fa in una frase senza usare la parola e (e nessuna formulazione di donnola per aggirare quella restrizione).
Bart van Ingen Schenau,

Risposte:


27

Naturalmente, il principio YAGNI ti dirà di applicare SRP non prima di averne davvero bisogno. Ma la domanda che dovresti porti è: devo prima applicare SRP e solo quando devo davvero cambiare il mio codice?

Per la mia esperienza, l'applicazione di SRP ti dà un vantaggio molto prima: quando devi scoprire dove e come applicare una modifica specifica nel tuo codice. Per questa attività, devi leggere e comprendere le tue funzioni e classi esistenti. Questo diventa molto più semplice quando tutte le tue funzioni e classi hanno una responsabilità specifica. Quindi IMHO dovresti applicare SRP ogni volta che rende il tuo codice più facile da leggere, ogni volta che rende le tue funzioni più piccole e più auto-descrittive. Quindi la risposta è , ha senso applicare SRP anche per il nuovo codice.

Ad esempio, quando il codice di stampa legge un documento, formatta il documento e stampa il risultato su un dispositivo specifico, si tratta di 3 responsabilità chiaramente separabili. Quindi crea almeno 3 funzioni, assegnale secondo i nomi. Per esempio:

 void RunPrintWorkflow()
 {
     var document = ReadDocument();
     var formattedDocument = FormatDocument(document);
     PrintDocumentToScreen(formattedDocument);
 }

Ora, quando ricevi un nuovo requisito per cambiare la formattazione del documento o un altro per stampare in PDF, sai esattamente a quale di queste funzioni o posizioni nel codice devi applicare le modifiche, e ancora più importante, dove no.

Quindi, ogni volta che si arriva a una funzione non si capisce perché la funzione fa "troppo" e non si è sicuri se e dove applicare una modifica, quindi considerare di refactificare la funzione in funzioni separate e più piccole. Non aspettare fino a quando non devi cambiare qualcosa. Il codice è 10 volte più letto che modificato e le funzioni più piccole sono molto più facili da leggere. Secondo la mia esperienza, quando una funzione ha una certa complessità, puoi sempre dividere la funzione in diverse responsabilità, indipendentemente dal sapere quali cambiamenti arriveranno in futuro. Bob Martin di solito fa un passo avanti, vedi il link che ho dato nei miei commenti qui sotto.

MODIFICA: al tuo commento: la principale responsabilità della funzione esterna nell'esempio sopra non è quella di stampare su un dispositivo specifico o di formattare il documento, ma di integrare il flusso di lavoro di stampa . Pertanto, a livello di astrazione della funzione esterna, un nuovo requisito come "i documenti non devono più essere formattati" o "i documenti devono essere inviati anziché stampati" è solo "lo stesso motivo" - vale a dire "il flusso di lavoro di stampa è cambiato". Se parliamo di cose del genere, è importante attenersi al giusto livello di astrazione .


Generalmente sviluppo sempre con TDD, quindi nel mio esempio non sarei stato fisicamente in grado di mantenere tutta quella logica in un modulo perché sarebbe impossibile testarlo. Questo è solo un sottoprodotto di TDD e non perché sto deliberatamente applicando SRP. Il mio esempio aveva responsabilità abbastanza chiare e separate, quindi forse non è un buon esempio. Penso che ciò che sto chiedendo sia: puoi scrivere qualche nuovo pezzo di codice e dire inequivocabilmente, sì, ciò non viola SRP? I "motivi per cambiare" non sono essenzialmente definiti dall'azienda?
NoWeevil,

3
@thecapsaicinkid: sì, è possibile (almeno mediante refactoring immediato). Ma otterrai funzioni molto, molto piccole - e non piace a tutti i programmatori. Vedi questo esempio: sites.google.com/site/unclebobconsultingllc/…
Doc Brown,

Se steste applicando SRP anticipando i motivi per cambiare, nel vostro esempio potrei ancora sostenere che ha più di un singolo cambiamento di motivo. L'azienda potrebbe decidere di non voler più formattare un documento e successivamente decidere di volerlo inviare per e-mail anziché stamparlo. EDIT: basta leggere il link e, sebbene il risultato finale non mi piaccia particolarmente, "Estrai finché non riesci più a estrarre" ha molto più senso ed è meno ambiguo di "solo un motivo per cambiare". Non molto pragmatico però.
NoWeevil,

1
@thecapsaicinkid: vedi la mia modifica. La responsabilità principale della funzione esterna non è stampare su un dispositivo specifico o formattare il documento, ma è integrare il flusso di lavoro di stampa. E quando questo flusso di lavoro cambia, questa è l'unica ragione per cui cambierà la funzione
Doc Brown,

Il tuo commento sull'adesione al giusto livello di astrazione sembra essere quello che mi mancava. Ad esempio, ho una classe che descriverei come "Crea strutture di dati da un array JSON". Sembra una sola responsabilità per me. Passa attraverso gli oggetti in un array JSON e li mappa in POJO. Se mi attengo allo stesso livello di astrazione della mia descrizione, è difficile sostenere che abbia più di un motivo per cambiare, ad esempio "Come JSON si associa all'oggetto". Essendo meno astratto potrei obiettare che ha più di un motivo ad esempio, come ho Mappa campi data cambia, come i valori numerici sono mappati giorni, ecc
SeeNoWeevil

7

Penso che tu abbia frainteso SRP.

L'unico motivo della modifica NON riguarda la modifica del codice, ma ciò che fa il codice.


3

Penso che la definizione di SRP come "avere un motivo per cambiare" sia fuorviante proprio per questo motivo. Prendilo esattamente al valore nominale: il principio di responsabilità singola afferma che una classe o una funzione dovrebbe avere esattamente una responsabilità. Avere solo una ragione per cambiare è un effetto collaterale di fare solo una cosa per cominciare. Non c'è motivo per cui non puoi almeno fare uno sforzo verso la singola responsabilità nel tuo codice senza sapere nulla su come potrebbe cambiare in futuro.

Uno dei migliori indizi per questo genere di cose è quando scegli i nomi di classe o funzione. Se non è immediatamente chiaro come si dovrebbe chiamare la classe, o il nome è particolarmente lungo / complesso, o il nome usa termini generici come "manager" o "utility", allora probabilmente sta violando SRP. Allo stesso modo, quando si documenta l'API, dovrebbe diventare rapidamente evidente se si sta violando SRP in base alla funzionalità che si sta descrivendo.

Ci sono, naturalmente, sfumature di SRP che non si possono conoscere fino a dopo nel progetto - quella che sembrava una singola responsabilità si è rivelata essere due o tre. Questi sono casi in cui dovrai refactoring per implementare SRP. Ciò non significa che SRP debba essere ignorato fino a quando non si ottiene una richiesta di modifica; che sconfigge lo scopo di SRP!

Per parlare direttamente al tuo esempio, considera di documentare il tuo metodo di stampa. Se diresti "questo metodo formatta i dati per la stampa e li invia alla stampante", questo ed è quello che ti prende: non è una singola responsabilità, sono due responsabilità: formattazione e invio alla stampante. Se lo riconosci e lo dividi in due funzioni / classi, allora quando arrivano le tue richieste di modifica, avresti già un solo motivo per cambiare ogni sezione.


3

Un esempio potrebbe essere un pezzo di codice che stampa un documento. Una richiesta arriva per cambiarlo per stampare in PDF e quindi viene fatta una seconda richiesta per cambiarlo per applicare una formattazione diversa al documento. A questo punto hai la prova di più di un singolo motivo per cambiare (e violazione di SRP) e dovresti effettuare il refactoring appropriato.

Mi sono sparato così tante volte ai piedi trascorrendo troppo tempo ad adattare il codice per far fronte a quei cambiamenti. Invece di stampare semplicemente quel dannato stupido PDF.

Refactor per ridurre il codice

Il modello monouso può creare un eccesso di codice. Dove i pacchetti sono inquinati con piccole classi specifiche che creano un mucchio di codice immondizia che non ha senso individualmente. Devi aprire dozzine di file di origine solo per capire come arriva alla parte di stampa. Inoltre, ci possono essere centinaia se non migliaia di righe di codice che sono in atto solo per eseguire 10 righe di codice che eseguono la stampa effettiva.

Crea un Bullseye

Il modello monouso aveva lo scopo di ridurre il codice sorgente e migliorare il riutilizzo del codice. Doveva creare specializzazione e implementazioni specifiche. Una sorta di bullseyenel codice sorgente per tego to specific tasks . Quando si è verificato un problema con la stampa, sapevi esattamente dove andare per risolverlo.

L'uso singolo non significa fratture ambigue

Sì, hai un codice che stampa già un documento. Sì, ora è necessario modificare il codice per stampare anche PDF. Sì, ora è necessario modificare la formattazione del documento.

Sei sicuro che usagesia cambiato in modo significativo?

Se il refactoring provoca eccessive generalizzazioni di sezioni del codice sorgente. Al punto che l'intento originale di printing stuffnon è più esplicito, allora hai creato una frattura ambigua nel codice sorgente.

Il nuovo ragazzo sarà in grado di capirlo rapidamente?

Mantenere sempre il codice sorgente nell'organizzazione più semplice da comprendere.

Non essere un produttore di orologi

Troppe volte ho visto gli sviluppatori indossare un oculare e concentrarmi sui piccoli dettagli al punto che nessun altro potrebbe rimettere insieme i pezzi se dovesse cadere a pezzi.

inserisci qui la descrizione dell'immagine


2

Un motivo per cambiare è, in definitiva, un cambiamento nelle specifiche o nelle informazioni sull'ambiente in cui l'applicazione viene eseguita. Quindi un unico principio di responsabilità ti dice di scrivere ogni componente (classe, funzione, modulo, servizio ...) in modo che debba considerare il meno possibile delle specifiche e dell'ambiente di esecuzione.

Dato che conosci le specifiche e l'ambiente quando scrivi il componente, puoi applicare il principio.

Se si considera l'esempio di codice che stampa un documento. Dovresti considerare se puoi definire il modello di layout senza considerare che il documento finirà in PDF. Puoi, così SRP ti sta dicendo che dovresti.

Naturalmente YAGNI ti sta dicendo che non dovresti. È necessario trovare un equilibrio tra i principi di progettazione.


2

Flup è diretto nella giusta direzione. Il "principio della responsabilità unica" si applicava originariamente alle procedure. Ad esempio, Dennis Ritchie direbbe che una funzione dovrebbe fare una cosa e farlo bene. Quindi, in C ++, Bjarne Stroustrup direbbe che una classe dovrebbe fare una cosa e farlo bene.

Si noti che, ad eccezione delle regole empiriche, questi due hanno formalmente poco o nulla a che fare l'uno con l'altro. Si rivolgono solo a ciò che è conveniente esprimere nel linguaggio di programmazione. Bene, questo è qualcosa. Ma è una storia alquanto diversa da quella che sta guidando Flup.

Le implementazioni moderne (ovvero, agili e DDD) si concentrano più su ciò che è importante per l'azienda che su ciò che il linguaggio di programmazione può esprimere. La parte sorprendente è che i linguaggi di programmazione non hanno ancora raggiunto. I vecchi linguaggi simili a FORTRAN catturano responsabilità che si adattano ai principali modelli concettuali dell'epoca: i processi che uno applicava a ciascuna carta mentre passava attraverso il lettore di carte o (come in C) l'elaborazione che accompagnava ogni interruzione. Poi vennero le lingue ADT, che erano maturate al punto da catturare ciò che il DDD avrebbe poi reinventato come importante (anche se Jim Neighbours aveva scoperto, pubblicato e utilizzato la maggior parte di questo nel 1968): ciò che oggi chiamiamo classi . (NON sono moduli.)

Questo passaggio è stato meno un'evoluzione di un'oscillazione del pendolo. Man mano che il pendolo passava ai dati, abbiamo perso la modellazione dei casi d'uso inerente a FORTRAN. Va bene quando il tuo focus principale riguarda i dati o le forme su uno schermo. È un ottimo modello per programmi come PowerPoint, o almeno per le sue semplici operazioni.

Ciò che si è perso sono le responsabilità del sistema . Non vendiamo gli elementi di DDD. E non conosciamo bene i metodi di classe. Vendiamo responsabilità di sistema. Ad un certo livello, è necessario progettare il sistema in base al principio della responsabilità singola.

Quindi, se guardi persone come Rebecca Wirfs-Brock, o me, che parlavano di metodi di classe, ora parliamo di casi d'uso. Questo è ciò che vendiamo. Quelle sono le operazioni di sistema. Un caso d'uso dovrebbe avere un'unica responsabilità. Un caso d'uso è raramente un'unità architettonica. Ma tutti cercavano di fingere che lo fosse. Testimone del popolo SOA, per esempio.

Questo è il motivo per cui sono entusiasta dell'architettura DCI di Trygve Reenskaug, che è quella descritta nel libro Lean Architecture sopra. Finalmente dà una certa statura a quello che era un arbitrario e mistico obbedimento alla "singola responsabilità" - come si trova nella maggior parte delle argomentazioni di cui sopra. Quella statura si riferisce a modelli mentali umani: prima gli utenti finali e poi i programmatori. Si riferisce a problemi commerciali. E, quasi per caso, incapsula il cambiamento mentre il flup ci sfida.

Il principio della responsabilità singola, come lo conosciamo, è o un dinosauro rimasto dai suoi giorni di origine o un cavallo da hobby che usiamo come sostituto della comprensione. Devi lasciare alcuni di questi cavalli per hobby per fare un ottimo software. E questo richiede pensare fuori dagli schemi. Mantenere le cose semplici e facili da capire funziona solo quando il problema è semplice e facile da capire. Non sono terribilmente interessato a quelle soluzioni: non sono tipiche e non è dove sta la sfida.


2
Quando ho letto quello che hai scritto, da qualche parte lungo la strada ho perso completamente di vista quello di cui stai parlando. Le buone risposte non trattano la domanda come il punto di partenza di una passeggiata nel bosco, ma piuttosto come un tema definito a cui collegare tutta la scrittura.
Donal Fellows

1
Ah, sei uno di quelli, come uno dei miei vecchi manager. "Non vogliamo capirlo: vogliamo migliorarlo!" La questione tematica chiave qui è di principio: questa è la "P" in "SRP". Forse avrei risposto direttamente alla domanda se fosse la domanda giusta: non lo era. Puoi accettarlo con chi ha mai posto la domanda.
Far fronte

C'è una buona risposta sepolta qui da qualche parte. Penso ...
RubberDuck,

0

Sì, il principio di responsabilità singola dovrebbe essere applicato al nuovo codice.

Ma! Qual è una responsabilità?

"Stampa un rapporto è una responsabilità"? La risposta, credo sia "Forse".

Proviamo a usare la definizione di SRP come "avere un solo motivo per cambiare".

Supponiamo di avere una funzione che stampa i rapporti. Se hai due modifiche:

  1. cambia quella funzione perché il tuo rapporto deve avere uno sfondo nero
  2. cambia quella funzione perché devi stampare in pdf

Quindi la prima modifica è "cambia lo stile del report", l'altra è "cambia il formato di output del report" e ora dovresti metterli in due diverse funzioni perché sono cose diverse.

Ma se il tuo secondo cambiamento sarebbe stato:

2b. cambia quella funzione perché il tuo report richiede un carattere diverso

Direi che entrambe le modifiche sono "cambia lo stile del rapporto" e possono rimanere in una funzione.

Quindi dove ci lascia? Come al solito, dovresti cercare di mantenere le cose semplici e facili da capire. Se cambiare il colore di sfondo significa 20 righe di codice e cambiare il carattere significa 20 righe di codice, renderlo nuovamente due funzioni. Se è una riga ciascuno, tenerlo in uno.


0

Quando si progetta un nuovo sistema, è saggio considerare il tipo di modifiche che potrebbe essere necessario apportare durante il suo ciclo di vita e quanto costosi verranno forniti all'architettura che si sta realizzando. Dividere il sistema in moduli è una decisione costosa da sbagliare.

Una buona fonte di informazioni è il modello mentale nel capo degli esperti di dominio delle imprese. Prendi l'esempio del documento, la formattazione e il pdf. Gli esperti di dominio probabilmente ti diranno che formattano le loro lettere utilizzando i modelli di documento. O stazionario o in Word o altro. È possibile recuperare queste informazioni prima di iniziare a scrivere codice e utilizzarle nel progetto.

Un'ottima lettura su queste cose: Lean Architecture di Coplien


0

"Stampa" è molto simile a "visualizza" in MVC. Chiunque capisca le basi degli oggetti lo capirà.

È una responsabilità di sistema . È implementato come un meccanismo - MVC - che coinvolge una stampante (la Vista), l'oggetto in fase di stampa (il Modulo) e la richiesta e le opzioni della stampante (dal Controller).

Cercare di localizzarlo come responsabilità di classe o modulo è asinino e riflette il pensiero di 30 anni. Da allora abbiamo imparato molto ed è ampiamente dimostrato nella letteratura e nel codice dei programmatori maturi.


0

Non è una buona idea iniziare davvero ad applicare SRP solo quando iniziano le richieste di modifica del codice?

Idealmente, avrai già una buona idea di quali siano le responsabilità delle varie parti del codice. Suddividere le responsabilità in base al tuo primo istinto, tenendo probabilmente conto di ciò che le librerie che stai usando vogliono fare (delegare un'attività, una responsabilità, a una biblioteca è di solito una cosa grandiosa, a condizione che la biblioteca possa effettivamente svolgere l'attività ). Quindi, affina la tua comprensione delle responsabilità in base alle mutevoli esigenze. Migliore è la comprensione iniziale del sistema, meno è necessario modificare sostanzialmente le assegnazioni di responsabilità (anche se a volte si scopre che è meglio suddividere una responsabilità in sotto-responsabilità).

Non che dovresti passare molto tempo a preoccupartene. Una caratteristica chiave del codice è che può essere modificato in un secondo momento, non è necessario farlo completamente bene la prima volta. Cerca solo di migliorare nel tempo imparando che tipo di responsabilità formali hanno in modo da poter fare meno errori in futuro.

Un esempio potrebbe essere un pezzo di codice che stampa un documento. Una richiesta arriva per cambiarlo per stampare in PDF e quindi viene fatta una seconda richiesta per cambiarlo per applicare una formattazione diversa al documento. A questo punto hai la prova di più di un singolo motivo per cambiare (e violazione di SRP) e dovresti effettuare il refactoring appropriato.

Ciò è strettamente indicativo del fatto che la responsabilità generale - "stampare" il codice - ha delle responsabilità secondarie e dovrebbe essere suddivisa in parti. Questa non è una violazione dell'SRP in sé, ma piuttosto un'indicazione che il partizionamento (forse in "formattazione" e "rendering" delle attività secondarie) è probabilmente necessario. Puoi descrivere chiaramente queste responsabilità in modo da poter capire cosa sta succedendo nei sotto-compiti senza osservarne l'implementazione? Se puoi, è probabile che siano divisioni ragionevoli.

Potrebbe anche essere più chiaro se guardiamo un semplice esempio reale. Consideriamo il sort()metodo di utilità in java.util.Arrays. Che cosa fa? Ordina una matrice e questo è tutto. Non stampa gli elementi, non trova il membro moralmente più adatto, non fischia Dixie . Ordina semplicemente un array. Non devi nemmeno sapere come. L'ordinamento è l'unica responsabilità di quel metodo. (In effetti, ci sono molti metodi di ordinamento in Java per motivi tecnici piuttosto brutti da fare con i tipi primitivi; non devi prestare attenzione a questo, poiché hanno tutti responsabilità equivalenti.)

Rendi i tuoi metodi, le tue classi, i tuoi moduli, in modo che abbiano un ruolo così chiaramente definito nella vita. Mantiene la quantità che devi capire subito e che a sua volta è ciò che ti consente di gestire la progettazione e la manutenzione di un sistema di grandi dimensioni.

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.