Wątek przeniesiony 2018-03-14 17:10 z Algorytmy i struktury danych przez somekind.

Hermetyzacja odpowiedzialności vs. SRP

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.

1

według mnie

  1. w Board bo figura nie musi (a wręcz nie powinna) wiedzieć nic o planszy
  2. nie ma wiedzieć w które miejsce konkretnie - wystarczy, że wie, że z pozycji (0,0) może się poruszyć np. na (1, 1), (2, 2), (-1, 1), (-2, 2), (1, -1), (2, -2), (-1, -1), (-2, -2) itd - czy będą to na sztywno wpisane czy liczone to od inwencji zależy. Przekażesz jej miejsce docelowe z przesunięciem do pozycji (0, 0) i tyle
  3. niezachodzi
0

Nie wiem czy jest jedna słuszna odpowiedź.

Przesuwanie figury widziałbym tak:

  1. Gracz pobiera figurę
  2. Gracz mówi figurze, gdzie ma się przesunąć
  3. Figura weryfikuje czy ruch jest dopuszczalny.
  4. Figura prosi Szachownicę o przesunięcie
  5. Szachownica przesuwa figurę
 // Gracz pobiera figurę
 Figura f = szachownica->dajMiFigure(this,"C3");  
 // szachownica wie czy na C3 jest figura i czy ma taki sam kolor jak kolor gracza i na tej podstawie
 // zwraca figure, albo wali wyjątkiem: FiguraNieNalezyDoGracza, PustePole
 
 f->przesunNa("E4");
 
 // Figura wie jaki ma zakres ruchu, ale może zachodzić jedna z 2 opcji w modelu:  
 // a) Figura wie na którym polu się znajduje i na podstawie pola docelowego jest w stanie określić czy to jej zakres ruchu. 
 // b) Figura nie wie na którym polu się znajduje i musi zapytać o to szachownicę. 
 // Figura weryfikuje ruch i wali wyjątkiem jeśli żądany ruch nie jest w jej zakresie, albo prosi szachownice o przesunięcie na wybrane pole
 
 szachownica->przesunFigure(f,"E4");
 // szachownica wie co znajduje się "na trasie ruchu" figury i może dopuścić do przesunięcia, albo rzucić wyjątkiem
 
 // po przesunięciu figury, w zależności od przyjętego modelu (figura zna albo nie zna swojej pozycji)
 // Figura aktualizuje informację o tym, że została przesunięta na nowe pole 

Tyle w kwestii przesuwania.

2

Chyba problem polega na tym że dzielisz 3 odpowiedzialności między dwa elementy i przy takim podziale zawszę będzie coś nie tak. Tymi samymi pionkami na tej samej planszy można grać w różne gry np. szachy, warcaby, owce i wilki a zasady gry powinny być trzymane osobno a nie upychane po kontach.
Board powinien odzwierciedlać pozycje i zapewniać API ruchów na planszy( z geometrią 2D jest więcej roboty niż się wydaje) itd. Piece implementować strategie ruchu po planszy, a trzecia klasa Game składać wszystko do kupy.
Board jest kolekcją Piece każdy Piece ma referencje do Board w którym się znajduje, tym sposobem odpowiedzialność za to czy nie wypadniemy z planszy deleguje do tego kto się o na tym zna najlepiej i nie musimy powielać kodu do ruchu w 2D. A w samych pionkach skupiamy się na w skazaniu jakie ruchu mogą wykonywać, za pomocą API w board. W Game ma referencje do border sprawdzamy wszystkie brzydkie rzeczy jak bicie w przelocie, roszady a awans pionków na figury, obowiązkowe bicia, klikanie na puste pola itp.

Czyli żeby wykonać ruch wołamy w Game.Move game wykonuje smutne sprawdzenia, pobiera Piece z planszy i pyta się go czy pole do celowe znajduje się na jego liście dozwolonych ruchów. Piece pyta się Boarer czy wykonując sekwencje ruchów x,y,z z jego API dojdzie do pola docelowego, przy założeniu bez kolizyjności, Board sprawdza czy pola są poprawne wykonuje nudne iteracje i daje odpowiedź, Game wykonturuje kolejne nudne sprawdzenia i wysyła eventy do GUI.
Ja bym to tak zrobił, w teorii tego typu rozwiązanie jest na tyle czyste że jakbyś zamienił implementacje Board np. jako wraper na jedno wymiarową tablice byte[] to nie musiał byś żadnego kodu na innych poziomach ruszać.

8

No cóż, takie efekty nauki OOP na kotkach i pieskach. Niby "obiekty mają reprezentować elementy świata rzeczywistego", tylko to w ogóle jest nie prawda. Obiekty mają modelować rzeczywistość, czyli przedstawiać pewne jej uproszczenie pozwalające na rozwiązanie konkretnego problemu. Patrząc z punktu widzenia człowieka, że szachy to plansza i bierki niewiele się osiągnie, bo trzeba też zamodelować reguły gry oraz historię rozgrywki.

TomRiddle napisał(a):

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?)

W żadnej z tych dwóch. Ani bierka ani plansza nie ma wiedzy wystarczającej do stwierdzenia, czy ruch jest prawidłowy.

  • 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?)

Nie wie - figura może dostarczyć jedynie standardowych zasad swojego ruchu.

  • 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?).

W teorii by mogła, ale tak to może się uda weryfikować prawidłowość ruchu w kółko i krzyżyk, na pewno nie w szachach.

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

Pytanie, czy Piece w ogóle powinno być interfejsem, czy potrzebna jest tak dużo odrębnych klas reprezentujących bierki i czym tak naprawdę ma być Board.
Tak czy siak, do sprawdzenia prawidłowości ruchu potrzebujesz znacznie więcej danych niż wymiary planszy oraz "geograficzne" zasady ruchu bierek, bo np.:

  • nie można bić swoich bierek;
  • pionek może się ruszyć o dwa pola tylko w pierwszym ruchu;
  • nie można ruszać się przez inne bierki (z wyjątkiem skoczka);
  • mimo, że pionek rusza się tylko na wprost, a bije tylko na skos, to jednak czasami może zbić pionka stojącego obok siebie;
  • ruch nie może spowodować odsłonięcia króla;
  • nie można przeprowadzić roszady, jeśli król jest atakowany, przechodziłby przez szachowane pole lub jeśli on albo wieża już się w danej partii ruszyły.

No i do tego trzeba uwzględnić też sposób przekazywania danych o ruchu. Dobry walidator nie powinien przecież dopuścić do sytuacji, w której zostanie wykonany ruch z pustego pola, albo pola źródłowe i docelowe będą takie same. Musi też wykryć, czy ruch piona jest promocją, czy podano docelową bierkę, czy nie następuje próba promocji z nieostatniego wiersza, itd.

P.S. Rook nie Castle.

0
somekind napisał(a):
TomRiddle napisał(a):

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?)

W żadnej z tych dwóch. Ani bierka ani plansza nie ma wiedzy wystarczającej do stwierdzenia, czy ruch jest prawidłowy.

Stosując GRASP (Information Expert), to kto ma największą wiedzę o rozmieszczeniu Figur na planszy i jest w stanie na tej podstawie określić czy ruch jest dozwolony?
(Zakładając, że zna zasady gry, jeśli nie, to pewnie potrzebuje skomunikować się z kimś, kto te zasady gry zna - edited: czyli zawężęnie odpowiedzialności w myśl zasady: High Cohesion)

0

@yarel: myślę, że człowiek. Tylko w tym przypadku oczywiście nie ma sensu modelować całego człowieka, wystarczy jakiś jego fragment odpowiedzialny tylko za grę w szachy.

0
somekind napisał(a):

@yarel: myślę, że człowiek. Tylko w tym przypadku oczywiście nie ma sensu modelować całego człowieka, wystarczy jakiś jego fragment odpowiedzialny tylko za grę w szachy.

Człowiek może oszukiwać i łamać reguły gry i to chyba jedyny powód dla którego nie dawałbym kontroli nad określeniem mu odpowiedzialności czy ruch jest poprawny.
Choć czasem to kuszące przesunąć figurę przeciwnika pole obok, choć wtedy to już krok do SzachoBoksu ;)

0

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.

0

W niektórych językach programowania, logika nie była by ani w klasie planszy ani w klasie pionka - tam logika jest w funkcjach a klasy to czyste dane. (poczytaj o multiple dispatch)

0

@somekind:

somekind napisał(a):
TomRiddle napisał(a):

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?)

W żadnej z tych dwóch. Ani bierka ani plansza nie ma wiedzy wystarczającej do stwierdzenia, czy ruch jest prawidłowy.

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

  • 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?)

Nie wie - figura może dostarczyć jedynie standardowych zasad swojego ruchu.

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ć?

Pytanie, czy Piece w ogóle powinno być interfejsem, czy potrzebna jest tak dużo odrębnych klas reprezentujących bierki i czym tak naprawdę ma być Board.

No właśnie, to moje pytania do was.

No i do tego trzeba uwzględnić też sposób przekazywania danych o ruchu. Dobry walidator nie powinien przecież dopuścić do sytuacji, w której zostanie wykonany ruch z pustego pola, albo pola źródłowe i docelowe będą takie same. Musi też wykryć, czy ruch piona jest promocją, czy podano docelową bierkę, czy nie następuje próba promocji z nieostatniego wiersza, itd.

True

P.S. Rook nie Castle.

My bad

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 ;-)

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.

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.

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.

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.

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".

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.

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).

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.

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.

0
somekind napisał(a):

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

Nie widzę póki co też wersji obiektowej, z którą miałbym się mierzyć.

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.

Specyfika języka - np. w C nie ma problemu by umieszczać funkcje w osobnych plikach - tu w wątku nie widzę, aby język był narzucony. Ale nawet uwzględniając, że ma to być w języku, w którym funkcji nie można umieszczać w osobnych plikach: dlaczego bez sprawdzenia zakładać, że w jednym pliku będzie nieczytelnie?
Ten akurat problem dobrze się dzieli na kolejne następujące po sobie kroki, natomiast źle na warstwy (czy jak to nazwać). Umieszczanie funkcji/obiektów w kolekcji w niczym w tym zadaniu nie pomaga, jedyne co może zrobić, to zaciemnić co się rzeczywiście dzieje. Nic pozytywnego.

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).

No i pragmatycznym podejściem powinno być rozpoczynanie od rozwiązania najprostszego, czyli proste wykonywanie kolejnych kroków, nie rozpoczynanie od podziału programu na sztuczne byty. Funkcje dają krótszy zapis, gdy nie są dodatkowo opakowane w dedykowane im obiekty.

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.

Kiedyś się interesowałem algorytmami szachowymi, więc generator ruchów siłą rzeczy też musiałem robić. I wniosek mam taki jak napisałem: zadanie dobrze się dzieli na kolejne kroki, ale z tych kolejnych kroków i tak każdy z nietrywialnych kroków operuje na prawie wszystkich dostępnych danych, więc grupowanie ze względu na odpowiedzialność (reguły ruchu dla danej figury; weryfikacja czy nie wychodzi za planszę - to akurat pojedynczy if, więc po co zaciemniać wydzielając go w osobne miejsce?; ...) jest grupowaniem sztucznym, nie odzwierciedlającym rozwiązywanego problemu.

0
Troll anty OOP napisał(a):

Nie widzę póki co też wersji obiektowej, z którą miałbym się mierzyć.

Nie rozumiem po co chcesz się z czymś mierzyć - jeśli coś jest łatwe, to wystarczy to pokazać, nie trzeba z niczym porównywać.

dlaczego bez sprawdzenia zakładać, że w jednym pliku będzie nieczytelnie?

Bo długie pliki są nieczytelne. To zostało sprawdzone wielokrotnie i niezależnie od paradygmatu oraz języka.

Ten akurat problem dobrze się dzieli na kolejne następujące po sobie kroki, natomiast źle na warstwy (czy jak to nazwać).

Tu nie chodzi o podział na warstwy tylko na kroki. W programowaniu obiektowym nie chodzi o tworzenie warstw.

Umieszczanie funkcji/obiektów w kolekcji w niczym w tym zadaniu nie pomaga, jedyne co może zrobić, to zaciemnić co się rzeczywiście dzieje. Nic pozytywnego.

Mamy do sprawdzenia zestaw pewnych warunków, są one od siebie niezależne i kolejność nie ma znaczenia. Co tu można zaciemnić poprzez niewywołanie ich w sekwencji?
Zaciemniać można raczej właśnie przez wprowadzanie uporządkowania tam, gdzie nie jest potrzebne.

No i pragmatycznym podejściem powinno być rozpoczynanie od rozwiązania najprostszego, czyli proste wykonywanie kolejnych kroków, nie rozpoczynanie od podziału programu na sztuczne byty. Funkcje dają krótszy zapis, gdy nie są dodatkowo opakowane w dedykowane im obiekty.

Pełna zgoda. Zaczynamy pisać kod, jak się okazuje, że jakiś fragment jest długi, powtarzalny, albo jakieś elementy silnie ze sobą współpracują, to wtedy je wydzielamy do oddzielnych bytów.

z tych kolejnych kroków i tak każdy z nietrywialnych kroków operuje na prawie wszystkich dostępnych danych

No nie bardzo, na przykład:

  • Zasady ruchu danej bierki nie zależą od historii ruchów ani od sytuacji na planszy.
  • Położenie pola na planszy ani istnienie wolnej drogi między polami nie zależy od historii ani zasad ruchu poszczególnych bierek.
  • W ogóle legalność ruchów innych niż roszada również nie zależy od historii.
2

imo na dobra sprawe potrzebujesz:
Board - do trzymania biezacych pozycji bierek i sprawdzania jakie ruchy sa dostepne
Piece - do trzymania ruchow (niezaleznych od stanu planszy) bierek
Player - do trzymania stanu per gracz (np roszady, czas etc)
Game - do reszty typu kolejnosc graczy, wygrana/przegrana etc

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

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 };

wut? no rzeczywiscie duzo prostsze i czytelniejsze niz np if(castlingPossible(player)) { albo if(player.castlingPossible()) { wez sie lepiej za podstawy programowania zamiast doradzac glupoty.
narzekasz ze oop wprowadza zbedne abstrakcje a sam bez powodu robisz jakies zbedne obfuskacje. no ale ok, trolluj dalej, szkoda tylko kogos kto to wezmie na powaznie.

0

@katelx: Myślę, że to raczej bierka powinna trzymać swoją pozycję (dwa pola typu short) niż miałaby to robić plansza - bo tam albo trzeba byłoby:

  1. robić mapę bierka -> współrzędne co jest niefajne;
  2. mieć tablicę 8x8 z nullami i bierkami, co jest chyba jeszcze bardziej niefajne.
3

A może byśmy tak sobie zrobili projekt GraWSzachy4Programmers na githubie i przeszli od wizji do projektu? ;-)
Ba, nawet można by mieć realizację w różnych paradygmatach dla porównania.

2
somekind napisał(a):

@katelx: Myślę, że to raczej bierka powinna trzymać swoją pozycję (dwa pola typu short) niż miałaby to robić plansza - bo tam albo trzeba byłoby:

mogloby byc to przydatne (choc wolalabym pole typu Position) ale mysle ze bardziej intuicyjne jest traktowanie planszy jako mapy wspolrzednych do bierek z dodatkowymi metodami np do okreslenia mozliwych ruchow dla danej bierki

0
katelx napisał(a):

mogloby byc to przydatne (choc wolalabym pole typu Position)

Pod spodem i tak będą dwa shorty. ;)

ale mysle ze bardziej intuicyjne jest traktowanie planszy jako mapy wspolrzednych do bierek z dodatkowymi metodami np do okreslenia mozliwych ruchow dla danej bierki

To chyba wróciliśmy do punktu wyjścia - sama plansza nie może tego stwierdzić.

2

Pod spodem i tak będą dwa shorty. ;)

czemu shorty, lepiej to trzymac w jednym bajcie zeby oszczedzic pamiec :):)

To chyba wróciliśmy do punktu wyjścia - sama plansza nie może tego stwierdzić.

chyba ktos musi zakodowac te szachy bo tak na sucho gadac to bez sensu ;)

0

Ludzie 10 lat expa i muszą debatować na 10 stron jak napisać grę w szachy :/

just kiddin'

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