Nie martw się o zasadę pojedynczej odpowiedzialności. Nie pomoże ci to w podjęciu właściwej decyzji, ponieważ możesz subiektywnie wybrać określoną koncepcję jako „odpowiedzialność”. Można powiedzieć, że odpowiedzialnością klasy jest zarządzanie trwałością danych w bazie danych, lub można powiedzieć, że jej zadaniem jest wykonanie całej pracy związanej z tworzeniem użytkownika. Są to tylko różne poziomy zachowania aplikacji i oba są prawidłowymi pojęciowymi wyrażeniami „pojedynczej odpowiedzialności”. Zasada ta jest więc pomocna w rozwiązaniu problemu.
Najbardziej przydatną zasadą do zastosowania w tym przypadku jest zasada najmniejszego zaskoczenia . Zadajmy więc pytanie: czy zaskakujące jest to, że repozytorium z podstawową rolą utrwalania danych w bazie danych również wysyła wiadomości e-mail?
Tak, to bardzo zaskakujące. Są to dwa całkowicie odrębne systemy zewnętrzne, a nazwa SaveChanges
nie oznacza również wysyłania powiadomień. Fakt, że delegujesz to na zdarzenie, czyni to zachowanie jeszcze bardziej zaskakującym, ponieważ ktoś czytający kod nie może już łatwo zobaczyć, jakie dodatkowe zachowania są wywoływane. Pośrednictwo szkodzi czytelności. Czasami korzyści są warte kosztów czytelności, ale nie wtedy, gdy automatycznie powołujesz się na dodatkowy system zewnętrzny, którego efekty są widoczne dla użytkowników końcowych. (Rejestrowanie można tutaj wykluczyć, ponieważ jego efektem jest zasadniczo prowadzenie rejestrów do celów debugowania. Użytkownicy końcowi nie zużywają dziennika, więc nie zawsze jest szkodliwe przy logowaniu.) Co gorsza, zmniejsza to elastyczność czasową wysyłania wiadomości e-mail, uniemożliwiając przeplatanie innych operacji między zapisem a powiadomieniem.
Jeśli kod zwykle wymaga wysłania powiadomienia o pomyślnym utworzeniu użytkownika, możesz utworzyć metodę, która to zrobi:
public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
repo.Add(user);
repo.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
Ale to, czy wnosi wartość dodaną, zależy od specyfiki aplikacji.
W rzeczywistości odradzałbym w ogóle istnienie tej SaveChanges
metody. Ta metoda prawdopodobnie spowoduje transakcję bazy danych, ale inne repozytoria mogły zmodyfikować bazę danych w tej samej transakcji . Fakt, że popełnia je wszystkie, znów jest zaskakujący, ponieważ SaveChanges
jest ściśle związany z tym wystąpieniem repozytorium użytkowników.
Najprostszym wzorem do zarządzania transakcją w bazie danych jest using
blok zewnętrzny :
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
context.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
Daje to programiście wyraźną kontrolę nad zapisywaniem zmian dla wszystkich repozytoriów, zmusza kod do jawnego udokumentowania sekwencji zdarzeń, które muszą wystąpić przed zatwierdzeniem, zapewnia wycofanie w przypadku błędu (zakładając, że DataContext.Dispose
powoduje wycofanie) i pozwala uniknąć ukrytego połączenia między klasami stanowymi.
Wolałbym też nie wysyłać wiadomości e-mail bezpośrednio w żądaniu. Bardziej niezawodne byłoby rejestrowanie potrzeby powiadomienia w kolejce. Pozwoliłoby to na lepszą obsługę awarii. W szczególności, jeśli wystąpi błąd podczas wysyłania wiadomości e-mail, można spróbować ponownie później bez przerywania zapisywania użytkownika i pozwala to uniknąć sytuacji, w której użytkownik jest tworzony, ale witryna zwraca błąd.
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
_emailNotificationQueue.AddUserCreateNotification(user);
_emailNotificationQueue.Commit();
context.SaveChanges();
}
Lepiej najpierw zatwierdzić kolejkę powiadomień, ponieważ konsument kolejki może sprawdzić, czy użytkownik istnieje przed wysłaniem wiadomości e-mail w przypadku context.SaveChanges()
niepowodzenia połączenia. (W przeciwnym razie będziesz potrzebować pełnej strategii zatwierdzania dwufazowego, aby uniknąć błędów typu heisenbugs).
Najważniejsze jest, aby być praktycznym. Właściwie przemyśl konsekwencje (zarówno pod względem ryzyka, jak i korzyści) pisania kodu w określony sposób. Uważam, że „zasada pojedynczej odpowiedzialności” nie bardzo często mi to pomaga, podczas gdy „zasada najmniejszego zaskoczenia” często pomaga mi dostać się do głowy innego programisty (że tak powiem) i pomyśleć o tym, co może się zdarzyć.