Wątek przeniesiony 2018-03-14 18:10 z Algorytmy i struktury danych przez somekind. Powód: Niepoprawna kategoria forum

Hermetyzacja odpowiedzialności vs. SRP

Odpowiedz Nowy wątek
2018-03-11 19:43
0

Z jednej strony mówi się że obiekt sam powinien wiedzieć co ma robić, tzn lepiej napisać kod

Client client = new Client();
client.pay();

niż

Client client = new Client();
ClientHelper.pay(client);

Z drugiej strony, mówi się też że obiekt powinien mieć jedną odpowiedzialność, jeśli ma mieć więcej, należałoby ją oddelegować do innej klasy. Powiedziawszy to, chciałbym poznać podejście forumowiczów do tematu.

Stawiam problem.

Napisz interfejs Piece która ma kilka implementacji (Pawn, Knight, Bishop, Castle, King, Queen) oraz klasę Board która ma te figury.

  • Jak mógłaby wyglądać metoda do wykonania ruchu jedną figurą (powinna być w interfejsie Piece czy w Board?)
  • Skąd figura ma wiedzieć w której miejsce może się ruszyć? (W szczególności skąd wie gdzie jest granica planszy i skąd wie które miejsce nie jest zajete?)
  • Jeśli figura ma znać swoją pozycję (x oraz y), czy może założyć że plansza ma 8x8, i użyć tego założenia do stwierdzenia czy wyjdzie poza planszę lub nie? (Czy to założenie łamie SRP? Bo niby plansza mogłaby mieć więcej?).

PS: Tylko interfejs Piece oraz klasa Board są istotne.


char mander; bool basaur;

Pozostało 580 znaków

2018-03-12 09:54
0
somekind napisał(a):

No, a jak ludzie grają w szachy, to kto weryfikuje, czy ruch jest poprawny jak nie człowiek?

Modelując grę w szachy trzeba zrobić to samo - stworzyć wszechwiedzący walidator znający reguły oraz pamiętający historię danej rozgrywki.

Ludzie są w stanie dyskutować i odwoływać się do konkretnych zasad. Z modelem, w którym człowiek określa czy ruch jest poprawny, idziemy w kierunku "uzgadniania między graczami czy ruch jest dopuszczalny", co jeśli nie uda się osiągnąć wspólnego stanowiska? np. na rozpoczęcie partii pionem zabijam królówkę, bo uznałem, że do dobry ruch. Przeciwnik powinien akceptować/odrzucać takie posunięcie, ewentualnie nie powinno być możliwości wykonania tego ruchu "z automatu" (SędziaSzachowy? Trudno jednak po nim wymagać, że będzie zapamiętywał historię rozgrywki:-) To raczej Gracz powinien po wykonaniu ruchu zapisać sobie na jakimś ArkuszuGry).

Na pewno można modelować na wiele sposobów, każdy będzie miał swoje wady/zalety i te modele można wyrzucać do kosza/rozbudowywać po ich zderzeniu z różnymi sytuacjami, aż osiągniemy nasz UltimateModel ;-)

Pozostało 580 znaków

2018-03-12 14:07
1

Nie wiem po co dodałeś Game tutaj, nie jest ona potrzebna imo. Po drugie ciągle problem istnieje, bo Piece wie o Board.

Jest potrzebna bo co właściwie robi board? pamięta pozycje pionków? zna zasady? validuje ruchy? ustawia pionki na planszy? pobiera dane od użytkownika? To jest bardzo dużo odpowiedzialności w jednym miejscu @somekind to ładniej wytłumaczył.

Po drugie ciągle problem istnieje, bo Piece wie o Board.

Poza tym to żaden problem, jeśli ograniczysz role Board do zaradzania pozycją figur i dawania odpowiedzi czy ruch po skosie do pola {x,y} bez kolizji jest możliwy, to nie przekujesz im dostępu god objectu, tylko do abstrakcji pozywającej na ruchy prosto na bok i skoki, to co wym złego. Poszczególne implantacje Piece ograniczają się do wyboru ruchów. Dostęp do pozycji figur, pozwala na nadużycia ale można to łatwo naprawić, implementując Board jako IMoveProvider i przekazywać do figur tylko ten interfejs. Znowu się okazuje że problem jest to że jedna klasa robi za dużo.
Podobnie można zapewnić wsparcie dla specjalnych przypadków. Funkcje do sprawdzania czy roszada albo bicie w przelocie jest możliwe implementujesz w Game/GodObjecie a potem przekazujesz je jako strategie konstruktorze figur. W taki sposób figury były by wstawanie same pokazać możliwe dla nich ruchy uwzględniając wszelkie niuanse i równocześnie tak naprawdę nie wiedzieć nic o rozgrywce.

Ważne jest też żeby nie odlatywać z abstrakcjami za bardzo w kosmos i trzymać się zdrowego rozsądku.

edytowany 4x, ostatnio: topik92, 2018-03-12 15:15

Pozostało 580 znaków

2018-03-12 14:53
0

@topik92: Napisałeś odpowiedź w której jest tak dużo, a jednocześnie mówi tak mało. Nie pytałem o to jak to technicznie zrobić, jak porozdzielać interfejsy czy fabryki i odpowiedzialności. Moje pytanie (skrócone) brzmi:

  • Jak zaimplementować klasy by ruch Pawn'a zależał od jego implementacji, ale również od sytuacji na planszy i samej planszy.

Twoja odpowiedź to jakieś mambo dżambo, które nie mówi nic nt. mojego pytania.


char mander; bool basaur;
To była odpowiedź na to dlaczego to nie problem że piece wie o borad masz u góry cytat i dlaczego potrzebna jest 3 klasa. Poza tym opisałem jak zaimplementować by ruch Pawna zależał od jego implementacji(...)i samej planszy przekazujesz rozwiązanie sytuacji specjalnych w jako strategie konstruktorze. Pionek nic nie wie o samej grze, a równocześnie uwzględnia wszystkie szczegóły. - topik92 2018-03-12 14:58

Pozostało 580 znaków

2018-03-12 15:46
1
TomRiddle napisał(a):

Więc w jaki sposób powinny delegować tą decyzję do innych klas? Mógłbyś podać przykład Twojej struktury?

Zakładając, że Walidator dostaje jakieś Polecenie Ruchu, które mówi jaką bierkę, z którego pola, na które chce się ruszyć, to następnie musi:

  1. spytać coś (no niech będzie, że Planszę), czy na polu startowym stoi ta bierka, czy pole docelowe jest puste, a jeśli nie, to czy stoi tam bierka przeciwnika;
  2. spytać Bierkę o zasady jej ruchu i Planszę o to, czy jest możliwe przejście między tymi polami;
  3. stwierdzić, czy ruch jest możliwy "geograficznie" (czy nie wyjdziemy poza planszę, czy pole jest w zasięgu, niezajęte i czy jest przejście);
  4. zweryfikować pozostałe zasady.

W szachach to może jest trywialne, ale co z grą w której jest nieskończenie wiele ruchów (albo za dużo by je zwracać), ale można je prosto walidować?

Co masz na myśli? Wszystko jest kwestią tego w jaki sposób opiszesz w kodzie zasady ruchu. Wiadomo, że raczej nie można hardcodować wszystkich możliwych współrzędnych pól docelowych każdej bierki na każdym polu, trzeba raczej myśleć o kierunku(kierunkach) i zasięgu.

yarel napisał(a):

Ludzie są w stanie dyskutować i odwoływać się do konkretnych zasad. Z modelem, w którym człowiek określa czy ruch jest poprawny, idziemy w kierunku "uzgadniania między graczami czy ruch jest dopuszczalny" co jeśli nie uda się osiągnąć wspólnego stanowiska? np. na rozpoczęcie partii pionem zabijam królówkę, bo uznałem, że do dobry ruch. Przeciwnik powinien akceptować/odrzucać takie posunięcie, ewentualnie nie powinno być możliwości wykonania tego ruchu "z automatu"

Jakie uzgadnianie? Zasady są ściśle określone, jeśli ich nie znamy, to nie możemy zaimplementować algorytmu weryfikującego. :)
Piszesz o graczach, chodzi Ci o partię między ludźmi? Bo ja cały czas piszę o tym, jak obiektowo zamodelować walidację ruchów w grze w szachy. W tym celu odzwierciedlenie fizycznych przedmiotów takich jak bierki i plansza jest niekonieczne, ważniejszy jest walidator znający zasady ruchów i historię rozgrywki.

(SędziaSzachowy? Trudno jednak po nim wymagać, że będzie zapamiętywał historię rozgrywki:-) To raczej Gracz powinien po wykonaniu ruchu zapisać sobie na jakimś ArkuszuGry).

To jak nazwiemy klasy to rzecz wtórna, pewne jest to, że te informacje trzeba jakoś przechowywać i walidacja musi z nich korzystać. No i uzupełnianie historii raczej powinno odbywać się automatycznie, nie polegać na człowieku.


"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."

Pozostało 580 znaków

2018-03-12 22:01
0

Moim zdaniem walidatorów może być kilka, związanych z poszczególnymi zasadami. Bierki mogą być połączone z kilkoma walidatorami, które są "installowane" dla danego typu bierki podczas rozruchu. Poruszenie bierki wymaga uruchomienia wszystkich walidatorów.
Sam walidator zdaje się potrzebować jako argumentu stanu gry, t.j. rozkładu planszy, informacji do którego gracza należy ruch, czy jest aktualnie szach, etc.

edytowany 1x, ostatnio: nalik, 2018-03-12 22:08

Pozostało 580 znaków

2018-03-14 09:10
0
somekind napisał(a):

Jakie uzgadnianie? Zasady są ściśle określone, jeśli ich nie znamy, to nie możemy zaimplementować algorytmu weryfikującego. :)
Piszesz o graczach, chodzi Ci o partię między ludźmi? Bo ja cały czas piszę o tym, jak obiektowo zamodelować walidację ruchów w grze w szachy. W tym celu odzwierciedlenie fizycznych przedmiotów takich jak bierki i plansza jest niekonieczne, ważniejszy jest walidator znający zasady ruchów i historię rozgrywki.

Co do zasad nie ma wątpliwości :-) Chodziło mi o Gracza jako element modelu, ale z bardzo prostą odpowiedzialnością "interakcja z grą" (dołącz do gry, wykonaj ruch, poddaj się, zaproponuj remis, odrzuć remis). W tym sensie umieszczanie walidacji w tej klasie nie wydało mi się właściwe, bo taki "Gracz" korzystający z "interfejsu" Gry mógłby oszukiwać przez implementację walidacji ruchu jako "wszystko dozwolone".

edytowany 1x, ostatnio: yarel, 2018-03-14 09:10

Pozostało 580 znaków

2018-03-14 10:02
0
nalik napisał(a):

Moim zdaniem walidatorów może być kilka, związanych z poszczególnymi zasadami. Bierki mogą być połączone z kilkoma walidatorami, które są "installowane" dla danego typu bierki podczas rozruchu. Poruszenie bierki wymaga uruchomienia wszystkich walidatorów.
Sam walidator zdaje się potrzebować jako argumentu stanu gry, t.j. rozkładu planszy, informacji do którego gracza należy ruch, czy jest aktualnie szach, etc.

Wydaje się mi się, że jednak szachy są zbyt mało skomplikowaną grą żeby rozdrabniać się na wiele walidatorów - jeżeli już to zaczynałbym od pojedynczego i dopiero gdy zacząłby przerastać swoje możliwości to pomyślałbym o podziale.
W zasadzie potrzebowałbyś też jakiegoś MasterWalidatora który trzymałby wszystkie inne walidatory i je odpalał dla każdego ruchu.

Tak czy inaczej, walidator potrzebuje dostępu do wszystkich danych - planszy, wszystkich typów bierek na planszy, obecnego ruchu, historii itd. Ewentualnie mógłby trzymać np. jakąś metamapę z polami zagrożonymi i update'ować ją po każdym ruchu... hmm...

Inna sprawa - czy takie podejście nie jest 'nieobiektowe'? Podejrzewam, że mistrzowie od DDD / 'obiektowości' mieliby problem z dodatkową klasą która będzie miała całą/większość logiki. Wtedy plansza i bierki raczej przypominają zwykłe struktury danych z minimalnym zachowaniem.

edytowany 1x, ostatnio: Fedaykin, 2018-03-14 10:06
Tak to jest nie obiektowe, właśnie dlatego uważam że plansza powinna zajmować sie detalami z geometria 2d, wtedy nie była by wydmuszką, a bierki powinny przetwarzać złożone ruchy za pomocą strategii przekazanych w konstruktorze. Teraz myślę że wystarczyła by jedna classa bierka i kilka klas ze strategiami. Wyciągneło by to sporo logiki z godObjectu i było całkiem proste. - topik92 2018-03-14 13:15

Pozostało 580 znaków

2018-03-14 18:09
3
yarel napisał(a):

Chodziło mi o Gracza jako element modelu, ale z bardzo prostą odpowiedzialnością "interakcja z grą" (dołącz do gry, wykonaj ruch, poddaj się, zaproponuj remis, odrzuć remis). W tym sensie umieszczanie walidacji w tej klasie nie wydało mi się właściwe, bo taki "Gracz" korzystający z "interfejsu" Gry mógłby oszukiwać przez implementację walidacji ruchu jako "wszystko dozwolone".

Ok, no jak dla mnie Gracz nie powinien być elementem modelu w tym przypadku. W samej grze owszem, jakiś obiekt reprezentujący gracza być może, ale nie w silniku sprawdzającym poprawność ruchu.

Fedaykin napisał(a):

Wydaje się mi się, że jednak szachy są zbyt mało skomplikowaną grą żeby rozdrabniać się na wiele walidatorów - jeżeli już to zaczynałbym od pojedynczego i dopiero gdy zacząłby przerastać swoje możliwości to pomyślałbym o podziale.
W zasadzie potrzebowałbyś też jakiegoś MasterWalidatora który trzymałby wszystkie inne walidatory i je odpalał dla każdego ruchu.

No właśnie - bo jeśli nie podzielimy, to skończymy z długaśną klasą z dziesiątkami ifów. A tak, to pewnie jakiś chain of responsibility możne nam pomóc zgrabnie rozwiązać problem.

Tak czy inaczej, walidator potrzebuje dostępu do wszystkich danych - planszy, wszystkich typów bierek na planszy, obecnego ruchu, historii itd. Ewentualnie mógłby trzymać np. jakąś metamapę z polami zagrożonymi i update'ować ją po każdym ruchu... hmm...

Taka mapa wszystkich pól zagrożonych przez wszystkie bierki byłaby raczej dość duża i raczej niepotrzebna do celów walidacji.

Inna sprawa - czy takie podejście nie jest 'nieobiektowe'? Podejrzewam, że mistrzowie od DDD / 'obiektowości' mieliby problem z dodatkową klasą która będzie miała całą/większość logiki.

Pytanie, czy walidacja ruchów w grze w szachy to jest w ogóle problem do rozwiązywania takimi narzędziami jak DDD. A jeśli o normalną obiektowość chodzi, to jeśli podzielimy walidację poszczególnych złamań reguł do oddzielnych klas, które będą miały jedna odpowiedzialność, to raczej nic złego nie będzie. Przykłady takich klas, to np.:

  • CzyNieBijemySwojejBierkiWalidator
  • CzyRoszadaJestMożliwaWalidator
  • CzyKrólNieZostajeOdsłoniętyWalidator

CzyRoszadaJestMożliwa może z kolei się składać z kolejnych "podwalidatorów":

  • CzyKrólByłRuszonyWalidator
  • CzyWieżaByłaRuszonaWalidator
  • CzyKrólIWieżaSąNaWoichPOlachWalidator
  • CzyPolaNaDrodzeKólaSąAtakowaneWalidator

Wtedy plansza i bierki raczej przypominają zwykłe struktury danych z minimalnym zachowaniem.

Ja bym w bierce trzymał informację o jej współrzędnych + opis jej ruchów i ataków, a plansza mogłaby poza trzymaniem dwóch kolekcji bierek dostarczać informacji o tym, czy jest możliwy ruch między dwoma polami (nie stoi nic na drodze).


"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."
Tak, jak się dłużej zastanowiłem to pomysł z metamapą raczej nietrafiony i problematyczny. - Fedaykin 2018-03-15 10:14

Pozostało 580 znaków

2018-03-14 20:46
0

Ogólnie, na co by się autor wątku w ostateczności nie zdecydował, polecałbym później zrobienie unikając koncepcji z programowania obiektowego i porównanie, który kod będzie bardziej zwięzły i zrozumiały. Wkładanie obiektowości w tym zastosowaniu jest bardzo na siłę i nie przyniesie nic dobrego.

somekind napisał(a):

Pytanie, czy walidacja ruchów w grze w szachy to jest w ogóle problem do rozwiązywania takimi narzędziami jak DDD. A jeśli o normalną obiektowość chodzi, to jeśli podzielimy walidację poszczególnych złamań reguł do oddzielnych klas, które będą miały jedna odpowiedzialność, to raczej nic złego nie będzie. Przykłady takich klas, to np.:

  • CzyNieBijemySwojejBierkiWalidator
  • CzyRoszadaJestMożliwaWalidator
  • CzyKrólNieZostajeOdsłoniętyWalidator

Jednak prościej i czytelniej będzie zrobić do tego pojedyncze funkcje a nie klasy.

CzyRoszadaJestMożliwa może z kolei się składać z kolejnych "podwalidatorów":

  • CzyKrólByłRuszonyWalidator
  • CzyWieżaByłaRuszonaWalidator

Proste rozwiązanie, które przychodzi mi do głowy to pojedyncza maska bitowa dla każdego z graczy. I wtedy np.: if (maska_ruszonych_figur[nr_gracza] & MASKA_DLA_ROSZADY_LEWEJ) == 0) { mozna-wykonac };

  • CzyKrólIWieżaSąNaWoichPOlachWalidator

Jeśli nie były ruszane to są. Jeśli były ruszane, to jest bez znaczenia, czy są na swoich polach.

  • CzyPolaNaDrodzeKólaSąAtakowaneWalidator

Tu znowu funkcja lepiej pokazuje intencje niż klasa.

Pokaż pozostałe 7 komentarzy
"wywołam kolejno wszystkie potrzebne funkcje" - Mi by się nie chciało tyle pisać ;) - mstl 2018-03-14 22:01
Noo, np. korzyść jest taka, że można wszczynać idiotyczne dyskusje z kimś, kto ewidentnie nie chce dyskutować. Pewnie jeszcze kilka korzyści by się znalazło, ale jakoś nie przychodzą mi do głowy. - Michał Sikora 2018-03-14 22:01
"Jeszcze kilka"? To może przytocz te korzyści, które już zostały wymienione, bo ja nie widzę ANI JEDNEJ. - Troll anty OOP 2018-03-14 22:03
Maski binarne != prostsciej. Wydzielenie funkcji jako klasę, pozwala na korzystanie ze ze zmiennych prywatnych, dzięki czemu nie trzeba przekazywać całej tablicy Mendelejewa jako parametry, a potem na rozbicie funkcji publicznej na kilka małych prywatnych. Pisząc strukturalnie taki refactoring byłby trudny bo jakoś trzeba przekazać wspólne paramenty każdej pod funkcji. - topik92 2018-03-14 22:35

Pozostało 580 znaków

2018-03-15 02:09
2
Troll anty OOP napisał(a):

Ogólnie, na co by się autor wątku w ostateczności nie zdecydował, polecałbym później zrobienie unikając koncepcji z programowania obiektowego i porównanie, który kod będzie bardziej zwięzły i zrozumiały. Wkładanie obiektowości w tym zastosowaniu jest bardzo na siłę i nie przyniesie nic dobrego.

Generalnie jestem za - napisz wersję w pełni proceduralną i pokaż nam jaka ona będzie łatwa. :)

Jednak prościej i czytelniej będzie zrobić do tego pojedyncze funkcje a nie klasy.

Nie wiem czy wiesz, ale klasy mogą zawierać funkcje. Co więcej, ideą istnienia klas jest zawieranie w sobie funkcji. (Coś takiego jak obiekt bez funkcji nie jest obiektem w OOP.)
Zaletą funkcji w oddzielnych klasach jest to, że można je umieścić w oddzielnych plikach co ma wpływ na przejrzystość i łatwość czytania kodu, a z drugiej strony można je umieścić w jednej kolekcji, co daje łatwość wywołania.

Ja ogólnie jestem zwolennikiem podejścia pragmatycznego - nie wiem, jak długie będą te funkcje i ile ich będzie, a sposobów organizacji może być wiele:

  • wszystkie funkcje w jednej klasie;
  • każda funkcja w oddzielnej klasie;
  • pogrupowanie tematyczne (razem funkcje sprawdzające prawidłowość ruchu zgodnie z zasadami bierek, w drugiej klasie funkcje sprawdzające czy ruch zawiera się w planszy, w trzeciej np. roszada, w czwratej jeszcze coś innego).

Trudno powiedzieć, które rozwiązanie będzie lepsze zanim zacznie pisać się kod i zobaczy, co z niego wyjdzie. Jeśli ktoś twierdzi, że lepiej wie, co będzie lepsze zanim zacznie pisanie kodu, to lepiej niech wraca do swoich kredek i maluje te swoje UMLe dalej.

Proste rozwiązanie, które przychodzi mi do głowy to pojedyncza maska bitowa dla każdego z graczy. I wtedy np.: if (maska_ruszonych_figur[nr_gracza] & MASKA_DLA_ROSZADY_LEWEJ) == 0) { mozna-wykonac };

To jest naprawdę szczegół, generalnie i tak należy się zdecydować na jedno z dwóch podejść: albo w momencie próby wykonania roszady przez gracza sprawdzamy historię i stan planszy, albo po każdym ruchu zapisujemy w bieżącym stanie gry informację o tym, czy roszada jest możliwa.

Jeśli nie były ruszane to są. Jeśli były ruszane, to jest bez znaczenia, czy są na swoich polach.

Chodziło mi o roszadę z wieżą powstałą na skutek promocji piona, ale mniejsza z tym. Chciałem zarysować ogólną koncepcję, nie projektować wszystko z góry. Takie szczegóły jak dokładny podział na funkcje czy klasy wychodzą w trakcie tworzenia.

Tu znowu funkcja lepiej pokazuje intencje niż klasa.

Niewątpliwie.
Tylko ja mam wrażenie, że Ty kompletnie nie rozumiesz idei klas. Klasy nie zawierają instrukcji, to funkcje zawierają instrukcje. Klasy są sposobem modularyzacji oraz trzymania przy funkcji danych i zależności, których funkcja potrzebuje.


"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0