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 SaveChanges
non 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 SaveChanges
affatto 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 using
blocco 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.Dispose
un 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.