Czysty sposób OOP mapowania obiektu na jego prezentera

13

Tworzę grę planszową (takich jak szachy) w Javie, gdzie każdy element ma swój własny typ (jak Pawn, Rookitd.). 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?

Lishaak
źródło
Jaki jest cel posiadania własnej klasy dla każdego rodzaju figury szachowej? Wydaje mi się, że przedmiot w kawałku zachowuje pozycję, kolor i rodzaj pojedynczego elementu. Prawdopodobnie miałbym do tego celu klasę Piece i dwa wyliczenia (PieceColor, PieceType).
PRZYJDŹ
@ COMEFROM cóż, oczywiście różne rodzaje elementów mają różne zachowania, więc musi być jakiś niestandardowy kod, który rozróżnia między powiedzeniem wieżami i pionkami. To powiedziawszy, wolałbym raczej mieć standardową klasę elementów, która obsługuje wszystkie typy i używa obiektów strategii do dostosowywania zachowania.
Jules
@Jules i jaka byłaby korzyść z posiadania strategii dla każdego utworu w porównaniu z oddzielną klasą dla każdego elementu zawierającego zachowanie samo w sobie?
lishaak
2
Jedną z wyraźnych korzyści z oddzielenia reguł gry od obiektów stanowych, które reprezentują poszczególne elementy, jest natychmiastowe rozwiązanie problemu. Generalnie w ogóle nie mieszałbym modelu reguł z modelem państwowym podczas wdrażania turowej gry planszowej.
PRZYJEDŹ
2
Jeśli nie chcesz rozdzielać reguł, możesz zdefiniować reguły ruchu w sześciu obiektach PieceType (nie klasach!). W każdym razie uważam, że najlepszym sposobem na uniknięcie tego rodzaju problemów jest oddzielenie problemów i zastosowanie dziedziczenia tylko wtedy, gdy jest to naprawdę przydatne.
PRZYJDŹ

Odpowiedzi:

10

czy istnieje czyste rozwiązanie OOP bez instanceof, getClass () itp., które pozwoliłyby na tego rodzaju rozszerzalność?

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ł PiecePresenterinterfejs i wyglądałby tak:

class PiecePresenter implements PieceOutput {

  BoardPresenter board;
  Image pieceImage;

  @Override
  PiecePresenter(BoardPresenter board, Image image) {
    public void display(int rank, int file) {
      board.display(pieceImage, rank, file);
    } 
  }
}

Konstrukcja wyglądałaby mniej więcej tak:

rookWhiteImage = new Image("Rook-White.png");
PieceOutput rookWhiteOutPort = new PiecePresenter(boardPresenter, rookWhiteImage);
PieceInput rookWhiteInPort = new Rook(rookWhiteOutPort);
board[0, 0] = rookWhiteInPort;

Zastosowanie wyglądałoby mniej więcej tak:

board[rank, file].display(rank, file);

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 :

wprowadź opis zdjęcia tutaj

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.

candied_orange
źródło
2
Myślę, że to prawdopodobnie dobra odpowiedź (+1), ale nie jestem przekonany, że twoje rozwiązanie jest dobrym przykładem czystej architektury: encja Rook teraz bardzo bezpośrednio zawiera odniesienie do prezentera. Czy to nie jest ten rodzaj sprzężenia, któremu stara się zapobiegać Czysta Architektura? A twoja odpowiedź nie jest do końca jasna: mapowanie między bytami i prezenterami jest teraz obsługiwane przez tego, kto tworzy instancję. Jest to prawdopodobnie bardziej eleganckie niż alternatywne rozwiązania, ale jest to trzecie miejsce, które należy zmodyfikować po dodaniu nowych bytów - teraz nie jest to gość, ale fabryka.
amon
@amon, jak powiedziałem, brakuje warstwy przypadku użycia. Terry Pratchett nazwał takie rzeczy „kłamstwami dla dzieci”. Staram się nie tworzyć przytłaczającego przykładu. Jeśli uważasz, że muszę zostać zabrany do szkoły, jeśli chodzi o czystą architekturę, zapraszam do zabrania mnie tutaj .
candied_orange
przepraszam, nie, nie chcę cię uczyć, chcę tylko lepiej zrozumieć ten wzór architektury. Dosłownie natknąłem się na koncepcje „czystej architektury” i „architektury heksagonalnej” niespełna miesiąc temu.
amon
@amon w takim przypadku dobrze się przyjrzyj, a następnie zabierz mnie do zadania. Z przyjemnością bym to zrobił. Nadal sam zastanawiam się nad tym. W tej chwili pracuję nad uaktualnieniem projektu Pythona opartego na menu do tego stylu. Krytyczny przegląd Clean Architecture można znaleźć tutaj .
candied_orange
1
Tworzenie instancji @lishaak może nastąpić w trybie głównym. Warstwy wewnętrzne nie wiedzą o warstwach zewnętrznych. Zewnętrzne warstwy wiedzą tylko o interfejsach.
candied_orange
5

Co z tym:

Twój model (klasy figur) ma wspólne metody, których możesz potrzebować również w innym kontekście:

interface ChessFigure {
  String getPlayerColor();
  String getFigureName();
}

Obrazy, które mają być użyte do wyświetlenia określonej figury, otrzymują nazwy plików według schematu nazewnictwa:

King-White.png
Queen-Black.png

Następnie możesz załadować odpowiedni obraz bez uzyskiwania dostępu do informacji o klasach Java.

new File(FIGURE_IMAGES_DIR,
         String.format("%s-%s.png",
                       figure.getFigureName(),
                       figure.getPlayerColor)));

Interesuje mnie również ogólne rozwiązanie tego rodzaju problemów, gdy muszę dołączyć pewne informacje (nie tylko obrazy) do potencjalnie rosnącego zestawu klas ”.

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.

Timothy Truckle
źródło
Uważam to za bardzo praktyczne i praktyczne rozwiązanie dla tego konkretnego scenariusza. Interesuje mnie również ogólne rozwiązanie tego rodzaju problemów, gdy muszę dołączyć pewne informacje (nie tylko obrazy) do potencjalnie rosnącego zestawu klas.
lishaak
3
@lishaak: tutaj ogólne podejście polega na zapewnieniu wystarczającej liczby metadanych w obiektach biznesowych, aby ogólne mapowanie do zasobu lub elementów interfejsu użytkownika mogło zostać wykonane automatycznie.
Doc Brown
2

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:

class Pawn : public Piece {
public:
    Vec2 position() const;
    /**
     The rest of the piece's interface
     */
}

class PawnView : public PieceView {
public:
    PawnView(Piece* piece) { _piece = piece; }
    void drawSelf(BoardView* board) const{
         board.drawPiece(_image, _piece->position);
    }
private:
    Piece* _piece;
    Image _image;
}

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.

Lasse Jacobs
źródło
OK, powiedzmy, że mam trochę Piece* p. Skąd mam wiedzieć, że muszę go utworzyć, PawnViewaby go wyświetlić, a nie a RookViewlub KingView? 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 przypadku PawnViewkonstruktor może również pobrać Pawn*nie tylko Piece*.
amon
Tak, przepraszam, konstruktor PawnView wziąłby pion *. I niekoniecznie musisz jednocześnie tworzyć pionek i pionek. Załóżmy, że istnieje gra, która może mieć 100 pionków, ale tylko 10 może być wizualnych jednocześnie, w tym przypadku możesz ponownie użyć widoków pionków dla wielu pionków.
Lasse Jacobs
I zgadzam się z rozwiązaniem @ CandiedOrange. Po prostu chciałbym podzielić się moimi 2 centami.
Lasse Jacobs
0

Podszedłbym do tego, tworząc Pieceogó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:

public abstract class Piece<T>
{
    T type;
    public Piece (T type) { this.type = type; }
    public T getType() { return type; }
}
enum ChessPieceType { PAWN, ... }
public class Pawn extends Piece<ChessPieceType>
{
    public Pawn () { super (ChessPieceType.PAWN); }

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:

 public class ChessPiece extends Piece<ChessPieceType> {
    ....
   boolean isMoveValid (Move move)
    {
         return getType().movePatterns().contains (move.asVector()) && ....


 public enum ChessPieceType {
    public abstract Set<Vector2D> movePatterns();
    PAWN {
         public Set<Vector2D> movePatterns () {
              return Util.makeSet(
                    new Vector2D(0, 1),
                    ....

(Oczywiście rzeczywiste implementacje muszą być bardziej złożone, ale mam nadzieję, że masz pomysł)

Jules
źródło
0

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.

public class Program
{
    public static void Main(string[] args)
    {
        new Rook(new Presenter { Image = "rook.png", Color = "blue" });
    }
}

public abstract class Piece
{
    public Presenter Presenter { get; private set; }
    public Piece(Presenter presenter)
    {
        this.Presenter = presenter;
    }
}

public class Pawn : Piece
{
    public Pawn(Presenter presenter) : base(presenter) { }
}

public class Rook : Piece
{
    public Rook(Presenter presenter) : base(presenter) { }
}

public class Presenter
{
    public string Image { get; set; }
    public string Color { get; set; }
}

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?

Świeża krew
źródło
Po pierwsze, utwór musi wiedzieć, że jest tam warstwa prezentacji i że wymaga zdjęć. Co się stanie, jeśli jakaś warstwa prezentacji nie wymaga obrazów? Po drugie, utwór musi zostać utworzony przez warstwę interfejsu użytkownika, ponieważ utwór nie może istnieć bez prezentera. Wyobraź sobie, że gra działa na serwerze, na którym nie jest potrzebny interfejs użytkownika. Nie możesz utworzyć wystąpienia, ponieważ nie masz interfejsu użytkownika.
lishaak
Można również zdefiniować reprezentator jako parametr opcjonalny.
Freshblood
0

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.

Świeża krew
źródło
To rozwiązanie jest z pewnością funkcjonalne i doceniam ten pomysł. Jest to jednak bardzo ograniczone do szachów. Jako przykładu użyłem szachy do zilustrowania ogólnego problemu (być może wyjaśniłem to w pytaniu). Proponowane rozwiązanie nie może być używane w innej domenie i, jak słusznie mówisz, nie ma możliwości rozszerzenia go o nowe obiekty biznesowe (części). Rzeczywiście istnieje wiele sposobów na rozszerzenie szachów o więcej elementów ...
lishaak