Jak konkretny powinien być wzorzec pojedynczej odpowiedzialności dla klas?

14

Załóżmy na przykład, że masz program do gier konsolowych, który ma wszystkie metody wejścia / wyjścia do iz konsoli. Byłoby inteligentny, aby utrzymać je wszystkie w jednej inputOutputklasie lub przerwać je do większej liczby klas konkretnych jak startMenuIO, inGameIO, playerIO, gameBoardIO, itd. Tak, że każda klasa ma około 1-5 metodami?

Z tego samego powodu, jeśli lepiej je rozbić, czy mądrze byłoby umieścić je w IOprzestrzeni nazw, powodując, że nazwanie ich byłoby nieco bardziej szczegółowe, np .: IO.inGameitd.?

shinzou
źródło
Powiązane: Nigdy nie widziałem dobrego powodu, aby łączyć dane wejściowe i wyjściowe. A kiedy celowo je rozdzielam, mój kod okazuje się znacznie czystszy.
Kaczka Mooing
1
W jakim języku w ogóle nazywasz klasy w lowerCamelCase?
user253751 21.04.2016

Odpowiedzi:

7

Aktualizacja (podsumowanie)

Ponieważ napisałem dość pełną odpowiedź, oto, co sprowadza się do:

  • Przestrzenie nazw są dobre, używaj ich, kiedy ma to sens
  • Używanie inGameIOi playerIOklasy prawdopodobnie stanowiłyby naruszenie SRP. Prawdopodobnie oznacza to, że łączysz sposób obsługi IO z logiką aplikacji.
  • Mają kilka ogólnych klas we / wy, które są używane (lub czasem współużytkowane) przez klasy obsługi. Te klasy procedur obsługi tłumaczyłyby następnie surowe dane wejściowe na format, jaki może mieć logika aplikacji.
  • To samo dotyczy danych wyjściowych: może to być zrobione przez dość ogólne klasy, ale przekazanie stanu gry przez obiekt procedury obsługi / mapowania, który przekształca wewnętrzny stan gry w coś, co mogą obsłużyć ogólne klasy we / wy.

Myślę, że patrzysz na to w niewłaściwy sposób. Oddzielasz IO w zależności od komponentów aplikacji, podczas gdy - dla mnie - bardziej sensowne jest posiadanie oddzielnych klas IO opartych na źródle i „typie” IO.

Posiadanie pewnych podstawowych / ogólnych KeyboardIOklas MouseIOna początek, a następnie opartych na tym, kiedy i gdzie ich potrzebujesz, mają podklasy, które inaczej obsługują wspomniane IO.
Na przykład wprowadzanie tekstu jest czymś, co prawdopodobnie chcesz obsługiwać inaczej niż sterowanie w grze. Przekonasz się, że chcesz mapować niektóre klucze w różny sposób w zależności od każdego przypadku użycia, ale to mapowanie nie jest częścią samego IO, to sposób, w jaki obsługujesz IO.

Trzymając się SRP, miałbym kilka klas, których mogę użyć do operacji na klawiaturze. W zależności od sytuacji prawdopodobnie będę chciał wchodzić w interakcje z tymi klasami w różny sposób, ale ich jedynym zadaniem jest powiedzenie mi, co robi użytkownik.

Następnie wstrzyknąłem te obiekty do obiektu obsługi, który albo odwzorowałby surowe IO na coś, z czym mogłaby współpracować moja logika aplikacji (np. Użytkownik naciska „w” , program obsługi odwzorowuje to na MOVE_FORWARD).

Te uchwyty z kolei służą do poruszania postaci i odpowiednio rysują ekran. Rażące uproszczenie, ale istotą tego jest taka struktura:

[ IO.Keyboard.InGame ] // generic, if SoC and SRP are strongly adhered to, changing this component should be fairly easy to do
   ||
   ==> [ Controls.Keyboard.InGameMapper ]

[ Game.Engine ] <- Controls.Keyboard.InGameMapper
                <- IO.Screen
                <- ... all sorts of stuff here
    InGameMapper.move() //returns MOVE_FORWARD or something
      ||
      ==> 1. Game.updateStuff();//do all the things you need to do to move the character in the given direction
          2. Game.Screen.SetState(GameState); //translate the game state (inverse handler)
          3. IO.Screen.draw();//generate actual output

To, co mamy teraz, to klasa odpowiedzialna za IO klawiatury w jej surowej formie. Kolejna klasa, która tłumaczy te dane na coś, co silnik gry może właściwie zrozumieć, dane te są następnie wykorzystywane do aktualizacji stanu wszystkich zaangażowanych komponentów, a na koniec osobna klasa zajmie się wyjściem na ekran.

Każda klasa ma jedno zadanie: obsługa wprowadzania z klawiatury odbywa się przez klasę, która nie wie / nie obchodzi / musi wiedzieć, co oznacza przetwarzanie, które przetwarza. Wystarczy wiedzieć, jak uzyskać dane wejściowe (buforowane, niebuforowane, ...).

Program obsługi tłumaczy to na wewnętrzną reprezentację reszty aplikacji, aby zrozumieć te informacje.

Silnik gry pobiera przetłumaczone dane i wykorzystuje je do powiadomienia wszystkich odpowiednich komponentów, że coś się dzieje. Każdy z tych elementów wykonuje tylko jedną rzecz, czy to kontrole kolizji, czy zmiany animacji postaci, to nie ma znaczenia, to zależy od każdego obiektu.

Obiekty te następnie przekazują swój stan z powrotem, a dane są przekazywane do Game.Screen, co w istocie jest odwrotnym modułem obsługi We / Wy. Odwzorowuje wewnętrzną reprezentację na coś, co IO.Screenskładnik może wykorzystać do wygenerowania rzeczywistych wyników.

Elias Van Ootegem
źródło
Ponieważ jest to aplikacja konsolowa, nie ma myszy, a drukowanie wiadomości lub płyty jest ściśle powiązane z danymi wejściowymi. W twoim przykładzie, czy IOi oraz gameprzestrzenie nazw lub klasy z podklasami?
shinzou,
@kuhaku: Są to przestrzenie nazw. Istotą tego, co mówię, jest to, że jeśli zdecydujesz się utworzyć podklasy w oparciu o to, w której części aplikacji się znajdujesz, skutecznie ściśle łączysz podstawową funkcjonalność IO z logiką aplikacji. Skończysz z klasami, które są odpowiedzialne za IO w funkcji aplikacji . Dla mnie to brzmi jak naruszenie SRP. Jeśli chodzi o nazwy vs klasy z przestrzenią nazw: wolę 95% czasu na nazwy
Elias Van Ootegem,
Zaktualizowałem swoją odpowiedź, podsumowując moją odpowiedź
Elias Van Ootegem,
Tak, to właściwie inny problem, który mam (sprzężenie IO z logiką), więc naprawdę zaleca się oddzielenie wejścia od wyjścia?
shinzou,
@kuhaku: To naprawdę zależy od tego, co robisz i od tego, jak skomplikowane są wejścia / wyjścia. Jeśli koparki (tłumaczenia stanu gry vs raw wejścia / wyjścia) różnią się zbyt wiele, to prawdopodobnie będziesz chciał oddzielić klas wejściowych i wyjściowych, jeśli nie, to pojedyncza klasa IO jest w porządku
Elias Van Ootegem
23

Zasada pojedynczej odpowiedzialności może być trudna do zrozumienia. Za użyteczne uważam myślenie o tym, jak piszesz zdania. Nie próbujesz wcisnąć wielu pomysłów w jedno zdanie. Każde zdanie powinno jasno określać jeden pomysł i odkładać szczegóły. Na przykład, jeśli chcesz zdefiniować samochód, powiedziałbyś:

Pojazd drogowy, zazwyczaj z czterema kołami, napędzany silnikiem spalinowym.

Następnie zdefiniowałbyś osobno takie rzeczy jak „pojazd”, „droga”, „koła” itp. Nie próbowałbyś powiedzieć:

Pojazd do transportu osób drogą krajową, trasą lub drogą lądową między dwoma miejscami, który został utwardzony lub w inny sposób ulepszony, aby umożliwić podróż, który ma cztery okrągłe obiekty, które obracają się na osi zamocowanej pod pojazdem i jest napędzany silnikiem, który wytwarza moc napędowa przez spalanie benzyny, oleju lub innego paliwa za pomocą powietrza.

Podobnie, powinieneś postarać się, aby twoje klasy, metody itp. Określały centralną koncepcję tak prosto, jak to możliwe, i odkładały szczegóły na inne metody i klasy. Podobnie jak przy pisaniu zdań, nie ma twardej zasady, jak duże powinny być.

Vaughn Cato
źródło
Podobnie jak w przypadku definiowania samochodu za pomocą kilku słów zamiast wielu, a następnie definiowanie małych konkretnych, ale podobnych klas jest w porządku?
shinzou,
2
@kuhaku: Małe jest dobre. Specyficzne jest dobre. Podobny jest w porządku, pod warunkiem, że jest SUCHY. Starasz się łatwo zmienić swój projekt. Utrzymywanie drobiazgów w szczegółach pomaga wiedzieć, gdzie wprowadzić zmiany. Utrzymanie stanu DRY pomaga uniknąć konieczności zmiany wielu miejsc.
Vaughn Cato,
6
Twoje drugie zdanie wygląda tak: „Cholera, nauczyciel powiedział, że muszą to być 4 strony ...„ generuje siłę napędową ”, to jest !!”
corsiKa 20.04.16
2

Powiedziałbym, że najlepszym sposobem jest trzymanie ich w oddzielnych klasach. Małe klasy nie są złe, w rzeczywistości przez większość czasu są dobrym pomysłem.

Jeśli chodzi o konkretny przypadek, myślę, że oddzielenie może pomóc zmienić logikę jednego z tych specyficznych programów obsługi bez wpływu na inne i, jeśli to konieczne, łatwiej byłoby ci dodać nową metodę wejścia / wyjścia, jeśli do tego dojdzie.

Zalomon
źródło
0

Dyrektor ds. Jednolitej Odpowiedzialności stwierdza, że klasa powinna mieć tylko jeden powód do zmiany. Jeśli twoja klasa ma wiele powodów do zmiany, możesz podzielić ją na inne klasy i wykorzystać kompozycję, aby wyeliminować ten problem.

Aby odpowiedzieć na twoje pytanie, muszę zadać ci pytanie: czy twoja klasa ma tylko jeden powód do zmiany? Jeśli nie, nie bój się dodawać bardziej specjalistycznych klas, dopóki każda z nich nie będzie miała tylko jednego powodu do zmiany.

Greg Burghardt
źródło