Questo design di classe viola il principio della responsabilità singola?


63

Oggi ho avuto una discussione con qualcuno.

Stavo spiegando i vantaggi di avere un modello di dominio ricco rispetto a un modello di dominio anemico. E ho dimostrato il mio punto con una semplice classe simile a quella:

public class Employee
{
    public Employee(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastname;
    }

    public string FirstName { get private set; }
    public string LastName { get; private set;}
    public int CountPaidDaysOffGranted { get; private set;}

    public void AddPaidDaysOffGranted(int numberOfdays)
    {
        // Do stuff
    }
}

Mentre difendeva il suo approccio al modello anemico, uno dei suoi argomenti era: "Sono un sostenitore del SOLID . Stai violando il principio della responsabilità singola (SRP) poiché rappresenti entrambi i dati ed esegui la logica nella stessa classe".

Ho trovato questa affermazione davvero sorprendente, poiché seguendo questo ragionamento, ogni classe che ha una proprietà e un metodo viola l'SRP, e quindi OOP in generale non è SOLIDO, e la programmazione funzionale è l'unica via per il paradiso.

Ho deciso di non rispondere ai suoi numerosi argomenti, ma sono curioso di sapere cosa pensi la comunità su questa domanda.

Se avessi risposto, avrei iniziato indicando il paradosso menzionato sopra, e quindi indicando che l'SRP dipende fortemente dal livello di granularità che si desidera prendere in considerazione e che se lo si prende abbastanza lontano, qualsiasi classe contenente più di una la proprietà o un metodo la viola.

Cosa avresti detto?

Aggiornamento: l'esempio è stato generosamente aggiornato da Guntbert per rendere il metodo più realistico e aiutarci a concentrarci sulla discussione di base.


19
questa classe viola SRP non perché mescola i dati con la logica, ma perché ha una bassa coesione - potenziale oggetto di Dio
moscerino

1
A quale scopo è aggiungere le ferie al dipendente? Dovrebbe esserci forse una lezione di calendario o qualcosa che abbia le vacanze. Penso che il tuo amico abbia ragione.
James Black,

9
Non ascoltare mai qualcuno che dice "Sono un credente in X".
Stig Hemmer,

29
Non è tanto se si tratta di una violazione dell'SRP, sia se si tratta di una buona modellazione. Supponiamo che io sia un dipendente. Quando chiedo al mio manager se va bene con lui se trascorro un lungo weekend per andare a sciare, il mio manager non mi aggiunge le vacanze . Il codice qui non corrisponde alla realtà che intende modellare, quindi è sospetto.
Eric Lippert,

2
+1 per la programmazione funzionale è l'unica via per il paradiso.
Erip

Risposte:


68

La singola responsabilità dovrebbe essere intesa come un'astrazione delle attività logiche nel sistema. Una classe dovrebbe avere la sola responsabilità di (fare tutto il necessario per) svolgere un singolo compito specifico. Questo può effettivamente portare molto in una classe ben progettata, a seconda di quale sia la responsabilità. La classe che esegue il tuo motore di script, ad esempio, può avere molti metodi e dati coinvolti nell'elaborazione degli script.

Il tuo collega si sta concentrando sulla cosa sbagliata. La domanda non è "quali membri ha questa classe?" ma "quali operazioni utili esegue questa classe all'interno del programma?" Una volta capito, il tuo modello di dominio sembra perfetto.


Se la programmazione orientata agli oggetti è stata creata e ha lo scopo di modellare gli oggetti e le classi della vita reale (azioni + attributi), perché non scrivere la classe di codice che ha più responsabilità (azioni)? Un oggetto del mondo reale può avere più responsabilità. Ad esempio un giornalista scrive editoriali su giornali e intervista politici in uno spettacolo televisivo. Due responsabilità per un oggetto di vita reale! E se avessi intenzione di scrivere un giornalista di classe?
user1451111

41

Il singolo principio di responsabilità riguarda solo se un pezzo di codice (in OOP, in genere stiamo parlando di classi) ha la responsabilità di un pezzo di funzionalità . Penso che il tuo amico dicendo che funzioni e dati non possono mescolarsi davvero non ha capito quell'idea. Se Employeeanche contenesse informazioni sul suo posto di lavoro, sulla velocità della sua auto e sul tipo di cibo che mangia il suo cane, avremmo un problema.

Dal momento che questa classe si occupa solo di un Employee, penso che sia giusto dire che non viola palesemente SRP, ma le persone avranno sempre le proprie opinioni.

Un posto in cui potremmo migliorare è quello di separare le informazioni dei dipendenti (come nome, numero di telefono, e-mail) dalla sua vacanza. Nella mia mente, questo non significa che metodi e dati non possano mescolarsi , significa solo che forse la funzionalità di vacanza potrebbe essere in un posto separato.


20

A mio parere questa classe potrebbe potenzialmente violare SRP se avesse continuato a rappresentare sia una Employeee EmployeeHolidays.

Così com'è, e se mi venisse in mente per Peer Review, probabilmente lo lascerei passare. Se fossero state aggiunte più proprietà e metodi specifici per i dipendenti, e fossero state aggiunte altre proprietà specifiche per le vacanze, probabilmente consiglierei una divisione, citando sia SRP che ISP.


Sono d'accordo. Se il codice è semplice come fornito qui, probabilmente lo lascerei scorrere. Ma nella mia mente, non dovrei essere la responsabilità del Dipendente di gestire le proprie vacanze. Potrebbe non sembrare un grosso problema con la posizione della responsabilità, ma guardala in questo modo: se tu fossi nuovo alla base di codice e dovessi lavorare sulla funzionalità specifica per le vacanze, dove vorresti guardare prima? Per quanto riguarda la logica delle vacanze, personalmente NON guarderei l'entità Employee per cominciare.
Niklas H,

1
@NiklasH Concordato. Personalmente, non guarderei in modo casuale e proverei a indovinare la lezione con cui avrei cercato in studio la parola "Vacanza" e avrei visto in quali classi si
trovava

4
Vero. Ma cosa succede se non viene chiamato "Vacanza" in questo nuovo sistema, ma è "Vacanza" o "Tempo libero". Ma sono d'accordo, con quello in genere hai la possibilità di cercarlo o puoi chiedere a un collega. Il mio commento era principalmente quello di convincere l'OP a modellare mentalmente la responsabilità e dove sarebbe il posto più ovvio per la logica :-)
Niklas H

Sono d'accordo con la tua prima dichiarazione. Tuttavia, se si arrivasse alla revisione tra pari, non penso che lo farei perché violare SRP è una pendenza scivolosa e questa potrebbe essere la prima di molte finestre rotte. Saluti.
Jim Speaker,

20

Ci sono già ottime risposte che sottolineano che SRP è un concetto astratto sulla funzionalità logica, ma ci sono punti più sottili che penso valga la pena aggiungere.

Le prime due lettere in SOLID, SRP e OCP riguardano entrambe le modalità di modifica del codice in risposta a una modifica dei requisiti. La mia definizione preferita di SRP è: "un modulo / classe / funzione dovrebbe avere solo un motivo per cambiare". Discutere sulle possibili ragioni per cui il tuo codice cambia è molto più produttivo che discutere se il tuo codice è SOLIDO o meno.

Quanti motivi deve cambiare la tua classe Employee? Non lo so, perché non conosco il contesto in cui lo stai usando e non riesco nemmeno a vedere il futuro. Quello che posso fare è fare il brainstorming su possibili cambiamenti basati su ciò che ho visto in passato e puoi valutare soggettivamente la loro probabilità. Se più di un punteggio tra "ragionevolmente probabile" e "il mio codice è già cambiato per quel motivo esatto", allora stai violando SRP contro quel tipo di cambiamento. Eccone uno: qualcuno con più di due nomi si unisce alla tua azienda (o un architetto legge questo eccellente articolo del W3C ). Eccone un altro: la tua azienda cambia il modo in cui assegna i giorni festivi.

Si noti che questi motivi sono ugualmente validi anche se si rimuove il metodo AddHolidays. Molti modelli di domini anemici violano l'SRP. Molti di essi sono solo tabelle di database nel codice ed è molto comune che le tabelle di database abbiano più di 20 motivi per cambiare.

Ecco qualcosa da masticare: la classe dei tuoi dipendenti cambierebbe se il tuo sistema dovesse tenere traccia degli stipendi dei dipendenti? Indirizzi? Informazioni di contatto di emergenza? Se hai detto "sì" (e "probabile che accada") a due di questi, la tua classe violerebbe SRP anche se non avesse ancora un codice! SOLID riguarda i processi e l'architettura tanto quanto il codice.


9

Che la classe rappresenti i dati non è responsabilità della classe, è un dettaglio dell'implementazione privata.

La classe ha una responsabilità, quella di rappresentare un dipendente. In questo contesto, ciò significa che presenta alcune API pubbliche che ti forniscono le funzionalità di cui hai bisogno per trattare con i dipendenti (se AddHolidays è un buon esempio è discutibile).

L'implementazione è interna; succede che ciò necessita di alcune variabili private e della logica. Ciò non significa che la classe ora abbia più responsabilità.


Interessante linea di pensiero, grazie mille per la condivisione
tobiak777

Bello - Buon modo per esprimere gli obiettivi previsti di OOP.
user949300

5

L'idea che mescolare logica e dati sia sempre sbagliata, è così ridicola da non meritare nemmeno una discussione. Tuttavia, c'è davvero una chiara violazione della singola responsabilità nell'esempio, ma non è perché c'è una proprietà DaysOfHolidayse una funzione AddHolidays(int).

È perché l'identità del dipendente è mescolata alla gestione delle vacanze, il che è un male. L'identità del dipendente è una cosa complessa che è richiesta per tenere traccia delle ferie, dello stipendio, degli straordinari, per rappresentare chi fa parte di una squadra, per collegarsi ai rapporti sulle prestazioni, ecc. Un dipendente può anche cambiare nome, cognome o entrambi, e rimanere lo stesso dipendente, impiegato. I dipendenti possono anche avere più ortografie del loro nome come ASCII e ortografia Unicode. Le persone possono avere da 0 a n nome e / o cognome. Possono avere nomi diversi in diverse giurisdizioni. Tracciare l'identità di un dipendente è una responsabilità sufficiente per cui la gestione delle ferie o delle ferie non può essere aggiunta in cima senza chiamarla una seconda responsabilità.


"Tracciare l'identità di un dipendente è abbastanza una responsabilità che la gestione delle ferie o delle ferie non può essere aggiunta al massimo senza definirla una seconda responsabilità." + Il dipendente potrebbe avere diversi nomi, ecc ... Il punto di un modello è quello di concentrarsi sui fatti rilevanti del mondo reale per il problema in questione. Esistono requisiti per i quali questo modello è ottimale. In questi requisiti, i dipendenti sono interessanti solo nella misura in cui possiamo modificare le loro vacanze e non siamo troppo interessati a gestire altri aspetti delle loro specifiche di vita reale.
tobiak777,

@reddy "I dipendenti sono interessanti solo nella misura in cui possiamo modificare le loro vacanze" - Il che significa che devi identificarli correttamente. Non appena hai un dipendente, possono cambiare il loro cognome in qualsiasi momento a causa del matrimonio o del divorzio. Possono anche cambiare nome e sesso. Licenzierai i dipendenti se il loro cognome cambia in modo tale che il loro nome corrisponda a quello di un altro dipendente? Non aggiungerai tutte queste funzionalità in questo momento. Invece lo aggiungerai quando ne avrai bisogno, il che è positivo. Indipendentemente da quanto viene implementato, la responsabilità dell'identificazione rimane la stessa.
Peter,

3

"Sono un sostenitore di SOLID. Stai violando il principio della responsabilità singola (SRP) poiché rappresenti entrambi i dati ed esegui la logica nella stessa classe."

Come gli altri, non sono d'accordo con questo.

Direi che l'SRP viene violato se si esegue più di un pezzo di logica nella classe. La quantità di dati che devono essere memorizzati all'interno della classe per ottenere quel singolo pezzo di logica è irrilevante.


No! SRP non è violato da più parti di logica, più parti di dati o qualsiasi combinazione delle due. L'unico requisito è che l'oggetto dovrebbe attenersi al suo scopo. Il suo scopo può potenzialmente comportare molte operazioni.
Martin Maat,

@MartinMaat: molte operazioni, sì. Di conseguenza un pezzo di logica. Penso che stiamo dicendo la stessa cosa ma con termini diversi (e sono felice di presumere che i tuoi siano quelli giusti in quanto non studio queste cose spesso)
Lightness Races con Monica il

2

In questi giorni non trovo utile discutere di ciò che fa e non costituisce una singola responsabilità o un unico motivo per cambiare. Vorrei proporre un principio di lutto minimo al suo posto:

Principio del dolore minimo: il codice dovrebbe cercare di ridurre al minimo la probabilità di richiedere modifiche o massimizzare la facilità di modifica.

Com'è quello? Non dovrebbe prendere uno scienziato missilistico per capire perché questo può aiutare a ridurre i costi di manutenzione e speriamo che non dovrebbe essere un punto di dibattito senza fine, ma come con SOLID in generale, non è qualcosa da applicare ciecamente ovunque. È qualcosa da considerare durante il bilanciamento dei compromessi.

Per quanto riguarda la probabilità di richiedere cambiamenti, ciò si riduce con:

  1. Buoni test (maggiore affidabilità).
  2. Coinvolgere solo il codice minimo necessario per fare qualcosa di specifico (questo può includere la riduzione degli accoppiamenti afferenti).
  3. Basta rendere il codice tosto in quello che fa (vedi Make Badass Principle).

Per quanto riguarda la difficoltà di apportare modifiche, aumenta con gli accoppiamenti efferenti. Il test introduce giunti efficienti ma migliora l'affidabilità. Fatto bene, generalmente fa più bene che male ed è totalmente accettabile e promosso dal principio del minimo dolore.

Crea il Badass Principle: le classi che vengono utilizzate in molti luoghi dovrebbero essere fantastiche. Dovrebbero essere affidabili, efficienti se questo si lega alla loro qualità, ecc.

E il principio Make Badass è legato al principio del minimo dolore, poiché le cose toste troveranno una probabilità inferiore di richiedere cambiamenti rispetto alle cose che fanno schifo in ciò che fanno.

Avrei iniziato indicando il paradosso menzionato sopra e quindi indicando che l'SRP è fortemente dipendente dal livello di granularità che si desidera prendere in considerazione e che se lo si porta abbastanza lontano, qualsiasi classe contenente più di una proprietà o un metodo viola esso.

Dal punto di vista dell'SRP, una classe che fa a malapena qualsiasi cosa avrebbe certamente solo una (a volte zero) ragioni per cambiare:

class Float
{
public:
    explicit Float(float val);
    float get() const;
    void set(float new_val);
};

Questo praticamente non ha motivi per cambiare! È meglio di SRP. È ZRP!

Tranne che suggerirei che è in palese violazione del principio Make Badass. È anche assolutamente inutile. Qualcosa che fa così poco non può sperare di essere tosto. Ha troppe poche informazioni (TLI). E naturalmente quando hai qualcosa che è TLI, non può fare nulla di veramente significativo, nemmeno con le informazioni che incapsula, quindi deve perderlo nel mondo esterno nella speranza che qualcun altro faccia effettivamente qualcosa di significativo e tosto. E quella perdita va bene per qualcosa che vuole solo aggregare i dati e niente di più, ma quella soglia è la differenza come vedo tra "dati" e "oggetti".

Naturalmente anche qualcosa che è TMI è male. Potremmo mettere il nostro intero software in una classe. Può anche avere solo un runmetodo. E qualcuno potrebbe persino sostenere che ora ha una ragione molto ampia per cambiare: "Questa classe dovrà essere cambiata solo se il software necessita di miglioramenti". Sono sciocco, ma ovviamente possiamo immaginarci tutti i problemi di manutenzione.

Quindi c'è un atto di bilanciamento per quanto riguarda la granularità o la ruvidezza degli oggetti che disegni. Lo giudico spesso in base a quante informazioni devi divulgare al mondo esterno e a quante funzionalità significative può svolgere. Trovo spesso utile il principio Rendi Badass per trovare l'equilibrio mentre lo combino con il principio del lutto minimo.


1

Al contrario, per me il modello di dominio anemico rompe alcuni concetti principali di OOP (che legano insieme attributi e comportamenti), ma può essere inevitabile sulla base di scelte architettoniche. I domini anemici sono più facili da pensare, meno organici e più sequenziali.

Molti sistemi tendono a farlo quando più livelli devono giocare con gli stessi dati (livello di servizio, livello Web, livello client, agenti ...).

È più semplice definire la struttura dei dati in una posizione e il comportamento in altre classi di servizio. Se la stessa classe è stata utilizzata su più livelli, questa può diventare grande e pone la domanda su quale livello è responsabile per definire il comportamento di cui ha bisogno e chi è in grado di chiamare i metodi.

Ad esempio, potrebbe non essere una buona idea di un processo agente che calcola le statistiche su tutti i dipendenti che possono chiamare un calcolo per giorni retribuiti. E la GUI dell'elenco dei dipendenti non ha certo bisogno del nuovo calcolo dell'ID aggregato utilizzato in questo agente statistico (e dei dati tecnici che ne derivano). Quando si separano i metodi in questo modo, generalmente si termina con una classe con solo strutture di dati.

Puoi serializzare / deserializzare troppo facilmente i dati "oggetto", o solo alcuni di essi, o in un altro formato (json) ... senza preoccuparti di alcun concetto / responsabilità dell'oggetto. Tuttavia, sono solo i dati a passare. Puoi sempre mappare i dati tra due classi (Employee, EmployeeVO, EmployeeStatistic ...) ma cosa significa veramente Employee qui?

Quindi sì, separa completamente i dati nelle classi di dominio e la gestione dei dati nelle classi di servizio, ma qui è necessario. Tale sistema è al tempo stesso funzionale per apportare valore commerciale e anche tecnico per propagare i dati ovunque sia necessario, pur mantenendo un adeguato ambito di responsabilità (e l'oggetto distribuito non risolve neanche questo).

Se non è necessario separare gli ambiti di comportamento, si è più liberi di inserire i metodi nelle classi di servizio o nelle classi di dominio, a seconda di come si vede l'oggetto. Tendo a vedere un oggetto come il concetto "reale", questo naturalmente aiuta a mantenere SRP. Quindi nel tuo esempio, è più realista del Boss del dipendente aggiungere il giorno di paga concesso al suo PayDayAccount. Un dipendente è assunto da società, Works, può essere malato o essere chiesto un consiglio e ha un account Payday (il Boss può recuperarlo direttamente da lui o da un registro PayDayAccount ...) Ma puoi creare un collegamento aggregato qui per semplicità se non vuoi troppa complessità per un software semplice.


Grazie per il tuo contributo Vince. Il punto è che non è necessario un livello di servizio quando si dispone di un dominio avanzato. C'è solo una classe responsabile per il comportamento ed è la tua entità di dominio. Gli altri livelli (livello Web, interfaccia utente ecc ...) in genere si occupano di DTO e ViewModels e questo va bene. Il modello di dominio consiste nel modellare il dominio, non nell'esecuzione delle operazioni dell'interfaccia utente o nell'invio di messaggi su Internet. Il tuo messaggio riflette questo malinteso comune, che deriva dal fatto che le persone semplicemente non sanno come adattare OOP al loro design. E penso che questo sia molto triste - per loro.
tobiak777,

Non penso di avere un'idea sbagliata del ricco dominio OOP, ho progettato molti sotwares in quel modo, ed è vero che è davvero buono per la manutenzione / evoluzione. Ma mi dispiace dirti che questo non è un proiettile d'argento.
Vince il

Non sto dicendo che lo sia. Per la scrittura di un compilatore probabilmente non lo è, ma per la maggior parte delle applicazioni line-of-business / SaaS, penso che sia molto meno arte / scienza di quanto tu suggerisca. La necessità di un modello di dominio può essere dimostrata matematicamente, i tuoi esempi mi fanno pensare a progetti discutibili piuttosto che a difetti di limitazioni in OOP
tobiak777

0

Stai violando il principio di responsabilità singola (SRP) poiché rappresenti entrambi i dati ed esegui la logica nella stessa classe.

Sembra molto ragionevole per me. Il modello potrebbe non avere proprietà pubbliche se espone azioni. È fondamentalmente un'idea di separazione comando-query. Si noti che Command avrà sicuramente uno stato privato.


0

Non è possibile violare il principio di responsabilità singola perché è solo un criterio estetico, non una regola della natura. Non lasciarti ingannare dal nome scientificamente valido e dalle lettere maiuscole.


1
Questa non è una vera risposta, ma sarebbe stato fantastico come commento alla domanda.
Jay Elston,
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.