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 SaveChanges
wokół systemu, postanowiłem zaktualizować SaveChanges
tak:
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 SaveChanges
naruszyła zasadę pojedynczej odpowiedzialności, co SaveChanges
powinno 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.
źródło
Odpowiedzi:
Tak, może być ważnym wymogiem posiadania repozytorium, które uruchamia określone zdarzenia w przypadku niektórych działań, takich jak
Add
lubSaveChanges
- 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.
Możesz nazwać klasę proxy a
NotifyingRepository
lubObservableRepository
jeś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
_userRepository
obiekt nowejEventFiringUserRepo
klasy. 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.
źródło
SaveChanges()
rzeczywistości nie tworzy rekordu bazy danych i może zostać wycofany. Wygląda na to, że musisz zastąpićAcceptAllChanges
lub subskrybować zdarzenie TransactionCompleted.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 _userRepository
naObservableUserCollection _userRepository
. To, czy jest to najlepszy sposób działania, zależy od aplikacji. Ale choć niewątpliwie czyni to_userRepository
znacznie mniej lekkim, moim skromnym zdaniem nie narusza SRP.źródło
ObservableCollection
w tym przypadku polega na tym, że wyzwala równoważne zdarzenie nie przy wywołaniuSaveChanges
, ale przy wywołaniuAdd
, 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ą.ObservableCollection<>
iList<>
dla porównania i kontekstu. Nie chciałem zalecać używania rzeczywistych klas ani do implementacji wewnętrznej, ani do interfejsu zewnętrznego.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.
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”
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.
źródło
Add
. Jego semantyka oznacza, że wszyscy dodani użytkownicy są nowymi użytkownikami. Połącz wszystkie argumenty przekazaneAdd
przed wywołaniemSave
- a uzyskasz wszystkich nowych użytkowników.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ć.
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ą.
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();
źródło
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”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.
źródło
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.
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
UserCreated
ma inną konotację niżUserStored
lubUserAdded
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.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).
Absolutnie nie. Rejestrowanie nie ma jednak żadnych skutków ubocznych, ponieważ sugerujesz, że zdarzenie
UserCreated
może spowodować inne operacje biznesowe. Podobnie jak powiadomienia. 3)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
UserCreated
póź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ć>
źródło
Before
lub,Preview
które nie dają żadnej gwarancji co do pewności.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.
źródło
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:
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 :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.
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ć.
źródło
My repository is not sending emails. It just raises an event
przyczyna-skutek. Repozytorium uruchamia proces powiadamiania.Obecnie
SaveChanges
robi 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.źródło
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
_userRepository
zostanie 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).źródło