Tworzę grę planszową (takich jak szachy) w Javie, gdzie każdy element ma swój własny typ (jak Pawn
, Rook
itd.). Do części GUI aplikacji potrzebuję obrazu dla każdego z tych elementów. Ponieważ robienie myśli jak
rook.image();
narusza separację interfejsu użytkownika i logiki biznesowej, utworzę inny prezenter dla każdego elementu, a następnie przypiszę typy elementów do odpowiednich prezenterów, takich jak
private HashMap<Class<Piece>, PiecePresenter> presenters = ...
public Image getImage(Piece piece) {
return presenters.get(piece.getClass()).image();
}
Jak na razie dobrze. Jednak wyczuwam, że rozsądny guru OOP zmarszczyłby brwi po wywołaniu getClass()
metody i sugerowałby użycie gościa, na przykład takiego:
class Rook extends Piece {
@Override
public <T> T accept(PieceVisitor<T> visitor) {
return visitor.visitRook(this);
}
}
class ImageVisitor implements PieceVisitor<Image> {
@Override
public Image visitRook(Rook rook) {
return rookImage;
}
}
Podoba mi się to rozwiązanie (dziękuję, guru), ale ma jedną istotną wadę. Za każdym razem, gdy do aplikacji dodawany jest nowy typ elementu, PieceVisitor wymaga aktualizacji o nową metodę. Chciałbym użyć mojego systemu jako frameworka do gry planszowej, w którym nowe elementy mogłyby być dodawane za pomocą prostego procesu, w którym użytkownik frameworka zapewniłby jedynie implementację zarówno elementu, jak i jego prezentera, i po prostu podłączył go do frameworka. Moje pytanie: czy istnieje czyste rozwiązanie OOP bez instanceof
, getClass()
itd., Które pozwoliłyby na tego rodzaju rozciągliwości?
źródło
Odpowiedzi:
Tak jest.
Pozwól, że cię o to zapytam. W obecnych przykładach znajdujesz sposoby mapowania typów elementów na obrazy. Jak to rozwiązuje problem przenoszenia elementu?
Silniejszą techniką niż pytanie o typ jest podążanie Powiedz, nie pytaj . Co jeśli każdy kawałek miał
PiecePresenter
interfejs i wyglądałby tak:Konstrukcja wyglądałaby mniej więcej tak:
Zastosowanie wyglądałoby mniej więcej tak:
Chodzi tutaj o to, aby unikać ponoszenia odpowiedzialności za robienie czegokolwiek, za co odpowiedzialne są inne rzeczy, poprzez nie pytanie o to ani podejmowanie na ich podstawie decyzji. Zamiast tego trzymaj odniesienie do czegoś, co wie, co z czymś zrobić i powiedz mu, żeby zrobiło coś z tym, co wiesz.
Pozwala to na polimorfizm. Nie obchodzi Cię to, z czym rozmawiasz. Nie obchodzi cię, co ma do powiedzenia. Po prostu dbasz o to, że może zrobić to, czego potrzebujesz.
Dobry schemat, który utrzymuje je w oddzielnych warstwach, następuje powiedzieć-nie-zapytać, i pokazuje, jak nie para warstwa na warstwę niesprawiedliwie jest to :
Dodaje warstwę przypadku użycia, której nie użyliśmy tutaj (i na pewno możemy dodać), ale postępujemy zgodnie z tym samym wzorem, który widzisz w prawym dolnym rogu.
Zauważysz również, że Prezenter nie korzysta z dziedziczenia. Wykorzystuje kompozycję. Dziedziczenie powinno być ostatecznym sposobem na uzyskanie polimorfizmu. Wolę projekty, które preferują kompozycję i delegowanie. To trochę więcej pisania na klawiaturze, ale o wiele większa moc.
źródło
Co z tym:
Twój model (klasy figur) ma wspólne metody, których możesz potrzebować również w innym kontekście:
Obrazy, które mają być użyte do wyświetlenia określonej figury, otrzymują nazwy plików według schematu nazewnictwa:
Następnie możesz załadować odpowiedni obraz bez uzyskiwania dostępu do informacji o klasach Java.
Myślę, że nie powinieneś zbytnio koncentrować się na zajęciach . Myśl raczej w kategoriach obiektów biznesowych .
A ogólnym rozwiązaniem jest wszelkiego rodzaju mapowanie . IMHO polega na przeniesieniu tego mapowania z kodu do zasobu, który jest łatwiejszy w utrzymaniu.
Mój przykład robi to mapowanie według konwencji, które jest dość łatwe do wdrożenia i unika dodawania informacji związanych z widokiem do modelu biznesowego . Z drugiej strony można uznać to za „ukryte” mapowanie, ponieważ nigdzie nie jest wyrażone.
Inną opcją jest postrzeganie tego jako osobnego przypadku biznesowego z własnymi warstwami MVC, w tym warstwą trwałości zawierającą odwzorowanie.
źródło
Dla każdego elementu utworzę osobną klasę interfejsu użytkownika / widoku, która zawiera informacje wizualne. Każda z tych klas widoków ma wskaźnik do swojego modelu / biznesowego odpowiednika, który zawiera pozycję i zasady gry.
Weźmy na przykład pionka:
Pozwala to na całkowite rozdzielenie logiki i interfejsu użytkownika. Możesz przekazać wskaźnik elementów logicznych do klasy gry, która poradziłaby sobie z przenoszeniem elementów. Jedyną wadą jest to, że tworzenie instancji musiałoby się zdarzyć w klasie interfejsu użytkownika.
źródło
Piece* p
. Skąd mam wiedzieć, że muszę go utworzyć,PawnView
aby go wyświetlić, a nie aRookView
lubKingView
? Czy też muszę natychmiast tworzyć widok towarzyszący lub prezentera za każdym razem, gdy tworzę nowy utwór? To byłoby w zasadzie rozwiązanie @ CandiedOrange z odwróconymi zależnościami. W takim przypadkuPawnView
konstruktor może również pobraćPawn*
nie tylkoPiece*
.Podszedłbym do tego, tworząc
Piece
ogólny, gdzie jego parametrem jest typ wyliczenia, który identyfikuje typ elementu, przy czym każdy element ma odniesienie do jednego takiego typu. Następnie interfejs użytkownika może użyć mapy z wyliczenia, jak poprzednio:Ma to dwie interesujące zalety:
Po pierwsze, dotyczy większości języków o typie statycznym: jeśli sparametryzujesz swoją tablicę z typem kawałka do xpect, nie możesz następnie wstawić do niej niewłaściwego rodzaju kawałka.
Po drugie, a może bardziej interesujące, jeśli pracujesz w Javie (lub innych językach JVM), powinieneś zauważyć, że każda wartość wyliczenia nie jest tylko niezależnym obiektem, ale może również mieć własną klasę. Oznacza to, że możesz używać obiektów typu elementu jako obiektów strategii w celu zachowania zachowania elementu:
(Oczywiście rzeczywiste implementacje muszą być bardziej złożone, ale mam nadzieję, że masz pomysł)
źródło
Jestem pragmatycznym programistą i naprawdę nie dbam o czystą lub brudną architekturę. Uważam, że wymagania powinny być obsługiwane w prosty sposób.
Wymagane jest, aby logika aplikacji szachowej była reprezentowana na różnych warstwach prezentacji (urządzeniach), takich jak przeglądarka internetowa, aplikacja mobilna, a nawet aplikacja konsolowa, dlatego musisz spełnić te wymagania. Możesz preferować używanie bardzo różnych kolorów, obrazków na każdym urządzeniu.
Jak widzieliście, parametr prezentera powinien być przekazywany na każde urządzenie (warstwę prezentacji) inaczej. Oznacza to, że warstwa prezentacji zdecyduje, jak przedstawić każdy kawałek. Co jest złego w tym rozwiązaniu?
źródło
Istnieje inne rozwiązanie, które pomoże ci całkowicie wyodrębnić interfejs użytkownika i logikę domeny. Twoja tablica powinna być narażona na działanie warstwy interfejsu użytkownika, a warstwa interfejsu użytkownika może decydować, jak reprezentować elementy i pozycje.
Aby to osiągnąć, możesz użyć ciągu Fen . Łańcuch Fen jest w zasadzie informacją o stanie płyty i podaje aktualne elementy i ich pozycje na pokładzie. Tak więc twoja tablica może mieć metodę, która zwraca aktualny stan tablicy poprzez ciąg Fen, wtedy twoja warstwa interfejsu użytkownika może reprezentować tablicę według własnego uznania. Tak właśnie działają obecne silniki szachowe. Silniki szachowe są aplikacjami konsolowymi bez GUI, ale używamy ich za pośrednictwem zewnętrznego GUI. Silnik szachowy komunikuje się z GUI za pomocą ciągów znaków i notacji szachowej.
Pytasz o to, co jeśli dodam nowy utwór? Nie jest realistyczne, że szachy wprowadzą nowy kawałek. To byłaby ogromna zmiana w Twojej domenie. Postępuj zgodnie z zasadą YAGNI.
źródło