Applicabilità del principio di responsabilità singola


40

Di recente ho riscontrato un problema architettonico apparentemente banale. Nel mio codice avevo un semplice repository chiamato così (il codice è in C #):

var user = /* create user somehow */;
_userRepository.Add(user);
/* do some other stuff*/
_userRepository.SaveChanges();

SaveChanges era un semplice wrapper che apporta modifiche al database:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
}

Quindi, dopo qualche tempo, avevo bisogno di implementare una nuova logica che avrebbe inviato notifiche e-mail ogni volta che un utente veniva creato nel sistema. Dato che c'erano molte chiamate verso _userRepository.Add()e SaveChangesintorno al sistema, ho deciso di aggiornare in SaveChangesquesto modo:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
    foreach (var newUser in dataContext.GetAddedUsers())
    {
       _eventService.RaiseEvent(new UserCreatedEvent(newUser ))
    }
}

In questo modo, il codice esterno potrebbe abbonarsi a UserCreatedEvent e gestire la logica aziendale necessaria per inviare notifiche.

Ma mi è stato fatto notare che la mia modifica del SaveChangesprincipio della singola responsabilità era stata violata e che SaveChangesavrebbe dovuto salvare e non generare alcun evento.

È un punto valido? Mi sembra che la raccolta di un evento qui sia essenzialmente la stessa cosa della registrazione: basta aggiungere qualche funzionalità laterale alla funzione. E SRP non ti proibisce di utilizzare la registrazione o l'attivazione di eventi nelle tue funzioni, dice solo che tale logica dovrebbe essere incapsulata in altre classi ed è OK per un repository chiamare queste altre classi.


22
La vostra risposta è: "OK, così come faresti tu scrivere in modo che esso non viola SRP, ma permette comunque un unico punto di modifica?"
Robert Harvey,

43
La mia osservazione è che la presentazione di un evento non aggiunge una responsabilità aggiuntiva. Anzi, al contrario: delega la responsabilità altrove.
Robert Harvey,

Penso che il tuo collega abbia ragione, ma la tua domanda è valida e utile, quindi votata!
Andres F.

16
Non esiste una definizione definitiva di singola responsabilità. La persona che sottolinea che viola SRP è corretta usando la sua definizione personale e tu hai ragione usando la tua definizione. Penso che il tuo design sia perfettamente adeguato con l'avvertenza che questo evento non è una tantum in base al quale altre funzionalità simili vengono eseguite in diversi modi. La coerenza è molto, molto, molto più importante a cui prestare attenzione rispetto ad alcune linee guida vaghe come SRP che ha portato all'estremo finisce con tonnellate di classi molto facili da capire che nessuno sa come lavorare in un sistema.
Dunk il

Risposte:


14

Sì, può essere un requisito valido disporre di un repository che genera determinati eventi su determinate azioni come Addo SaveChanges- e non ho intenzione di metterlo in discussione (come alcune altre risposte) solo perché il tuo esempio specifico di aggiunta di utenti e di invio di e-mail potrebbe apparire un un po 'inventato. Di seguito, supponiamo che questo requisito sia perfettamente giustificato nel contesto del tuo sistema.

Quindi , la codifica della meccanica degli eventi, nonché la registrazione e il salvataggio in un metodo violano l'SRP . Per molti casi, è probabilmente una violazione accettabile, soprattutto quando nessuno vuole distribuire le responsabilità di manutenzione di "salvare le modifiche" e "aumentare l'evento" a diversi team / manutentori. Ma supponiamo che un giorno qualcuno voglia fare esattamente questo, può essere risolto in modo semplice, magari inserendo il codice di tali problemi in diverse librerie di classi?

La soluzione a questo è lasciare che il repository originale sia responsabile delle modifiche apportate al database, nient'altro e creare un repository proxy che abbia esattamente la stessa interfaccia pubblica, riutilizzare il repository originale e aggiungere la meccanica degli eventi aggiuntiva ai metodi.

// In EventFiringUserRepo:
public void SaveChanges()
{
  _basicRepo.SaveChanges();
   FireEventsForNewlyAddedUsers();
}

private void FireEventsForNewlyAddedUsers()
{
  foreach (var newUser in _basicRepo.DataContext.GetAddedUsers())
  {
     _eventService.RaiseEvent(new UserCreatedEvent(newUser))
  }
}

Puoi chiamare la classe proxy a NotifyingRepositoryo ObservableRepositoryse vuoi, sulla falsariga della risposta molto votata di @ Peter (che in realtà non dice come risolvere la violazione di SRP, dicendo solo che la violazione è ok).

La nuova e la vecchia classe di repository dovrebbero entrambe derivare da un'interfaccia comune, come mostrato nella descrizione del modello proxy classico .

Quindi, nel codice originale, inizializzare _userRepositorycon un oggetto della nuova EventFiringUserRepoclasse. In questo modo, manterrai il repository originale separato dalla meccanica degli eventi. Se necessario, è possibile disporre fianco a fianco del repository di attivazione eventi e del repository originale e consentire ai chiamanti di decidere se utilizzare il primo o il secondo.

Per eliminare una preoccupazione menzionata nei commenti: ciò non porta a deleghe oltre a deleghe oltre a deleghe e così via? In realtà, l'aggiunta della meccanica degli eventi crea una base per aggiungere ulteriori requisiti del tipo "invia e-mail" semplicemente iscrivendosi agli eventi, quindi attenendosi all'SRP con tali requisiti, senza ulteriori proxy. Ma l'unica cosa che deve essere aggiunta una volta qui è la meccanica degli eventi stessa.

Se questo tipo di separazione è davvero valsa la pena nel contesto del tuo sistema è qualcosa che tu e il tuo recensore dovete decidere da soli. Probabilmente non separerei la registrazione dal codice originale, né usando un altro proxy non aggiungendo un logger all'evento listener, anche se sarebbe possibile.


3
Oltre a questa risposta. Esistono alternative ai proxy, come AOP .
Laiv

1
Penso che manchi il punto, non è che sollevare un evento rompe l'SRP, è che sollevare un evento solo per "Nuovi" utenti richiede che il repository sia responsabile di sapere cosa costituisce un "Nuovo" utente piuttosto che un "Newly Added to Me "utente
Ewan

@Ewan: leggi di nuovo la domanda. Si trattava di un posto nel codice che compie determinate azioni che devono essere accoppiate ad altre azioni al di fuori della responsabilità di quell'oggetto. E mettere l'azione e il rilancio dell'evento in un unico posto è stato messo in discussione da un revisore dei pari come una rottura dell'SRP. L'esempio di "salvataggio di nuovi utenti" è solo a scopo dimostrativo, chiamare l'esempio inventato se lo si desidera, ma questo è IMHO non il punto della domanda.
Doc Brown,

2
Questa è la risposta IMO migliore / corretta. Non solo mantiene SRP, ma mantiene anche il principio Open / Closed. E pensa a tutti i test automatici che i cambiamenti all'interno della classe potrebbero interrompere. Modificare i test esistenti quando si aggiungono nuove funzionalità è un grande odore.
Keith Payne,

1
Come funziona questa soluzione se è in corso una transazione? Quando ciò accade, in SaveChanges()realtà non crea il record del database e potrebbe finire con il rollback. Sembra che dovresti sovrascrivere AcceptAllChangeso iscriverti all'evento TransactionCompleted.
John Wu,

29

L'invio di una notifica che l'archivio dati persistente è cambiato sembra una cosa ragionevole da fare durante il salvataggio.

Ovviamente non dovresti trattare Aggiungi come un caso speciale: dovresti anche attivare eventi per Modifica ed Elimina. È il trattamento speciale del caso "Aggiungi" che odora, costringe il lettore a spiegare perché ha un odore e alla fine porta alcuni lettori del codice a concludere che deve violare SRP.

Un repository "notificante" che può essere interrogato, modificato e che genera eventi sulle modifiche, è un oggetto perfettamente normale. Puoi aspettarti di trovarne più varianti in quasi tutti i progetti di dimensioni decenti.


Ma un repository "notificante" è effettivamente ciò di cui hai bisogno? Hai citato C #: molte persone concorderebbero sul fatto che usare un System.Collections.ObjectModel.ObservableCollection<>invece di System.Collections.Generic.List<>quando quest'ultimo è tutto ciò di cui hai bisogno sono tutti i tipi di cattivi e sbagliati, ma pochi indicherebbero immediatamente SRP.

Quello che stai facendo ora è scambiare il tuo UserList _userRepositorycon un ObservableUserCollection _userRepository. Se questo è il miglior modo di agire o meno dipende dall'applicazione. Ma mentre rende senza dubbio il _userRepositoryconsiderevolmente meno leggero, secondo la mia modesta opinione non viola SRP.


Il problema con l'utilizzo ObservableCollectionper questo caso è che innesca l'evento equivalente non alla chiamata SaveChanges, ma alla chiamata a Add, che porterebbe a un comportamento molto diverso da quello mostrato nell'esempio. Vedi la mia risposta su come mantenere leggero il repository originale e attenersi all'SRP mantenendo intatta la semantica.
Doc Brown,

@DocBrown Ho invocato le classi conosciute ObservableCollection<>e List<>per confronto e contesto. Non intendevo raccomandare l'uso delle classi effettive né per l'implementazione interna né per l'interfaccia esterna.
Peter

Ok, ma anche se l'OP aggiungesse eventi a "Modifica" ed "Elimina" (che penso che l'OP abbia lasciato fuori per mantenere la domanda concisa, per semplicità), penso che un revisore potrebbe facilmente giungere alla conclusione di una violazione di SRP. Probabilmente è accettabile, ma nessuno che non può essere risolto se necessario.
Doc Brown,

16

Sì, è una violazione del principio della responsabilità singola e un punto valido.

Una progettazione migliore sarebbe quella di avere un processo separato per recuperare "nuovi utenti" dal repository e inviare le e-mail. Tenere traccia di quali utenti sono stati inviati un'e-mail, guasti, rinvii, ecc. Ecc.

In questo modo è possibile gestire errori, arresti anomali e simili, oltre a evitare che il proprio repository afferri tutti i requisiti che hanno l'idea che gli eventi accadano "quando qualcosa è impegnato nel database".

Il repository non sa che un utente che aggiungi è un nuovo utente. La sua responsabilità è semplicemente memorizzare l'utente.

Probabilmente vale la pena espandere i commenti qui sotto.

Il repository non sa che un utente che aggiungi è un nuovo utente - sì, ha un metodo chiamato Aggiungi. La sua semantica implica che tutti gli utenti aggiunti siano nuovi utenti. Combina tutti gli argomenti passati ad Aggiungi prima di chiamare Salva e otterrai tutti i nuovi utenti

Non corretto. Stai combinando "Aggiunto al repository" e "Nuovo".

"Aggiunto al repository" significa esattamente ciò che dice. Posso aggiungere e rimuovere e aggiungere nuovamente utenti a vari repository.

"Nuovo" è lo stato di un utente definito dalle regole aziendali.

Attualmente la regola aziendale potrebbe essere "Nuovo == appena aggiunto al repository", ma ciò non significa che non sia una responsabilità separata conoscere e applicare quella regola.

Devi stare attento a evitare questo tipo di pensiero incentrato sul database. Avrai processi edge case che aggiungono utenti non nuovi al repository e quando invii e-mail a loro tutti gli affari diranno "Naturalmente quelli non sono" nuovi "utenti! La regola effettiva è X"

A questa risposta l'IMHO non ha abbastanza ragione: il repository è esattamente l'unico posto centrale nel codice che sa quando vengono aggiunti nuovi utenti

Non corretto. Per i motivi di cui sopra, in più non è una posizione centrale a meno che tu non includa effettivamente il codice di invio e-mail nella classe piuttosto che generare un evento.

Avrai applicazioni che usano la classe repository, ma non hanno il codice per inviare l'e-mail. Quando aggiungi utenti in tali applicazioni, l'e-mail non verrà inviata.


11
Il repository non sa che un utente che aggiungi è un nuovo utente - sì, ha un metodo chiamato Add. La sua semantica implica che tutti gli utenti aggiunti siano nuovi utenti. Combina tutti gli argomenti passati Addprima di chiamare Savee otterrai tutti i nuovi utenti.
Andre Borges,

Mi piace questo suggerimento. Tuttavia, il pragmatismo prevale sulla purezza. A seconda delle circostanze, aggiungere un livello architettonico completamente nuovo a un'applicazione esistente può essere difficile da giustificare se tutto ciò che devi fare è letteralmente inviare una singola email quando viene aggiunto un utente.
Alexander

Ma l'evento non dice l'utente aggiunto. Dice che l'utente ha creato. Se consideriamo la denominazione corretta delle cose e siamo d'accordo con le differenze semantiche tra aggiungi e crea, allora l'evento nello snippet ha un nome errato o non è inserito correttamente. Non credo che il recensore abbia avuto qualcosa contro i repository notyfing. Probabilmente era preoccupato per il tipo di evento e i suoi effetti collaterali.
Laiv

7
@Andre Nuovo nel repository, ma non necessariamente "nuovo" nel senso degli affari. è la fusione di queste due idee che nasconde la responsabilità extra a prima vista. Potrei importare una tonnellata di vecchi utenti nel mio nuovo repository, o rimuovere e aggiungere nuovamente un utente, ecc. Ci saranno regole commerciali su ciò che un "nuovo utente" è oltre "è stato aggiunto al dB"
Ewan

1
Nota del moderatore: la tua risposta non è un'intervista giornalistica. Se hai delle modifiche, incorporale naturalmente nella tua risposta senza creare l'intero effetto "ultime notizie". Non siamo un forum di discussione.
Robert Harvey,

7

È un punto valido?

Sì, anche se dipende molto dalla struttura del tuo codice. Non ho il contesto completo, quindi cercherò di parlare in generale.

Mi sembra che la raccolta di un evento qui sia essenzialmente la stessa cosa della registrazione: basta aggiungere qualche funzionalità laterale alla funzione.

Non lo è assolutamente. La registrazione non fa parte del flusso aziendale, può essere disabilitata, non dovrebbe causare effetti collaterali (aziendali) e non dovrebbe influenzare in alcun modo lo stato e la salute dell'applicazione, anche se per qualche motivo non si fosse in grado di accedere niente di più. Ora confrontalo con la logica che hai aggiunto.

E SRP non ti proibisce di utilizzare la registrazione o l'attivazione di eventi nelle tue funzioni, dice solo che tale logica dovrebbe essere incapsulata in altre classi ed è OK per un repository chiamare queste altre classi.

SRP funziona in tandem con ISP (S e I in SOLID). Si finisce con molte classi e metodi che fanno cose molto specifiche e nient'altro. Sono molto focalizzati, molto facili da aggiornare o sostituire e in generale facili da testare. Ovviamente in pratica avrai anche alcune classi più grandi che si occupano dell'orchestrazione: avranno un certo numero di dipendenze e non si concentreranno su azioni atomizzate, ma su azioni commerciali, che potrebbero richiedere più passaggi. Finché il contesto aziendale è chiaro, possono anche essere definiti una singola responsabilità, ma come hai detto correttamente, con la crescita del codice, potresti voler astrarre parte di esso in nuove classi / interfacce.

Ora torniamo al tuo esempio particolare. Se devi assolutamente inviare una notifica ogni volta che viene creato un utente e magari eseguire anche altre azioni più specializzate, potresti creare un servizio separato che incapsula questo requisito, qualcosa di simile UserCreationService, che espone un metodo Add(user), che gestisce sia l'archiviazione (la chiamata al tuo repository) e la notifica come singola azione aziendale. O fallo nel tuo frammento originale, dopo_userRepository.SaveChanges();


2
La registrazione non fa parte del flusso aziendale : come è rilevante nel contesto di SRP? Se lo scopo del mio evento fosse l'invio di nuovi dati utente a Google Analytics, la sua disattivazione avrebbe lo stesso effetto commerciale della disabilitazione della registrazione: non critica, ma piuttosto sconvolgente. Qual è la regola di un pollice per aggiungere / non aggiungere nuova logica a una funzione? "Disabilitarlo causerà importanti effetti collaterali aziendali?"
Andre Borges,

2
If the purpose of my event would be to send new user data to Google Analytics - then disabling it would have the same business effect as disabling logging: not critical, but pretty upsetting . E se sparassi eventi prematuri causando false "notizie". Cosa succede se le analisi tengono conto degli "utenti" che non sono stati infine creati a causa di errori con la transazione DB? Che cosa succede se la società prende decisioni su premesse false, supportate da dati imprecisi? Sei troppo concentrato sul lato tecnico del problema. "A volte non riesci a vedere il bosco per gli alberi"
Laiv

@Laiv, stai facendo un punto valido, ma questo non è il punto della mia domanda, o questa risposta. La domanda è se questa è una soluzione valida nel contesto di SRP, quindi supponiamo che non ci siano errori di transazione DB.
Andre Borges,

In pratica mi stai chiedendo di dirti quello che vuoi sentire. Ti do solo spazio. Un ambito più ampio per decidere se SRP è importante o meno perché SRP è inutile senza il contesto appropriato. Il modo in cui stai affrontando la questione dell'IMO non è corretto perché ti stai concentrando solo sulla soluzione tecnica. Dovresti dare abbastanza rilevanza all'intero contesto. E sì, DB potrebbe fallire. C'è la possibilità che ciò accada e non dovresti ometterlo, perché come sai, le cose accadono e queste cose potrebbero farti cambiare idea riguardo ai dubbi su SRP o altre buone pratiche.
Laiv

1
Detto questo, ricorda che i principi non sono regole scritte nella pietra. Sono permeabili (adattivo). Come puoi vedere, sono aperti all'interpretazione. Il tuo recensore ha un'interpretazione e tu ne hai un'altra. Cerca di vedere quello che vedi, risolvi i suoi dubbi e le tue preoccupazioni o lascia che risolva i tuoi. Non troverai la risposta "giusta" qui. La risposta corretta spetta a te e al tuo revisore per trovare, chiedendo innanzitutto i requisiti (funzionali e non funzionali) del progetto.
Laiv

4

L'SRP riguarda, teoricamente, le persone , come spiega lo zio Bob nel suo articolo The Single Responsibility Principle . Grazie Robert Harvey per averlo fornito nel tuo commento.

La domanda corretta è:

Quale "stakeholder" ha aggiunto il requisito "invia e-mail"?

Se tale stakeholder è anche responsabile della persistenza dei dati (improbabile ma possibile), ciò non viola SRP. Altrimenti, lo fa.


2
Interessante: non ho mai sentito parlare di questa interpretazione dell'SRP. Hai qualche suggerimento per ulteriori informazioni / letteratura su questa interpretazione?
sleske,

2
@sleske: Dallo stesso zio Bob : "E questo arriva al nocciolo del principio della responsabilità singola. Questo principio riguarda le persone. Quando scrivi un modulo software, vuoi assicurarti che quando vengono richieste modifiche, queste modifiche possono solo avere origine da una singola persona, o meglio, da un singolo gruppo strettamente accoppiato di persone che rappresentano un'unica funzione aziendale strettamente definita. "
Robert Harvey,

Grazie Robert. IMO, Il nome "Principio della singola responsabilità" è terribile, in quanto sembra semplice, ma troppo poche persone seguono il significato inteso di "responsabilità". Un po 'come il modo in cui OOP è mutato da molti dei suoi concetti originali, ed è ora un termine abbastanza insignificante.
user949300

1
Sì. Questo è ciò che è successo al termine REST. Perfino Roy Fielding afferma che la gente lo sta usando male.
Robert Harvey,

Sebbene la citazione sia correlata, penso che questa risposta manchi che il requisito "invia e-mail" non è uno dei requisiti diretti di cui tratta la domanda di violazione di SRP. Tuttavia, dicendo "Quale" stakeholder "ha aggiunto il requisito" alzare l'evento " , questa risposta diventerebbe più correlata alla domanda reale. Ho modificato un po 'la mia risposta per renderlo più chiaro.
Doc Brown,

2

Mentre tecnicamente non c'è nulla di sbagliato nei repository che notificano eventi, suggerirei di guardarlo da un punto di vista funzionale in cui la sua convenienza solleva alcune preoccupazioni.

Creare un utente, decidere cos'è un nuovo utente e la sua persistenza sono 3 cose diverse .

Premessa mia

Considerare la premessa precedente prima di decidere se il repository è il luogo adatto per notificare eventi aziendali (indipendentemente dall'SRP). Nota che ho detto evento commerciale perché per me UserCreatedha una connotazione diversa da UserStoredo UserAdded 1 . Vorrei anche considerare che ciascuno di quegli eventi fosse indirizzato a un pubblico diverso.

Da un lato, la creazione di utenti è una regola specifica per l'azienda che potrebbe o meno implicare persistenza. Potrebbe coinvolgere più operazioni aziendali, coinvolgendo più operazioni di database / rete. Operazioni di cui il livello di persistenza non è a conoscenza. Il livello di persistenza non ha abbastanza contesto per decidere se il caso d'uso è terminato correttamente o meno.

D'altro canto, non è necessariamente vero che _dataContext.SaveChanges();l'utente ha persistito con successo. Dipenderà dall'intervallo di transazioni del database. Ad esempio, potrebbe essere vero per database come MongoDB, le cui transazioni sono atomiche, ma non potrebbe, per RDBMS tradizionale che implementa transazioni ACID in cui potrebbero esserci più transazioni coinvolte e ancora da impegnare.

È un punto valido?

Potrebbe essere. Tuttavia, oserei dire che non è solo una questione di SRP (tecnicamente parlando), è anche una questione di convenienza (funzionalmente parlando).

  • È conveniente licenziare eventi aziendali da componenti ignari delle operazioni aziendali in corso?
  • Rappresentano il posto giusto tanto quanto il momento giusto per farlo?
  • Devo consentire a questi componenti di orchestrare la mia logica aziendale attraverso notifiche come questa?
  • Potrei invalidare gli effetti collaterali causati da eventi prematuri? 2

Mi sembra che la raccolta di un evento qui sia essenzialmente la stessa cosa della registrazione

Assolutamente no. La registrazione non dovrebbe avere effetti collaterali, tuttavia, poiché hai suggerito che l'evento UserCreatedpotrebbe causare altre operazioni aziendali. Come le notifiche. 3

dice solo che tale logica dovrebbe essere incapsulata in altre classi, ed è OK per un repository chiamare queste altre classi

Non necessariamente vero. SRP non è solo una preoccupazione specifica della classe. Funziona a diversi livelli di astrazioni, come livelli, librerie e sistemi! Si tratta di coesione, di tenere insieme ciò che cambia per gli stessi motivi per mano degli stessi stakeholder . Se la creazione dell'utente ( caso d'uso ) cambia, è probabile che cambi anche il momento e le ragioni dell'evento.


1: Anche la denominazione delle cose è importante.

2: Supponiamo che abbiamo inviato UserCreateddopo _dataContext.SaveChanges();, ma l'intera transazione del database non è riuscita in seguito a causa di problemi di connessione o violazioni dei vincoli. Fai attenzione con la trasmissione prematura di eventi, perché i suoi effetti collaterali possono essere difficili da annullare (se ciò è persino possibile).

3: I processi di notifica non trattati in modo adeguato potrebbero causare l'attivazione di notifiche che non possono essere annullate / sup>


1
+1 Ottimo punto sull'intervallo di transazione. Può essere prematuro affermare che l'utente è stato creato, perché possono verificarsi rollback; e a differenza di un registro, è probabile che un'altra parte dell'app faccia qualcosa con l'evento.
Andres F.

2
Esattamente. Gli eventi denotano certezza. È successo qualcosa ma è finita.
Laiv

1
@Laiv: tranne quando non lo fanno. Microsoft ha tutti i tipi di eventi con prefisso Beforeo Previewche non offrono alcuna garanzia sulla certezza.
Robert Harvey,

1
@ jpmc26: senza un'alternativa, il tuo suggerimento non è utile.
Robert Harvey,

1
@ jpmc26: Quindi la tua risposta è "passare a un ecosistema di sviluppo completamente diverso con un insieme completamente diverso di strumenti e caratteristiche prestazionali". Chiamami al contrario, ma immagino che ciò sia impossibile per la stragrande maggioranza degli sforzi di sviluppo.
Robert Harvey,

1

No questo non viola l'SRP.

Molti sembrano pensare che il Principio della singola responsabilità significhi che una funzione dovrebbe fare solo "una cosa", e quindi rimanere coinvolti nella discussione su ciò che costituisce "una cosa".

Ma non è questo il significato del principio. Si tratta di preoccupazioni a livello aziendale. Una classe non dovrebbe implementare molteplici preoccupazioni o requisiti che possono cambiare in modo indipendente a livello aziendale. Diciamo che una classe memorizza l'utente e invia un messaggio di benvenuto codificato via e-mail. Indipendente multiplo preoccupazioni potrebbero far cambiare i requisiti di tale classe. Il designer potrebbe richiedere la modifica del foglio HTML / di stile della posta. L'esperto delle comunicazioni potrebbe richiedere la modifica della formulazione della posta. E l'esperto di UX potrebbe decidere che la posta dovrebbe essere effettivamente inviata in un punto diverso del flusso di onboarding. Quindi la classe è soggetta a più modifiche ai requisiti da fonti indipendenti. Questo viola l'SRP.

Ma l'attivazione di un evento non viola l'SRP, poiché l'evento dipende solo dal salvataggio dell'utente e non da altre preoccupazioni. Gli eventi sono in realtà un modo davvero piacevole per difendere l'SRP, dato che puoi avere l'email innescata dal salvataggio senza che il repository sia influenzato dalla, o addirittura conoscendo, la posta.


1

Non preoccuparti del principio della singola responsabilità. Non ti aiuterà a prendere una buona decisione qui perché puoi scegliere soggettivamente un concetto particolare come "responsabilità". Si potrebbe dire che la responsabilità della classe è la gestione della persistenza dei dati nel database, oppure si può dire che la sua responsabilità è di eseguire tutto il lavoro relativo alla creazione di un utente. Questi sono solo diversi livelli del comportamento dell'applicazione, ed entrambi sono valide espressioni concettuali di una "singola responsabilità". Quindi questo principio non è utile per risolvere il tuo problema.

Il principio più utile da applicare in questo caso è il principio della minima sorpresa . Facciamo quindi la domanda: è sorprendente che un repository con il ruolo principale di dati persistenti su un database invii anche e-mail?

Sì, è davvero sorprendente. Si tratta di due sistemi esterni completamente separati e il nome SaveChangesnon implica anche l'invio di notifiche. Il fatto che lo delegi a un evento rende il comportamento ancora più sorprendente, dal momento che qualcuno che legge il codice non può più facilmente vedere quali comportamenti aggiuntivi vengono invocati. La indiretta danneggia la leggibilità. A volte, i vantaggi valgono i costi di leggibilità, ma non quando si invoca automaticamente un sistema esterno aggiuntivo che ha effetti osservabili per gli utenti finali. (La registrazione può essere esclusa qui poiché il suo effetto è essenzialmente la tenuta dei registri ai fini del debug. Gli utenti finali non consumano il registro, quindi non vi è alcun danno nella registrazione sempre.) Ancora peggio, ciò riduce la flessibilità nella tempi di inviare l'e-mail, rendendo impossibile l'interlacciamento di altre operazioni tra il salvataggio e la notifica.

Se il tuo codice in genere deve inviare una notifica quando un utente viene creato correttamente, puoi creare un metodo che lo faccia:

public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
    repo.Add(user);
    repo.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

Ma se questo aggiunge valore dipende dalle specifiche dell'applicazione.


In realtà scoraggerei SaveChangesaffatto l'esistenza del metodo. Questo metodo presumibilmente eseguirà il commit di una transazione del database, ma altri repository potrebbero aver modificato il database nella stessa transazione . Il fatto che li commetta tutti è di nuovo sorprendente, poiché SaveChangesè specificamente legato a questa istanza del repository utente.

Il modello più semplice per la gestione di una transazione di database è un usingblocco esterno :

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    context.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

Questo dà al programmatore il controllo esplicito su quando vengono salvate le modifiche per tutti i repository, impone al codice di documentare esplicitamente la sequenza di eventi che devono verificarsi prima di un commit, garantisce un rollback in caso di errore (supponendo che abbia DataContext.Disposeun rollback) ed evita di essere nascosto connessioni tra classi stateful.

Preferirei anche non inviare l'e-mail direttamente nella richiesta. Sarebbe più solido registrare la necessità di una notifica in una coda. Ciò consentirebbe una migliore gestione degli errori. In particolare, se si verifica un errore durante l'invio dell'e-mail, può essere riprovato più tardi senza interrompere il salvataggio dell'utente ed evita il caso in cui l'utente viene creato ma viene restituito un errore dal sito.

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    _emailNotificationQueue.AddUserCreateNotification(user);
    _emailNotificationQueue.Commit();
    context.SaveChanges();
}

È meglio impegnare prima la coda di notifica poiché il consumatore della coda può verificare che l'utente esista prima di inviare l'e-mail, nel caso in cui la context.SaveChanges()chiamata fallisca. (Altrimenti, avrai bisogno di una vera e propria strategia di commit in due fasi per evitare gli heisenbugs.)


La linea di fondo deve essere pratica. Pensa in realtà attraverso le conseguenze (sia in termini di rischio che di beneficio) della scrittura del codice in un modo particolare. Trovo che il "principio della singola responsabilità" non mi aiuti molto spesso a farlo, mentre il "principio della minima sorpresa" spesso mi aiuta a entrare nella testa di un altro sviluppatore (per così dire) e pensare a cosa potrebbe accadere.


4
è sorprendente che un repository con il ruolo principale di dati persistenti in un database invii anche e-mail - penso che tu abbia perso il punto della mia domanda. Il mio repository non sta inviando e-mail. Solleva un evento e come questo evento viene gestito - è responsabilità del codice esterno.
Andre Borges,

4
Fondamentalmente stai argomentando "non usare gli eventi".
Robert Harvey,

3
[scrollata di spalle] Gli eventi sono fondamentali per la maggior parte dei framework dell'interfaccia utente. Elimina gli eventi e questi framework non funzionano affatto.
Robert Harvey,

2
@ jpmc26: si chiama ASP.NET Webforms. Fa schifo
Robert Harvey,

2
My repository is not sending emails. It just raises an eventcausa effetto. Il repository avvia il processo di notifica.
Laiv

0

Attualmente SaveChangesfa due cose: salva le modifiche e registra che lo fa. Ora vuoi aggiungere un'altra cosa: inviare notifiche e-mail.

Hai avuto l'idea intelligente di aggiungere un evento ad esso, ma questo è stato criticato per aver violato il principio di responsabilità singola (SRP), senza notare che era già stato violato.

Per ottenere una soluzione SRP pura, innescare prima l'evento, quindi chiamare tutti gli hook per quell'evento, di cui ora ce ne sono tre: salvataggio, registrazione e infine invio di e-mail.

O attivi prima l'evento o devi aggiungerlo a SaveChanges. La tua soluzione è un ibrido tra i due. Non affronta la violazione esistente mentre incoraggia a impedirle di aumentare oltre tre cose. Il refactoring del codice esistente per conformarsi a SRP potrebbe richiedere più lavoro di quanto sia strettamente necessario. Spetta al tuo progetto quanto vogliono portare SRP.


-1

Il codice violava già l'SRP: la stessa classe era responsabile della comunicazione con il contesto e della registrazione dei dati.

È sufficiente aggiornarlo per avere 3 responsabilità.

Un modo per ridurre le cose a 1 responsabilità sarebbe quello di sottrarre _userRepository; trasformalo in un emittente di comandi.

Ha una serie di comandi, oltre a una serie di ascoltatori. Riceve comandi e li trasmette ai suoi ascoltatori. Forse quegli ascoltatori sono ordinati e forse possono persino dire che il comando non è riuscito (che a sua volta viene trasmesso agli ascoltatori che erano già stati avvisati).

Ora, la maggior parte dei comandi può avere solo 1 listener (il contesto dei dati). SaveChanges, prima delle modifiche, ha 2: il contesto dei dati e quindi il logger.

La modifica aggiunge quindi un altro listener per salvare le modifiche, che consiste nel generare eventi creati da nuovi utenti nel servizio eventi.

Ci sono alcuni vantaggi in questo. Ora puoi rimuovere, aggiornare o replicare il codice di registrazione senza preoccuparti del resto del codice. È possibile aggiungere più trigger alle modifiche di salvataggio per più cose che ne hanno bisogno.

Tutto questo viene deciso quando _userRepositoryviene creato e cablato (o, forse, quelle funzionalità extra vengono aggiunte / rimosse al volo; essere in grado di aggiungere / migliorare la registrazione mentre l'esecuzione dell'applicazione potrebbe essere utile).

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.