Zastosowanie zasady pojedynczej odpowiedzialności

40

Ostatnio natrafiłem na pozornie trywialny problem architektoniczny. Miałem proste repozytorium w moim kodzie, które zostało tak nazwane (kod jest w C #):

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

SaveChanges był prostym opakowaniem, które zatwierdza zmiany w bazie danych:

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

Następnie po pewnym czasie musiałem zaimplementować nową logikę, która będzie wysyłać powiadomienia e-mail za każdym razem, gdy użytkownik zostanie utworzony w systemie. Ponieważ było wiele połączeń do _userRepository.Add()i SaveChangeswokół systemu, postanowiłem zaktualizować SaveChangestak:

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

W ten sposób zewnętrzny kod może zasubskrybować UserCreatedEvent i obsłużyć potrzebną logikę biznesową, która wysyła powiadomienia.

Wskazano mi jednak, że moja modyfikacja SaveChangesnaruszyła zasadę pojedynczej odpowiedzialności, co SaveChangespowinno po prostu ratować, a nie odpalać jakiekolwiek zdarzenia.

Czy to ważny punkt? Wydaje mi się, że wywołanie tutaj zdarzenia jest w zasadzie tym samym, co rejestrowanie: po prostu dodaje do funkcji pewną funkcjonalność boczną. A SRP nie zabrania ci używania rejestrowania lub uruchamiania zdarzeń w twoich funkcjach, po prostu mówi, że taka logika powinna być zamknięta w innych klasach, a repozytorium może wywoływać te inne klasy.

Andre Borges
źródło
22
Twoja retorta brzmi: „OK, więc jak byś to napisał, aby nie naruszył SRP, ale nadal pozwala na pojedynczy punkt modyfikacji?”
Robert Harvey
43
Moim spostrzeżeniem jest to, że zgłoszenie zdarzenia nie powoduje dodatkowej odpowiedzialności. Wręcz przeciwnie: przekazuje odpowiedzialność gdzie indziej.
Robert Harvey
Myślę, że twój współpracownik ma rację, ale twoje pytanie jest ważne i użyteczne, więc pochwalone!
Andres F.
16
Nie ma czegoś takiego jak ostateczna definicja pojedynczej odpowiedzialności. Osoba wskazująca, że ​​narusza SRP, ma rację, posługując się osobistą definicją, a ty masz rację, posługując się swoją definicją. Myślę, że twój projekt jest w porządku z zastrzeżeniem, że to wydarzenie nie jest jednorazowe, w którym inne podobne funkcje są wykonywane na różne sposoby. Konsekwencja jest o wiele bardziej istotna, na którą należy zwrócić uwagę, niż niektóre niejasne wytyczne, takie jak SRP, które doprowadziły do ​​skrajności, kończą się mnóstwem bardzo łatwych do zrozumienia klas, których nikt nie wie, jak wykonać pracę w systemie.
Dunk

Odpowiedzi:

14

Tak, może być ważnym wymogiem posiadania repozytorium, które uruchamia określone zdarzenia w przypadku niektórych działań, takich jak Addlub SaveChanges- i nie zamierzam kwestionować tego (podobnie jak niektórych innych odpowiedzi) tylko dlatego, że konkretny przykład dodawania użytkowników i wysyłania wiadomości e-mail może wyglądać nieco wymyślony. Poniżej załóżmy, że wymaganie to jest całkowicie uzasadnione w kontekście twojego systemu.

Więc tak , kodujący mechanikę zdarzeń, jak również rejestrowanie, a także oszczędności w jeden sposób niezgodny SRP . W wielu przypadkach jest to prawdopodobnie dopuszczalne naruszenie, szczególnie gdy nikt nigdy nie chce rozdzielić obowiązków związanych z utrzymaniem „zapisywania zmian” i „zgłaszania zdarzeń” do różnych zespołów / opiekunów. Ale załóżmy, że pewnego dnia ktoś chce to zrobić, czy można to rozwiązać w prosty sposób, być może poprzez umieszczenie kodu tych obaw w bibliotekach różnych klas?

Rozwiązaniem tego jest pozostawienie oryginalnego repozytorium odpowiedzialnym za dokonywanie zmian w bazie danych, nic więcej, i utworzenie repozytorium proxy, które ma dokładnie taki sam interfejs publiczny, ponowne użycie oryginalnego repozytorium i dodanie dodatkowej mechaniki zdarzeń do metod.

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

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

Możesz nazwać klasę proxy a NotifyingRepositorylub ObservableRepositoryjeśli chcesz, zgodnie z wysoko głosowaną odpowiedzią @ Peter (która tak naprawdę nie mówi, jak rozwiązać naruszenie SRP, tylko mówiąc, że naruszenie jest w porządku).

Zarówno nowa, jak i stara klasa repozytorium powinny pochodzić ze wspólnego interfejsu, jak pokazano w klasycznym opisie wzoru proxy .

Następnie w oryginalnym kodzie zainicjuj _userRepositoryobiekt nowej EventFiringUserRepoklasy. W ten sposób oryginalne repozytorium jest oddzielone od mechaniki zdarzeń. W razie potrzeby możesz mieć repozytorium odpalające zdarzenia i repozytorium oryginalne obok siebie i pozwolić dzwoniącym zdecydować, czy użyją tego pierwszego, czy drugiego.

Aby rozwiązać jeden problem wymieniony w komentarzach: czy nie prowadzi to do serwerów proxy na serwerach proxy i tak dalej? W rzeczywistości dodanie mechaniki zdarzeń tworzy podstawę do dodania dalszych wymagań typu „wysyłaj e-maile”, po prostu subskrybując zdarzenia, więc pozostając przy SRP również z tymi wymaganiami, bez żadnych dodatkowych serwerów proxy. Ale jedną rzeczą, którą należy tu dodać, jest sama mechanika zdarzeń.

Jeśli tego rodzaju separacja jest naprawdę warta w kontekście twojego systemu, to ty i twój recenzent musisz sam zdecydować. Prawdopodobnie nie oddzieliłbym rejestrowania od oryginalnego kodu, ani nie użyłbym innego proxy, nie dodając rejestratora do zdarzenia nasłuchującego, chociaż byłoby to możliwe.

Doktor Brown
źródło
3
Oprócz tej odpowiedzi. Istnieją alternatywy dla serwerów proxy, takie jak AOP .
Laiv
1
Myślę, że nie rozumiesz, że nie jest tak, że zgłoszenie zdarzenia łamie SRP, że zgłoszenie zdarzenia tylko dla „nowych” użytkowników wymaga, aby repo było odpowiedzialne za to, co stanowi „nowego” użytkownika, a nie „nowo dodanego do mnie” „użytkownik
Ewan
@Ewan: proszę ponownie przeczytać pytanie. Chodziło o miejsce w kodzie, które wykonuje pewne działania, które muszą być połączone z innymi działaniami poza zakresem odpowiedzialności tego obiektu. A umieszczenie akcji i organizowanie wydarzeń w jednym miejscu zostało zakwestionowane przez recenzenta jako złamanie SRP. Przykład „ratowania nowych użytkowników” służy wyłącznie do celów demonstracyjnych, jeśli chcesz, nazwij go wymyślonym, ale nie o to chodzi w IMHO.
Doc Brown
2
To jest najlepsza / poprawna odpowiedź IMO. Nie tylko utrzymuje SRP, ale także zachowuje zasadę Open / Closed. I pomyśl o wszystkich zautomatyzowanych testach, które zmiany w klasie mogą przerwać. Modyfikacja istniejących testów po dodaniu nowej funkcjonalności to duży zapach.
Keith Payne
1
Jak działa to rozwiązanie, jeśli trwa transakcja? Gdy tak się dzieje, w SaveChanges()rzeczywistości nie tworzy rekordu bazy danych i może zostać wycofany. Wygląda na to, że musisz zastąpić AcceptAllChangeslub subskrybować zdarzenie TransactionCompleted.
John Wu
29

Wysłanie powiadomienia o zmianie trwałego magazynu danych wydaje się rozsądnym rozwiązaniem podczas zapisywania.

Oczywiście nie powinieneś traktować Add jako specjalnego przypadku - będziesz musiał także uruchamiać zdarzenia dla Modify i Delete. To specjalne traktowanie przypadku „Dodaj”, które pachnie, zmusza czytelnika do wyjaśnienia, dlaczego pachnie, i ostatecznie prowadzi niektórych czytelników kodu do wniosku, że musi naruszać SRP.

Repozytorium „powiadamiające”, które można przeszukiwać, zmieniać i uruchamiać zdarzenia przy zmianach, jest całkowicie normalnym obiektem. Możesz spodziewać się ich wielu odmian w prawie każdym projekcie o przyzwoitej wielkości.


Ale czy repozytorium „powiadamiające” jest rzeczywiście tym, czego potrzebujesz? Wspomniałeś o C #: Wiele osób zgodzi się, że użycie System.Collections.ObjectModel.ObservableCollection<>zamiast tego, System.Collections.Generic.List<>gdy jest to wszystko, czego potrzebujesz, to wszelkiego rodzaju złe i złe, ale niewielu od razu wskazuje na SRP.

To, co teraz robisz, polega na zamianie UserList _userRepositoryna ObservableUserCollection _userRepository. To, czy jest to najlepszy sposób działania, zależy od aplikacji. Ale choć niewątpliwie czyni to _userRepositoryznacznie mniej lekkim, moim skromnym zdaniem nie narusza SRP.

Piotr
źródło
Problem z używaniem ObservableCollectionw tym przypadku polega na tym, że wyzwala równoważne zdarzenie nie przy wywołaniu SaveChanges, ale przy wywołaniu Add, co prowadziłoby do zupełnie innego zachowania niż pokazane w przykładzie. Zobacz moją odpowiedź, jak zachować oryginalne repo lekkie i nadal trzymać się SRP, zachowując semantykę nietkniętą.
Doc Brown
@DocBrown Przywołałem znane klasy ObservableCollection<>i List<>dla porównania i kontekstu. Nie chciałem zalecać używania rzeczywistych klas ani do implementacji wewnętrznej, ani do interfejsu zewnętrznego.
Peter
Ok, ale nawet gdyby PO dodał zdarzenia do „Modyfikuj” i „Usuń” (które, jak sądzę, pominięto, by zachować zwięzłość pytania, ze względu na prostotę), myślę, że recenzent mógłby łatwo dojść do wniosku naruszenie SRP. Jest to prawdopodobnie akceptowalny, ale nie można go rozwiązać w razie potrzeby.
Doc Brown
16

Tak, jest to naruszenie zasady pojedynczej odpowiedzialności i ważnego punktu.

Lepszym rozwiązaniem byłoby oddzielny proces pobierania „nowych użytkowników” z repozytorium i wysyłania wiadomości e-mail. Śledzenie, którzy użytkownicy otrzymali wiadomość e-mail, awarie, ponowne wysłania itp. Itp.

W ten sposób możesz obsługiwać błędy, awarie i tym podobne, a także unikać przechwytywania przez repozytorium wszystkich wymagań, które zakładają, że zdarzenia zdarzają się „kiedy coś jest przypisane do bazy danych”.

Repozytorium nie wie, że dodawany użytkownik jest nowym użytkownikiem. Jego zadaniem jest po prostu przechowywanie użytkownika.

Prawdopodobnie warto rozwinąć poniższe komentarze.

Repozytorium nie wie, że dodawany użytkownik jest nowym użytkownikiem - tak, ma metodę o nazwie Add. Jego semantyka oznacza, że ​​wszyscy dodani użytkownicy są nowymi użytkownikami. Połącz wszystkie argumenty przekazane do Add przed wywołaniem Save - i uzyskasz wszystkich nowych użytkowników

Błędny. Konflujesz „Dodano do repozytorium” i „Nowy”.

„Dodane do repozytorium” oznacza tylko to, co mówi. Mogę dodawać, usuwać i ponownie dodawać użytkowników do różnych repozytoriów.

„Nowy” to stan użytkownika zdefiniowany przez reguły biznesowe.

Obecnie regułą biznesową może być „Nowa == właśnie dodana do repozytorium”, ale to nie znaczy, że znajomość i stosowanie tej reguły nie jest osobnym obowiązkiem.

Musisz być ostrożny, aby uniknąć tego rodzaju myślenia skoncentrowanego na bazie danych. Będziesz mieć zaawansowane procesy przypadków, które dodadzą nie-nowych użytkowników do repozytorium, a kiedy wyślesz do nich e-maile, cała firma powie „Oczywiście, że nie są to„ nowi ”użytkownicy! Rzeczywista reguła to X”

W tej odpowiedzi IMHO całkiem nie rozumie: repo to dokładnie jedno centralne miejsce w kodzie, które wie, kiedy dodawani są nowi użytkownicy

Błędny. Z powyższych powodów plus nie jest to centralna lokalizacja, chyba że faktycznie uwzględnisz kod wysyłania wiadomości e-mail w klasie, a nie tylko wydarzenie.

Będziesz mieć aplikacje, które korzystają z klasy repozytorium, ale nie mają kodu do wysłania wiadomości e-mail. Gdy dodasz użytkowników do tych aplikacji, wiadomość e-mail nie zostanie wysłana.

Ewan
źródło
11
Repozytorium nie wie, że dodany użytkownik jest nowym użytkownikiem - tak, ma metodę o nazwie Add. Jego semantyka oznacza, że ​​wszyscy dodani użytkownicy są nowymi użytkownikami. Połącz wszystkie argumenty przekazane Addprzed wywołaniem Save- a uzyskasz wszystkich nowych użytkowników.
Andre Borges
Podoba mi się ta sugestia. Jednak pragmatyzm przeważa nad czystością. W zależności od okoliczności dodanie całkowicie nowej warstwy architektonicznej do istniejącej aplikacji może być trudne do uzasadnienia, jeśli wystarczy dosłownie wysłać pojedynczy e-mail po dodaniu użytkownika.
Alexander
Ale wydarzenie nie mówi, że użytkownik został dodany. Mówi, że użytkownik został utworzony. Jeśli weźmiemy pod uwagę prawidłowe nazywanie rzeczy i zgadzamy się z semantycznymi różnicami między dodawaniem a tworzeniem, to zdarzenie we fragmencie ma niepoprawną nazwę lub jest źle umieszczone. Nie sądzę, żeby recenzent miał coś przeciwko powiadamianiu repozytoriów. Prawdopodobnie chodziło o rodzaj zdarzenia i jego skutki uboczne.
Laiv
7
@Andre Nowy w repozytorium, ale niekoniecznie „nowy” w sensie biznesowym. to połączenie tych dwóch pomysłów, które od pierwszego spojrzenia ukrywa dodatkową odpowiedzialność. Mogę zaimportować tonę starych użytkowników do mojego nowego repozytorium lub usunąć i ponownie dodać użytkownika itp. Będą reguły biznesowe dotyczące tego, co „nowy użytkownik” wykracza poza ”zostało dodane do dB”
Ewan
1
Uwaga moderatora: Twoja odpowiedź nie jest wywiadem dziennikarskim. Jeśli masz zmiany, włącz je naturalnie do swojej odpowiedzi bez tworzenia całego efektu „najświeższych wiadomości”. Nie jesteśmy forum dyskusyjnym.
Robert Harvey
7

Czy to ważny punkt?

Tak, chociaż zależy to w dużej mierze od struktury kodu. Nie mam pełnego kontekstu, więc postaram się ogólnie mówić.

Wydaje mi się, że wywołanie tutaj zdarzenia jest w zasadzie tym samym, co rejestrowanie: po prostu dodaje do funkcji pewną funkcjonalność boczną.

To absolutnie nie jest. Rejestrowanie nie jest częścią procesu biznesowego, można je wyłączyć, nie powinno powodować skutków ubocznych (biznesowych) i nie powinno w żaden sposób wpływać na stan i zdrowie aplikacji, nawet jeśli z jakiegoś powodu nie można się zalogować cokolwiek już. Porównaj to teraz z dodaną logiką.

A SRP nie zabrania ci używania rejestrowania lub uruchamiania zdarzeń w twoich funkcjach, po prostu mówi, że taka logika powinna być zamknięta w innych klasach, a repozytorium może wywoływać te inne klasy.

SRP działa w tandemie z ISP (S i I w SOLID). W efekcie powstaje wiele klas i metod, które wykonują bardzo konkretne rzeczy i nic więcej. Są bardzo skoncentrowane, bardzo łatwe do aktualizacji lub wymiany i ogólnie łatwe (er) do testowania. Oczywiście w praktyce będziesz mieć także kilka większych klas, które zajmą się aranżacją: będą miały wiele zależności i nie będą koncentrować się na działaniach rozproszonych, ale na działaniach biznesowych, które mogą wymagać wielu kroków. Tak długo, jak kontekst biznesowy jest jasny, można je również nazwać pojedynczą odpowiedzialnością, ale jak prawidłowo powiedziałeś, w miarę wzrostu kodu, możesz chcieć wyodrębnić niektóre z nich w nowe klasy / interfejsy.

Wróćmy teraz do twojego konkretnego przykładu. Jeśli absolutnie musisz wysłać powiadomienie za każdym razem, gdy użytkownik jest tworzony, a może nawet wykonać inne, bardziej wyspecjalizowane działania, możesz utworzyć osobną usługę, która uwzględnia to wymaganie, coś w rodzaju UserCreationService, która ujawnia jedną metodę Add(user), która obsługuje zarówno pamięć masową (wywołanie do repozytorium) i powiadomienie jako pojedyncze działanie biznesowe. Lub zrób to w swoim oryginalnym fragmencie po_userRepository.SaveChanges();

asynchronizacja
źródło
2
Rejestrowanie nie jest częścią procesu biznesowego - w jaki sposób jest to istotne w kontekście SRP? Jeśli celem mojego wydarzenia byłoby wysłanie nowych danych użytkownika do Google Analytics - wówczas wyłączenie go miałoby taki sam efekt biznesowy jak wyłączenie rejestrowania: nie jest to krytyczne, ale dość denerwujące. Jaka jest ogólna zasada dodawania / nie dodawania nowej logiki do funkcji? „Czy wyłączenie go spowoduje poważne skutki uboczne dla biznesu?”
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 . Co jeśli strzelasz przedwcześnie, powodując fałszywe „wiadomości”. Co jeśli analitycy biorą pod uwagę „użytkowników”, którzy nie zostali ostatecznie stworzeni z powodu błędów w transakcji DB? Co jeśli firma podejmuje decyzje na podstawie fałszywych przesłanek, popartych nieprecyzyjnymi danymi? Jesteś zbyt skoncentrowany na technicznej stronie problemu. „Czasami nie widać drewna na drzewa”
Laiv
@Laiv, robisz słuszny punkt, ale nie o to chodzi w moim pytaniu ani w tej odpowiedzi. Pytanie brzmi, czy jest to prawidłowe rozwiązanie w kontekście SRP, więc załóżmy, że nie ma błędów transakcji DB.
Andre Borges,
Zasadniczo prosisz mnie, żebym powiedział ci, co chcesz usłyszeć. Po prostu daję ci zasięg. Szerszy zakres możliwości decydowania o tym, czy SRP ma znaczenie, czy nie, ponieważ SRP jest bezużyteczny bez odpowiedniego kontekstu. IMO sposób, w jaki podchodzisz do koncernu, jest nieprawidłowy, ponieważ koncentrujesz się tylko na rozwiązaniu technicznym. Powinieneś nadać wystarczające znaczenie całemu kontekstowi. I tak, DB może zawieść. Jest szansa, że ​​tak się stanie i nie należy tego pomijać, ponieważ, jak wiadomo, rzeczy się zdarzają i te rzeczy mogą zmienić zdanie na temat wątpliwości co do SRP lub innych dobrych praktyk.
Laiv
1
To powiedziawszy, pamiętajcie, że zasady nie są regułami zapisanymi w kamieniu. Są przepuszczalne (adaptacyjne). Jak widać, są one otwarte na interpretację. Twój recenzent ma interpretację, a ty masz inną. Spróbuj zobaczyć to, co widzisz, rozwiń jego wątpliwości i wątpliwości lub pozwól mu rozwiązać twoje. Nie znajdziesz tutaj „właściwej” odpowiedzi. Prawidłowa odpowiedź należy do ciebie i twojego recenzenta, najpierw pytając o wymagania (funkcjonalne i niefunkcjonalne) projektu.
Laiv
4

SRP dotyczy teoretycznie ludzi , jak wyjaśnia wujek Bob w swoim artykule Zasada pojedynczej odpowiedzialności . Dziękujemy Robertowi Harveyowi za udostępnienie go w komentarzu.

Prawidłowe pytanie brzmi:

Który „interesariusz” dodał wymóg „wysyłania e-maili”?

Jeśli ten interesariusz jest również odpowiedzialny za utrwalenie danych (mało prawdopodobne, ale możliwe), nie narusza to SRP. W przeciwnym razie tak jest.

użytkownik949300
źródło
2
Ciekawe - nigdy nie słyszałem o tej interpretacji SRP. Czy masz jakieś wskazówki na temat dodatkowych informacji / literatury na temat tej interpretacji?
śleske
2
@sleske: Od samego wuja Boba : „I to dotyczy sedna zasady pojedynczej odpowiedzialności. Zasada ta dotyczy ludzi. Pisząc moduł oprogramowania, musisz się upewnić, że kiedy zostaną poproszone o zmiany, zmiany te mogą powstać od pojedynczej osoby, a raczej pojedynczej ściśle powiązanej grupy osób reprezentujących jedną ściśle określoną funkcję biznesową ”.
Robert Harvey
Dzięki Robert. IMO, nazwa „Zasada pojedynczej odpowiedzialności” jest okropna, ponieważ brzmi prosto, ale zbyt mało osób śledzi, jakie jest zamierzone znaczenie „odpowiedzialności”. To tak, jakby OOP zmutował z wielu swoich oryginalnych koncepcji i jest obecnie dość bez znaczenia.
user949300
1
Tak. Tak stało się z terminem REST. Nawet Roy Fielding mówi, że ludzie źle go używają.
Robert Harvey
Chociaż cytowanie jest powiązane, myślę, że ta odpowiedź pomija fakt, że wymóg „wysyłanie wiadomości e-mail” nie jest żadnym z bezpośrednich wymagań, o które chodzi w pytaniu o naruszenie SRP. Jednak mówiąc „Który„ interesariusz ”dodał wymóg„ podniesienia zdarzenia ” , odpowiedź ta byłaby bardziej związana z rzeczywistym pytaniem. Zmodyfikowałem trochę swoją odpowiedź, aby była bardziej przejrzysta.
Doc Brown
2

Chociaż technicznie nie ma nic złego w repozytoriach powiadamiających o zdarzeniach, proponuję spojrzeć na to z funkcjonalnego punktu widzenia, gdzie jego wygoda budzi pewne obawy.

Tworzenie użytkownika, decydowanie, kim jest nowy użytkownik i jego trwałość to 3 różne rzeczy .

Przesłanka moja

Przed podjęciem decyzji, czy repozytorium jest właściwym miejscem do powiadamiania o zdarzeniach biznesowych (niezależnie od SRP), należy wziąć pod uwagę poprzednią przesłankę. Pamiętaj, że powiedziałem wydarzenie biznesowe, ponieważ dla mnie UserCreatedma inną konotację niż UserStoredlub UserAdded 1 . Uważałbym również, że każde z tych wydarzeń jest adresowane do różnych odbiorców.

Z jednej strony tworzenie użytkowników jest regułą specyficzną dla firmy, która może, ale nie musi wymagać trwałości. Może to obejmować więcej operacji biznesowych, obejmujących więcej operacji na bazie danych / sieci. Operacje warstwa trwałości jest nieświadoma. Warstwa trwałości nie ma wystarczającego kontekstu, aby zdecydować, czy przypadek użycia zakończył się pomyślnie, czy nie.

Z drugiej strony niekoniecznie jest prawdą, że _dataContext.SaveChanges();użytkownik z powodzeniem przetrwał. Będzie to zależeć od zakresu transakcji bazy danych. Na przykład może to być prawda w przypadku baz danych takich jak MongoDB, których transakcje są atomowe, ale nie mogło tak być w przypadku tradycyjnych RDBMS wdrażających transakcje ACID, w których może być więcej transakcji, które jeszcze nie zostały zatwierdzone.

Czy to ważny punkt?

Mogłoby być. Jednak odważyłbym się powiedzieć, że nie jest to tylko kwestia SRP (technicznie rzecz biorąc), to także kwestia wygody (funkcjonalnie rzecz biorąc).

  • Czy wygodnie jest wyzwalać zdarzenia biznesowe z komponentów nieświadomych trwających operacji biznesowych?
  • Czy reprezentują właściwe miejsce, a także odpowiedni moment, aby to zrobić?
  • Czy powinienem pozwolić tym komponentom na uporządkowanie mojej logiki biznesowej za pomocą takich powiadomień?
  • Czy mogę unieważnić działania niepożądane spowodowane przedwczesnymi zdarzeniami? 2)

Wydaje mi się, że wywołanie tutaj zdarzenia jest w zasadzie tym samym, co logowanie

Absolutnie nie. Rejestrowanie nie ma jednak żadnych skutków ubocznych, ponieważ sugerujesz, że zdarzenie UserCreatedmoże spowodować inne operacje biznesowe. Podobnie jak powiadomienia. 3)

po prostu mówi, że taka logika powinna być zamknięta w innych klasach, a repozytorium może wywoływać te inne klasy

Niekoniecznie prawda. SRP nie dotyczy wyłącznie klasy. Działa na różnych poziomach abstrakcji, takich jak warstwy, biblioteki i systemy! Chodzi o spójność, o utrzymanie razem zmian, które zmieniają się z tych samych powodów przez te same zainteresowane strony . Jeśli tworzenie użytkownika ( przypadek użycia ) ulegnie zmianie, prawdopodobnie zmieni się również moment i przyczyny zdarzenia.


1: Właściwe nazywanie rzeczy również ma znaczenie.

2: Powiedzmy, że wysłaliśmy UserCreatedpóźniej _dataContext.SaveChanges();, ale cała transakcja bazy danych nie powiodła się później z powodu problemów z połączeniem lub naruszenia ograniczeń. Zachowaj ostrożność przy przedwczesnym nadawaniu wydarzeń, ponieważ jego skutki uboczne mogą być trudne do cofnięcia (jeśli to w ogóle możliwe).

3: Procesy powiadomień, które nie są odpowiednio traktowane, mogą powodować wysyłanie powiadomień, których nie można cofnąć / wyłączyć>

Laiv
źródło
1
+1 Bardzo dobra uwaga na temat zakresu transakcji. Twierdzenie, że użytkownik został utworzony, może być przedwczesne, ponieważ może dojść do wycofania; i inaczej niż w przypadku dziennika, prawdopodobnie inna część aplikacji ma coś wspólnego ze zdarzeniem.
Andres F.
2
Dokładnie. Wydarzenia oznaczają pewność. Coś się stało, ale to koniec.
Laiv
1
@Laiv: Z wyjątkiem sytuacji, gdy nie. Microsoft ma wszelkiego rodzaju zdarzenia z prefiksem Beforelub, Previewktóre nie dają żadnej gwarancji co do pewności.
Robert Harvey
1
@ jpmc26: Bez alternatywy twoja sugestia nie jest pomocna.
Robert Harvey
1
@ jpmc26: Tak więc twoją odpowiedzią jest „zmiana na zupełnie inny ekosystem programistyczny z całkowicie innym zestawem narzędzi i charakterystyk wydajności”. Zadzwoń do mnie przeciwnie, ale wyobrażam sobie, że jest to niewykonalne dla zdecydowanej większości wysiłków na rzecz rozwoju.
Robert Harvey
1

Nie, to nie narusza SRP.

Wielu uważa, że ​​Zasada Jednej Odpowiedzialności oznacza, że ​​funkcja powinna robić tylko „jedną rzecz”, a następnie zostać wciągniętym w dyskusję na temat tego, co stanowi „jedną rzecz”.

Ale nie to oznacza ta zasada. Chodzi o obawy na poziomie biznesowym. Klasa nie powinna implementować wielu problemów lub wymagań, które mogą się zmieniać niezależnie na poziomie biznesowym. Powiedzmy, że klasa zarówno przechowuje użytkownika, jak i wysyła zakodowaną wiadomość powitalną za pośrednictwem poczty elektronicznej. Wiele niezależnych problemów może spowodować zmianę wymagań takiej klasy. Projektant może wymagać zmiany HTML w arkuszu stylów / styli. Ekspert ds. Komunikacji może wymagać zmiany treści wiadomości. I ekspert UX może zdecydować, że poczta powinna zostać faktycznie wysłana w innym punkcie procesu dołączania. Tak więc klasa podlega wielu zmianom wymagań z niezależnych źródeł. Narusza to SRP.

Jednak odpalenie zdarzenia nie narusza SRP, ponieważ zdarzenie zależy tylko od uratowania użytkownika, a nie od jakichkolwiek innych obaw. Zdarzenia są naprawdę bardzo dobrym sposobem na utrzymanie SRP, ponieważ możesz mieć wiadomość e-mail uruchamianą przez zapis bez wpływu na repozytorium - a nawet o nim wiedzącym.

JacquesB
źródło
1

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 SaveChangesnie 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 SaveChangesmetody. 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ż SaveChangesjest ściśle związany z tym wystąpieniem repozytorium użytkowników.

Najprostszym wzorem do zarządzania transakcją w bazie danych jest usingblok 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.Disposepowoduje 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ć.

jpmc26
źródło
4
czy to zaskakujące, że repozytorium, którego główną rolą jest utrwalanie danych w bazie danych, wysyła również e-maile - myślę, że nie trafiłeś w sedno mojego pytania. Moje repozytorium nie wysyła wiadomości e-mail. Po prostu wywołuje zdarzenie, a sposób obsługi tego zdarzenia - jest obowiązkiem zewnętrznego kodu.
Andre Borges
4
Zasadniczo argumentujesz „nie używaj zdarzeń”.
Robert Harvey
3
[wzruszenie ramionami] Zdarzenia są kluczowe dla większości platform interfejsu użytkownika. Wyeliminuj zdarzenia, a te ramy w ogóle nie działają.
Robert Harvey
2
@ jpmc26: Nazywa się to ASP.NET Webforms. To jest do bani.
Robert Harvey
2
My repository is not sending emails. It just raises an eventprzyczyna-skutek. Repozytorium uruchamia proces powiadamiania.
Laiv
0

Obecnie SaveChangesrobi dwie rzeczy: zapisuje zmiany i rejestruje, że to robi. Teraz chcesz dodać do niego jeszcze jedną rzecz: wysyłać powiadomienia e-mail.

Miałeś sprytny pomysł, aby dodać do niego wydarzenie, ale zostało to skrytykowane za naruszenie zasady jednolitej odpowiedzialności (SRP), nie zauważając, że zostało już naruszone.

Aby uzyskać czyste rozwiązanie SRP, najpierw uruchom zdarzenie, a następnie wywołaj wszystkie haczyki dla tego zdarzenia, z których są teraz trzy: zapisywanie, rejestrowanie i wysyłanie wiadomości e-mail.

Albo najpierw uruchomisz zdarzenie, albo musisz dodać SaveChanges. Twoje rozwiązanie jest hybrydą między nimi. Nie rozwiązuje problemu istniejącego naruszenia, ale zachęca do zapobiegania przekraczaniu trzech rzeczy. Refaktoryzacja istniejącego kodu w celu zapewnienia zgodności z SRP może wymagać więcej pracy niż jest to absolutnie konieczne. Od twojego projektu zależy, jak daleko chcą zabrać SRP.

CJ Dennis
źródło
-1

Kod już naruszył SRP - ta sama klasa była odpowiedzialna za komunikację z kontekstem danych i rejestrowanie.

Po prostu ulepszasz go do 3 obowiązków.

Jednym ze sposobów na przywrócenie rzeczy do 1 odpowiedzialności byłoby wyodrębnienie _userRepository; uczyń go nadawcą komend.

Ma zestaw poleceń i zestaw słuchaczy. Otrzymuje polecenia i rozsyła je do słuchaczy. Być może ci słuchacze są uporządkowani, a może nawet powiedzą, że polecenie nie powiodło się (co z kolei jest przekazywane słuchaczom, którzy zostali już powiadomieni).

Teraz większość poleceń może mieć tylko 1 detektor (kontekst danych). SaveChanges, przed twoimi zmianami, ma 2 - kontekst danych, a następnie rejestrator.

Twoja zmiana następnie dodaje innego detektora, aby zapisać zmiany, które mają wywoływać zdarzenia tworzone przez nowych użytkowników w usłudze zdarzeń.

Jest z tego kilka korzyści. Możesz teraz usuwać, aktualizować lub replikować kod rejestrujący bez konieczności dbania o resztę kodu. Możesz dodać więcej wyzwalaczy przy zmianach zapisu, aby uzyskać więcej potrzebnych rzeczy.

Wszystko to decyduje o tym, kiedy _userRepositoryzostanie utworzony i podłączony (lub być może te dodatkowe funkcje zostaną dodane / usunięte na bieżąco; możliwość dodawania / ulepszania rejestrowania podczas uruchamiania aplikacji może być przydatna).

Jak
źródło