MVC: Czy kontroler łamie zasadę pojedynczej odpowiedzialności?

16

Zasada jednolitej odpowiedzialności stwierdza, że ​​„klasa powinna mieć jeden powód zmiany”.

We wzorze MVC zadaniem kontrolera jest mediacja między widokiem a modelem. Oferuje interfejs dla widoku do raportowania działań wykonanych przez użytkownika w GUI (np. Zezwalanie na widok do wywołania controller.specificButtonPressed()) i jest w stanie wywoływać odpowiednie metody w modelu w celu manipulowania danymi lub wywoływania operacji (np. model.doSomething()) .

To znaczy że:

  • Kontroler musi wiedzieć o GUI, aby zaoferować View odpowiedni interfejs do zgłaszania działań użytkownika.
  • Musi także wiedzieć o logice w Modelu, aby móc wywoływać odpowiednie metody w Modelu.

Oznacza to, że istnieją dwa powody do zmiany : zmiana GUI i zmiana logiki biznesowej.

Jeśli GUI ulegnie zmianie, np. Dodano nowy przycisk, Kontroler może potrzebować dodać nową metodę, aby umożliwić Widokowi zgłoszenie naciśnięcia tego przycisku przez użytkownika.

A jeśli logika biznesowa w Modelu ulegnie zmianie, Kontroler może ulec zmianie, aby wywołać prawidłowe metody w Modelu.

Dlatego Administrator ma dwa możliwe powody zmiany . Czy to łamie SRP?

Aviv Cohn
źródło
2
Kontroler to ulica dwukierunkowa, nie jest to typowe podejście odgórne lub oddolne. Nie ma możliwości wyodrębnienia jednej z jej zależności, ponieważ kontrolerem jest sama abstrakcja. Ze względu na charakter wzoru nie jest możliwe przestrzeganie tutaj SRP. W skrócie: tak, narusza SRP, ale jest to nieuniknione.
Jeroen Vannevel
1
Jaki jest sens pytania? Jeśli wszyscy odpowiemy „tak”, co wtedy? Co jeśli odpowiedź brzmi „nie”? Jaki jest prawdziwy problem, który próbujesz rozwiązać za pomocą tego pytania?
Bryan Oakley
1
„powód zmiany” nie oznacza „kodu, który się zmienia”. Jeśli stworzysz siedem liter w nazwach zmiennych dla klasy, czy ta klasa ma teraz 7 obowiązków? Nie. Jeśli masz więcej niż jedną zmienną lub więcej niż jedną funkcję, możesz również ponosić tylko jedną odpowiedzialność.
Bob

Odpowiedzi:

14

Jeśli będziesz konsekwentnie rozumować na temat SRP, zauważysz, że „pojedyncza odpowiedzialność” jest w rzeczywistości gąbczastym terminem. Nasz ludzki mózg jest w stanie w jakiś sposób odróżnić różne obowiązki, a wiele obowiązków można przekształcić w jedną „ogólną” odpowiedzialność. Wyobraź sobie na przykład, że we wspólnej czteroosobowej rodzinie jest jeden członek rodziny odpowiedzialny za zrobienie śniadania. Teraz, aby to zrobić, trzeba ugotować jajka i tostów chlebowych i oczywiście ustawić zdrową filiżankę zielonej herbaty (tak, zielona herbata jest najlepsza). W ten sposób możesz rozbić „robienie śniadania” na mniejsze części, które razem są abstrakcyjne do „robienia śniadania”. Zauważ, że każdy utwór jest również obowiązkiem, który można np. Przekazać innej osobie.

Powrót do MVC: jeśli mediacja między modelem a widokiem nie jest jedną odpowiedzialnością, lecz dwiema, to jaka byłaby kolejna warstwa abstrakcji powyżej, łącząca te dwie? Jeśli nie możesz go znaleźć, albo nie wyodrębniłeś go poprawnie, albo nie ma żadnego, co oznacza, że ​​wszystko w porządku. I wydaje mi się, że tak jest w przypadku kontrolera, obsługującego widok i model.

walenteria
źródło
1
Jako dodatkową notatkę. Jeśli masz kontroler.makeBreakfast () i controller.wakeUpFamily (), wówczas ten kontroler łamie SRP, ale nie dlatego, że jest kontrolerem, tylko dlatego, że ma więcej niż jedną odpowiedzialność.
Bob
Dzięki za odpowiedź, nie jestem pewien, czy cię obserwuję. Czy zgadzasz się, że administrator ma więcej niż jedną odpowiedzialność? Myślę, że tak, ponieważ ma dwa powody do zmiany (myślę): zmianę modelu i zmianę poglądu. Czy zgadzasz się z tym?
Aviv Cohn
1
Tak, zgadzam się, że administrator ma więcej niż jedną odpowiedzialność. Są to jednak „niższe” obowiązki i nie stanowi to problemu, ponieważ na najwyższym poziomie abstrakcji ma tylko jedną odpowiedzialność (łączenie niższych, o których wspomniałeś), a zatem nie narusza SRP.
valenterry
1
Znalezienie odpowiedniego poziomu abstrakcji jest zdecydowanie ważne. Przykład „zrób śniadanie” jest dobry - aby wypełnić jedną odpowiedzialność, często trzeba wykonać szereg zadań. Dopóki kontroler tylko koordynuje te zadania, postępuje zgodnie z SRP. Ale jeśli za dużo wie o gotowaniu jajek, robieniu tostów lub parzeniu herbaty, to naruszyłoby SRP.
Allan
Ta odpowiedź ma dla mnie sens. Dziękuję Valenterry.
J86,
9

Jeśli klasa ma „dwa możliwe powody zmiany”, to tak, narusza SRP.

Kontroler powinien zwykle być lekki i ponosić wyłączną odpowiedzialność za manipulowanie domeną / modelem w odpowiedzi na pewne zdarzenie sterowane przez GUI. Możemy uznać każdą z tych manipulacji za zasadniczo przypadki użycia lub funkcje.

Jeśli do GUI zostanie dodany nowy przycisk, kontroler powinien zmienić tylko wtedy, gdy ten nowy przycisk reprezentuje jakąś nową funkcję (tj. W przeciwieństwie do tego samego przycisku, który istniał na ekranie 1, ale jeszcze nie istniał na ekranie 2, i tak jest dodano do ekranu 2). Konieczna byłaby również odpowiednia nowa zmiana w modelu, aby obsługiwać tę nową funkcjonalność / funkcję. Kontroler nadal ma jedynie obowiązek manipulowania domeną / modelem w odpowiedzi na pewne zdarzenie sterowane przez GUI.

Jeśli logika biznesowa w modelu zmienia się z powodu naprawienia błędu i wymaga zmiany kontrolera, jest to szczególny przypadek (lub być może model narusza zasadę otwartego zamknięcia). Jeśli logika biznesowa w modelu zmieni się, aby obsługiwać nowe funkcje / funkcje, niekoniecznie ma to wpływ na kontroler - tylko wtedy, gdy kontroler musi ujawnić tę funkcję (co prawie zawsze miałoby miejsce, w przeciwnym razie dlaczego miałoby być dodane do model domeny, jeśli nie będzie używany). Tak więc w tym przypadku kontroler musi zostać zmodyfikowany, aby obsługiwał model domeny w nowy sposób w odpowiedzi na pewne zdarzenie sterowane przez GUI.

Jeśli kontroler musi się zmienić, ponieważ powiedzmy, że warstwa trwałości zostaje zmieniona z płaskiego pliku na bazę danych, to z pewnością narusza SRP. Jeśli kontroler zawsze działa na tej samej warstwie abstrakcji, może to pomóc w osiągnięciu SRP.

Jordania
źródło
4

Kontroler nie narusza SRP. Jak sam twierdzisz, jego zadaniem jest mediacja między modelami a widokiem.

To powiedziawszy, problem w twoim przykładzie polega na tym, że łączysz metody kontrolera z logiką w widoku, tj controller.specificButtonPressed. Nazwanie metod w ten sposób wiąże kontroler z GUI, nie udało się poprawnie wyodrębnić rzeczy. Administrator powinien polegać na wykonywaniu określonych czynności, tj . controller.saveDataLub controller.retrieveEntry. Dodanie nowego przycisku w GUI niekoniecznie oznacza dodanie nowej metody do kontrolera.

Naciśnięcie przycisku w widoku oznacza zrobienie czegoś, ale cokolwiek to jest, można łatwo uruchomić w dowolny inny sposób, a nawet nie poprzez widok.

Z artykułu w Wikipedii o SRP

Martin definiuje odpowiedzialność jako przyczynę zmiany i stwierdza, że ​​klasa lub moduł powinien mieć jeden i tylko jeden powód zmiany. Jako przykład rozważmy moduł, który kompiluje i drukuje raport. Taki moduł można zmienić z dwóch powodów. Po pierwsze, treść raportu może ulec zmianie. Po drugie, format raportu może ulec zmianie. Te dwie rzeczy zmieniają się z bardzo różnych przyczyn; jeden merytoryczny i jeden kosmetyczny. Zasada pojedynczej odpowiedzialności mówi, że te dwa aspekty problemu to tak naprawdę dwie odrębne obowiązki, dlatego też powinny być w osobnych klasach lub modułach. Byłoby złym pomysłem połączenie dwóch rzeczy, które zmieniają się z różnych powodów w różnych momentach.

Kontroler nie przejmuje się tym, co jest w widoku, ale gdy wywoływana jest jedna z jego metod, zapewnia on określone dane do widoku. Musi tylko wiedzieć o funkcjonalności modelu, o ile wie, że musi wywoływać metody, które będzie miał. Nic więcej nie wie.

Wiedza, że ​​obiekt ma metodę wywoływania, nie jest tym samym, co znajomość jego funkcjonalności.

Schleis
źródło
1
Powodem, dla którego myślałem, że kontroler powinien stosować takie metody, specificButtonsPressed()jest to, że przeczytałem, że widok nie powinien wiedzieć nic o funkcjonalności jego przycisków i innych elementów GUI. Nauczono mnie, że po naciśnięciu przycisku widok powinien po prostu zgłosić się do kontrolera, a kontroler powinien zdecydować „co to znaczy” (a następnie wywołać odpowiednie metody w modelu). Wywołanie widoku controller.saveData()oznacza, że ​​widok musi wiedzieć, co oznacza naciśnięcie tego przycisku, oprócz faktu, że został naciśnięty.
Aviv Cohn
1
Jeśli porzucę ideę całkowitego oddzielenia naciśnięcia przycisku od jego znaczenia (co powoduje, że kontroler ma takie metody specificButtonPressed()), rzeczywiście kontroler nie byłby tak związany z GUI. Czy powinienem porzucić specificButtonPressed()metody? Czy myślę, że przewaga tych metod ma dla ciebie sens? A może posiadanie buttonPressed()metod w kontrolerze nie jest warte kłopotów?
Aviv Cohn
1
Innymi słowy (Przepraszam za długie komentarze): Ja myślę , że korzyść z posiadania specificButtonPressed()metody w kontrolerze, jest to, że oddziela widok z rozumieniu go za naciśnie przycisk całkowicie . Wadą jest jednak to, że w pewnym sensie wiąże kontroler z GUI. Które podejście jest lepsze?
Aviv Cohn
@prog IMO Kontroler powinien być ślepy na widok. Przycisk będzie miał jakąś funkcjonalność, ale nie musi znać jego szczegółów. Musi tylko wiedzieć, że wysyła pewne dane do metody kontrolera. Pod tym względem nazwa nie ma znaczenia. Można go nazwać foolub równie łatwo fireZeMissiles. Będzie wiedział tylko, że powinien zgłosić się do określonej funkcji. Nie wie, co robi funkcja, tylko to ją wywoła. Kontroler nie przejmuje się tym, w jaki sposób wywoływane są jego metody, tylko że będzie reagować w określony sposób, kiedy będą.
Schleis
1

Pojedyncza odpowiedzialność kontrolerów ma być umową pośredniczącą między widokiem a modelem. Widok powinien być odpowiedzialny tylko za wyświetlanie, model powinien być odpowiedzialny tylko za logikę biznesową. Odpowiedzialnością kontrolerów jest wypełnienie tych dwóch obowiązków.

To wszystko dobrze i dobrze, ale nieco wyruszę poza środowisko akademickie; Kontroler w MVC składa się zasadniczo z wielu mniejszych metod działania. Te działania zasadniczo odpowiadają rzeczom, które można zrobić. Jeśli sprzedaję produkty, prawdopodobnie będę mieć ProductController. Ten kontroler będzie miał akcje takie jak GetReviews, ShowSpecs, AddToCart itp.

Widok ma SRP wyświetlania interfejsu użytkownika, a część tego interfejsu zawiera przycisk z napisem AddToCart.

Administrator ma SRP znajomości wszystkich widoków i modeli zaangażowanych w proces.

AddToCart Action kontrolerów ma określoną zasadę SRP polegającą na znajomości wszystkich osób, które muszą być zaangażowane, gdy element zostanie dodany do koszyka.

Model produktu ma SRP modelowania logiki produktu, a ShoppingCart Model ma SRP modelowania, w jaki sposób elementy są zapisywane do późniejszego finalizowania. Model użytkownika ma SRP modelowania użytkownika, który dodaje rzeczy do koszyka.

Możesz i powinieneś ponownie wykorzystywać modele, aby załatwić swoją działalność, a modele te muszą zostać połączone w pewnym momencie w kodzie. Sterownik kontroluje każdy unikalny sposób zachodzenia sprzężenia.

WhiteleyJ
źródło
0

Na kontrolerach spoczywa tylko jedna odpowiedzialność: zmiana stanu aplikacji na podstawie danych wprowadzonych przez użytkownika.

Kontroler może wysyłać polecenia do modelu zaktualizować stan modelki (np edycji dokumentu). Może również wysyłać polecenia do powiązanego widoku, aby zmienić prezentację modelu w widoku (np. Przewijając dokument).

source: wikipedia

Jeśli zamiast tego masz „kontrolery” w stylu Railsów (które żonglują aktywnymi instancjami rekordów i głupimi szablonami) , to oczywiście łamie SRP.

Z drugiej strony aplikacje w stylu Railsów nie są tak naprawdę MVC.

mefisto
źródło