Zero obiektów zachowania w OOP - mój dylemat projektowania

94

Podstawową ideą OOP jest to, że dane i zachowanie (na podstawie tych danych) są nierozłączne i łączy je idea obiektu klasy. Obiekt ma dane i metody, które działają z tym (i innymi danymi). Oczywiście zgodnie z zasadami OOP obiekty, które są tylko danymi (jak struktury C) są uważane za anty-wzorzec.

Na razie w porządku.

Problem w tym, że zauważyłem, że mój kod wydaje się ostatnio coraz bardziej zmierzać w kierunku tego anty-wzorca. Wydaje mi się, że im bardziej staram się uzyskać informacje ukrywające się między klasami i luźno sprzężonymi projektami, tym bardziej moje klasy stają się mieszanką czystych danych bez klas zachowań i wszystkich zachowań bez klas danych.

Generalnie projektuję klasy w sposób, który minimalizuje ich świadomość istnienia innych klas i minimalizuje ich znajomość interfejsów innych klas. Egzekwuję to szczególnie od góry, klasy niższego poziomu nie wiedzą o klasach wyższego poziomu. Na przykład:

Załóżmy, że masz ogólny interfejs API gry karcianej. Masz klasę Card. Teraz ta Cardklasa musi określić widoczność dla graczy.

Jednym ze sposobów jest udział boolean isVisible(Player p)w Cardzajęciach.

Innym jest mieć boolean isVisible(Card c)na Playerzajęciach.

W szczególności nie podoba mi się pierwsze podejście, ponieważ zapewnia wiedzę o Playerklasie wyższego poziomu klasie niższej Card.

Zamiast tego wybrałem trzecią opcję, w której mamy Viewportklasę, która, biorąc pod uwagę a Playeri listę kart, określa, które karty są widoczne.

Jednak takie podejście pozbawia zarówno Cardi Playerzajęcia możliwej funkcji składowej. Po to zrobić dla innych rzeczy niż widoczności kart, pozostaje ci Cardi Playerklas, które zawierają wyłącznie dane jak cała funkcjonalność jest realizowana w innych klasach, które są głównie zajęcia z żadnych danych, tylko metody, podobnie jak Viewportpowyżej.

Jest to wyraźnie sprzeczne z główną ideą OOP.

Który jest właściwy sposób? Jak powinienem zająć się zadaniem zminimalizowania współzależności klas i zminimalizowania założonej wiedzy i sprzężenia, ale bez nakłaniania do dziwnego projektu, w którym wszystkie klasy niskiego poziomu zawierają tylko dane, a klasy wysokiego poziomu zawierają wszystkie metody? Czy ktoś ma jakieś trzecie rozwiązanie lub podejście do projektowania klas, które pozwala uniknąć całego problemu?

PS Oto inny przykład:

Załóżmy, że masz klasę, DocumentIdktóra jest niezmienna, ma tylko jednego BigDecimal idczłonka i moduł pobierający dla tego członka. Teraz musisz gdzieś mieć metodę, która daje DocumentIdzwrot Documenttego identyfikatora z bazy danych.

Czy ty:

  • Dodaj Document getDocument(SqlSession)metodę do DocumentIdklasy, nagle wprowadzając wiedzę na temat swojego persistence ( "we're using a database and this query is used to retrieve document by id"), API używanego do uzyskania dostępu do DB i tym podobnych. Również ta klasa wymaga teraz pliku JAR trwałości do kompilacji.
  • Dodaj inną klasę za pomocą metody Document getDocument(DocumentId id), pozostawiając DocumentIdklasę martwą, bez zachowania, klasę strukturalną.
RokL
źródło
21
Niektóre z twoich przesłanek są całkowicie błędne, co bardzo utrudni odpowiedź na podstawowe pytanie. Utrzymuj swoje pytania tak zwięzłe i bez opinii, jak to możliwe, a otrzymasz lepsze odpowiedzi.
pdr
31
„Jest to wyraźnie sprzeczne z główną ideą OOP” - nie, nie jest, ale jest powszechnym błędem.
Doc Brown
5
Myślę, że problem polega na tym, że w przeszłości istniały różne szkoły dla „Object Objectation” - sposób, w jaki pierwotnie miał na myśli Alan Kay (patrz geekswithblogs.net/theArchitectsNapkin/archive/2013/09/08/ … ), A sposób był nauczany w kontekście OOA / OOD przez ludzi z Rational ( en.wikipedia.org/wiki/Object-oriented_analysis_and_design ).
Doc Brown
21
To bardzo dobre pytanie i dobrze napisane - w przeciwieństwie do innych komentarzy, powiedziałbym. Pokazuje jasno, jak naiwne lub niekompletne są porady dotyczące struktury programu - i jak trudno to zrobić oraz jak nieosiągalny jest prawidłowy projekt w wielu sytuacjach, bez względu na to, jak bardzo staramy się to zrobić właściwie. I chociaż oczywistą odpowiedzią na konkretne pytanie jest wiele metod, podstawowy problem projektowania nadal występuje.
Thiago Silva
5
Kto powiedział, że lekcje bez zachowania są anty-wzorcem?
James Anderson

Odpowiedzi:

42

To, co opisujesz, jest znane jako anemiczny model domeny . Podobnie jak w przypadku wielu zasad projektowania OOP (takich jak Law of Demeter itp.), Nie warto pochylać się do tyłu, aby spełnić regułę.

Nie ma nic złego w posiadaniu worków wartości, o ile nie zagracają całego krajobrazu i nie polegają na innych przedmiotach, które mogłyby zrobić dla siebie .

Z pewnością byłby to zapach kodu, gdybyś miał osobną klasę tylko do modyfikowania właściwości Card- gdyby można było oczekiwać, że zajmie się nimi samodzielnie.

Ale czy to naprawdę zadanie Cardwiedzieć, dla kogo Playerjest widoczne?

A dlaczego wdrażać Card.isVisibleTo(Player p), ale nie Player.isVisibleTo(Card c)? Lub odwrotnie?

Tak, możesz spróbować wymyślić jakąś zasadę, jak to zrobiłeś - na przykład Playerbędąc na wyższym poziomie niż Card(?) - ale nie jest tak łatwo zgadnąć i będę musiał zajrzeć w więcej niż jedno miejsce znaleźć metodę.

Z biegiem czasu może to prowadzić do zgniłego kompromisu realizacji projektu isVisibleTona obu Card i Playerklasy, który moim zdaniem jest nie-nie. Dlaczego tak? Ponieważ już sobie wyobrażam ten haniebny dzień, kiedy player1.isVisibleTo(card1)zwróci inną wartość, niż card1.isVisibleTo(player1).myślę - to subiektywne - z założenia powinno to być niemożliwe .

Wzajemna widoczność kart i graczy powinna być lepiej zarządzana przez jakiś obiekt kontekstowy - czy to Viewport, Dealczy Game.

To nie jest równoznaczne z posiadaniem funkcji globalnych. W końcu może istnieć wiele jednoczesnych gier. Pamiętaj, że ta sama karta może być używana jednocześnie na wielu stołach. Czy stworzymy wiele Cardinstancji dla każdego asa pik?

I może nadal realizować isVisibleTona Card, ale przekazać obiekt kontekstu do niego i uczynić Cardpełnomocnikowi zapytania. Program do interfejsu, aby uniknąć wysokiego sprzężenia.

Jeśli chodzi o twój drugi przykład - jeśli identyfikator dokumentu składa się tylko z a BigDecimal, po co w ogóle tworzyć dla niego klasę opakowania?

Powiedziałbym, że wszystko czego potrzebujesz to DocumentRepository.getDocument(BigDecimal documentID);

Nawiasem mówiąc, podczas gdy nieobecne w Javie, są structw C #.

Widzieć

na przykład. Jest to język bardzo zorientowany obiektowo, ale nikt nie robi z niego wielkiego problemu.

Konrad Morawski
źródło
1
Tylko uwaga na temat struktur w języku C #: nie są to typowe struktury, tak jak je znasz w C. W rzeczywistości obsługują one także OOP z dziedziczeniem, enkapsulacją i polimorfizmem. Oprócz pewnych osobliwości główna różnica polega na tym, jak środowisko wykonawcze obsługuje instancje, gdy są przekazywane do innych obiektów: struktury są typami wartości, a klasy są typami referencyjnymi!
Aschratt
3
@Aschratt: Struktury nie obsługują dziedziczenia. Struktury mogą implementować interfejsy, ale struktury implementujące interfejsy zachowują się inaczej niż obiekty klasy, które robią podobnie. Chociaż możliwe jest, aby struktury zachowywały się trochę jak obiekty, najlepszy przypadek użycia dla struktur jest potrzebny, gdy chce się czegoś, co zachowuje się jak struktura C, a elementy, które się kapsułkuje, to albo prymitywy, albo niezmienne typy klas.
supercat
1
+1 za to, dlaczego „nie jest równoznaczne z posiadaniem funkcji globalnych”. Inni nie zajmowali się tym zbytnio. (Chociaż jeśli masz kilka talii, funkcja globalna nadal zwraca różne wartości dla różnych instancji tej samej karty).
Alexis
@ superupat Jest to warte osobnego pytania lub sesji czatu, ale obecnie nie jestem zainteresowany ani :-( Mówisz (w C #) „struktury, które implementują interfejsy zachowują się inaczej niż obiekty klasy, które robią podobnie”. Zgadzam się, że istnieją inne różnice behawioralne do rozważenia, ale AFAIK w następujący kod Interface iObj = (Interface)obj;, zachowanie iObjnie ma wpływu structlub classstatusu obj(oprócz tego, że będzie to box kopia w tym przydziału jeśli jest struct).
Mark Hurd
150

Podstawową ideą OOP jest to, że dane i zachowanie (na podstawie tych danych) są nierozłączne i łączy je idea obiektu klasy.

Popełniasz częsty błąd zakładając, że klasy są podstawową koncepcją w OOP. Klasy są tylko jednym szczególnie popularnym sposobem osiągnięcia enkapsulacji. Ale możemy pozwolić, aby to się przesuwało.

Załóżmy, że masz ogólny interfejs API gry karcianej. Masz kartę klasy. Teraz ta klasa kart musi określać widoczność dla graczy.

DOBRE NIEBO Kiedy grasz w brydża, pytasz siódemkę kier, kiedy nadszedł czas, aby zmienić rękę manekina z tajemnicy znanej tylko manekinowi na znany wszystkim? Oczywiście nie. To wcale nie dotyczy karty.

Jednym ze sposobów jest ustawienie wartości logicznej isVisible (Player p) w klasie Card. Innym jest posiadanie wartości logicznej isVisible (karta c) w klasie gracza.

Oba są okropne; nie rób żadnego z nich. Ani gracz, ani karta nie są odpowiedzialni za wdrożenie zasad gry w brydża!

Zamiast tego wybrałem trzecią opcję, w której mamy klasę Viewport, która, biorąc pod uwagę Gracza i listę kart, określa, które karty są widoczne.

Nigdy wcześniej nie grałem w karty z „rzutnią”, więc nie mam pojęcia, co ta klasa ma zawierać. I nie grał w karty z kilkoma taliami kart, niektórych graczy, stół i kopią Hoyle. Którą z tych rzeczy reprezentuje Viewport?

Jednak to podejście ograbia zarówno klasę karty, jak i gracza z możliwej funkcji członka.

Dobry!

Gdy zrobisz to dla innych rzeczy niż widoczność kart, pozostaniesz z klasami Card i Player, które zawierają wyłącznie dane, ponieważ cała funkcjonalność jest zaimplementowana w innych klasach, które są w większości klasami bez danych, a jedynie metodami, takimi jak powyższy Viewport. Jest to wyraźnie sprzeczne z główną ideą OOP.

Nie; podstawową ideą OOP jest to, że obiekty wyrażają swoje obawy . W twoim systemie karta nie przejmuje się zbytnio. Gracz też nie jest. To dlatego, że dokładnie modelujesz świat . W prawdziwym świecie właściwości to karty, które są istotne w grze, są niezwykle proste. Możemy zastąpić zdjęcia na kartach liczbami od 1 do 52 bez większych zmian w grze. Możemy zastąpić cztery osoby manekinami oznaczonymi jako Północ, Południe, Wschód i Zachód bez większych zmian w grze. Gracze i karty to najprostsze rzeczy w świecie gier karcianych. Reguły są skomplikowane, więc klasa reprezentująca reguły jest tam, gdzie powinna być komplikacja.

Teraz, jeśli jeden z twoich graczy jest sztuczną inteligencją, jego stan wewnętrzny może być bardzo skomplikowany. Ale ta sztuczna inteligencja nie określa, czy może zobaczyć kartę. Zasady to określają .

Oto jak zaprojektuję twój system.

Po pierwsze, karty są zaskakująco skomplikowane, jeśli istnieją gry z więcej niż jedną talią. Musisz rozważyć pytanie: czy gracze mogą rozróżnić dwie karty o tej samej wartości? Jeśli gracz pierwszy gra jednym z siedmiu kier, a następnie dzieje się coś, a następnie gracz drugi gra jednym z siedmiu kier, czy gracz trzeci może stwierdzić, że była to ta sama siódemka kier? Zastanów się dokładnie. Ale poza tym problemem karty powinny być bardzo proste; to tylko dane.

Następnie, jaka jest natura gracza? Gracz zużywa sekwencję widocznych działań i produkuje takie działanie .

Obiekt reguł koordynuje to wszystko. Reguły tworzą sekwencję widocznych akcji i informują graczy:

  • Gracz pierwszy, dziesiątka kier została Ci przekazana przez gracza trzeciego.
  • Gracz drugi, karta została przekazana graczowi jeden przez gracza trzy.

A następnie prosi gracza o akcję.

  • Gracz pierwszy, co chcesz zrobić?
  • Gracz pierwszy mówi: potrójny fromp.
  • Gracz pierwszy, jest to nielegalna akcja, ponieważ potrójne fromp wytwarza nie do obrony gambit.
  • Gracz pierwszy, co chcesz zrobić?
  • Gracz pierwszy mówi: odrzuć królową pik.
  • Gracz drugi, gracz pierwszy odrzucił królową pik.

I tak dalej.

Oddziel mechanizmy od zasad . Zasady gry powinny być zawarte w obiekcie zasad , a nie w kartach . Karty są tylko mechanizmem.

Eric Lippert
źródło
41
@gnat: Przeciwną opinią Alana Kay jest: „Właściwie stworzyłem termin„ obiektowy ”i mogę powiedzieć, że nie miałem na myśli C ++.” Są języki OO bez klas; JavaScript przychodzi na myśl.
Eric Lippert,
19
@gnat: Zgadzam się, że JS w obecnym kształcie nie jest świetnym przykładem języka OOP, ale pokazuje, że można łatwo zbudować język OO bez klas. Zgadzam się, że podstawową jednostką OO-ness w Eiffelu i C ++ jest klasa; nie zgadzam się z tym, że klasy są warunkiem koniecznym OO. Sine qua non z oo to obiekty, które hermetyzują zachowanie i komunikują się ze sobą poprzez dobrze zdefiniowanym interfejsie publicznym.
Eric Lippert
16
Zgadzam się z / @EricLippert, klasy nie są fundamentalne dla OO ani dziedziczenia, niezależnie od tego, co powie główny nurt. Osłonięcie danych, zachowania i odpowiedzialności jest jednak osiągnięte. Istnieją języki oparte na prototypach poza JavaScript, które są OO, ale bezklasowe. Szczególnie błędem jest koncentrowanie się na dziedziczeniu nad tymi pojęciami. To powiedziawszy, klasy są bardzo przydatnym sposobem organizowania enkapsulacji zachowań. To, że możesz traktować klasy jako obiekty (w językach prototypowych i odwrotnie) powoduje, że linia jest rozmyta.
Schwern
6
Rozważ to w ten sposób: jakie zachowanie pokazuje rzeczywista karta w realnym świecie? Myślę, że odpowiedź brzmi „brak”. Inne rzeczy działają na karcie. Sama karta, w prawdziwym świecie, dosłownie jest tylko informacją (4 trefl), bez żadnych wewnętrznych zachowań. Sposób wykorzystania tych informacji (czyli „karty”) zależy w 100% od czegoś / kogoś innego, czyli „zasad” i „graczy”. Te same karty mogą być używane do nieskończonej (a może nie do końca) różnorodności różnych gier przez dowolną liczbę różnych graczy. Karta jest tylko kartą i ma tylko właściwości.
Craig
5
@Montagist: Pozwól, że trochę wyjaśnię. Rozważ C. Myślę, że zgodziłbyś się, że C nie ma zajęć. Można jednak powiedzieć, że struktury są „klasami”, można tworzyć pola typu wskaźnika funkcji, można budować vtables, można tworzyć metody nazywane „konstruktorami”, które konfigurują vtables, tak aby niektóre struktury „dziedziczyły” od siebie, i tak dalej. Możesz emulować dziedziczenie klasowe w C. I możesz emulować je w JS. Ale zrobienie tego oznacza zbudowanie czegoś na języku, którego już nie ma.
Eric Lippert,
29

Masz rację, że łączenie danych i zachowania jest centralną ideą OOP, ale jest coś więcej. Na przykład enkapsulacja : OOP / programowanie modułowe pozwala nam oddzielić interfejs publiczny od szczegółów implementacji. W OOP oznacza to, że dane nigdy nie powinny być publicznie dostępne i powinny być wykorzystywane wyłącznie za pośrednictwem akcesoriów. Według tej definicji obiekt bez żadnych metod jest rzeczywiście bezużyteczny.

Klasa, która nie oferuje żadnych metod poza akcesoriami, jest w gruncie rzeczy nadmiernie skomplikowaną strukturą. Ale to nie jest złe, ponieważ OOP daje ci elastyczność do zmiany wewnętrznych szczegółów, czego nie robi struktura. Na przykład zamiast przechowywać wartość w polu elementu, można ją ponownie obliczyć za każdym razem. Albo algorytm tworzenia kopii zapasowych zostaje zmieniony, a wraz z nim stan, który należy śledzić.

Chociaż OOP ma pewne wyraźne zalety (zwłaszcza w porównaniu z prostym programowaniem proceduralnym), naiwnością jest dążenie do „czystego” OOP. Niektóre problemy nie są dobrze odwzorowane na podejście obiektowe i łatwiej je rozwiązać za pomocą innych paradygmatów. Kiedy napotykasz taki problem, nie nalegaj na gorsze podejście.

  • Rozważ obliczenie sekwencji Fibonacciego w sposób obiektowy . Nie mogę wymyślić zdrowego sposobu, aby to zrobić; proste programowanie strukturalne oferuje najlepsze rozwiązanie tego problemu.

  • Twoja isVisiblerelacja należy do obu klas, do żadnej, a właściwie do kontekstu . Zapisy bez zachowania są typowe dla programowania funkcjonalnego lub proceduralnego, które wydaje się najlepiej pasować do twojego problemu. Nie ma w tym nic złego

    static boolean isVisible(Card c, Player p);
    

    i nie ma nic złego w tym, Cardże nie ma żadnych metod poza ranki suitakcesoriów.

amon
źródło
11
@UMad tak, właśnie o to mi chodzi i nie ma w tym nic złego. Użyj właściwego paradygmatu <del> język </del> dla danego zadania. (Nawiasem mówiąc, większość języków oprócz Smalltalk nie jest czysto obiektowa. Np. Java, C # i C ++ obsługują programowanie imperatywne, strukturalne, proceduralne, modułowe, funkcjonalne i obiektowe. Wszystkie te paradygmaty inne niż OO są dostępne z jakiegoś powodu : abyś mógł z nich korzystać)
amon
1
Istnieje rozsądny sposób OO na wykonanie Fibonacciego, wywołanie fibonaccimetody na instancji integer. Chciałbym podkreślić, że OO dotyczy enkapsulacji, nawet w pozornie małych miejscach. Niech liczba całkowita wymyśli, jak wykonać zadanie. Później możesz poprawić implementację, dodać buforowanie, aby poprawić wydajność. W przeciwieństwie do funkcji, metody podążają za danymi, więc wszyscy dzwoniący korzystają z ulepszonej implementacji. Być może później zostaną dodane liczby całkowite o dowolnej dokładności, mogą być traktowane jak zwykłe liczby całkowite i mogą mieć własną fibonaccimetodę dostosowywania wydajności .
Schwern
2
@ Schwern, jeśli coś Fibonaccijest podklasą klasy abstrakcyjnej Sequence, sekwencja jest wykorzystywana przez dowolny zestaw liczb i jest odpowiedzialna za przechowywanie nasion, stanu, buforowania i iteratora.
George Reith,
2
Nie spodziewałem się, że „czysty OOP Fibonacci” będzie tak skuteczny w strzelaniu do kujona . Proszę przerwać dyskusję na ten temat w tych komentarzach, chociaż miała ona pewną wartość rozrywkową. Teraz zróbmy coś konstruktywnego dla odmiany!
amon
3
głupotą byłoby uczynić z Fibonacciego metodę liczb całkowitych, żebyś mógł powiedzieć, że to OOP. Jest to funkcja i powinna być traktowana jak funkcja.
immibis,
19

Podstawową ideą OOP jest to, że dane i zachowanie (na podstawie tych danych) są nierozłączne i łączy je idea obiektu klasy. Obiekt ma dane i metody, które działają z tym (i innymi danymi). Oczywiście zgodnie z zasadami OOP obiekty, które są tylko danymi (jak struktury C) są uważane za anty-wzorzec. (...) Jest to wyraźnie sprzeczne z główną ideą OOP.

To trudne pytanie, ponieważ opiera się na kilku błędnych przesłankach:

  1. Pomysł, że OOP jest jedynym prawidłowym sposobem pisania kodu.
  2. Idea, że ​​OOP jest dobrze zdefiniowaną koncepcją. Stało się to tak modnym hasłem, że trudno jest znaleźć dwie osoby, które mogą zgodzić się na temat OOP.
  3. Pomysł, że OOP polega na łączeniu danych i zachowania.
  4. Idea, że ​​wszystko jest / powinna być abstrakcją.

Nie będę się zbytnio poruszał # 1-3, ponieważ każdy z nich mógłby stworzyć własną odpowiedź i zachęca to do dyskusji opartej na opiniach. Uważam jednak, że idea „OOP polega na łączeniu danych i zachowania” jest szczególnie niepokojąca. Nie tylko prowadzi to do # 4, ale także prowadzi do idei, że wszystko powinno być metodą.

Istnieje różnica między operacjami definiującymi typ a sposobami korzystania z tego typu. Możliwość odzyskania itego elementu jest niezbędna w koncepcji tablicy, ale sortowanie jest tylko jedną z wielu rzeczy, które mogę wybrać z jednym. Sortowanie nie musi być niczym więcej niż metodą „stwórz nową tablicę zawierającą tylko parzyste elementy”.

OOP polega na użyciu obiektów. Przedmioty są tylko jednym ze sposobów osiągnięcia abstrakcji . Abstrakcja jest sposobem na uniknięcie niepotrzebnego łączenia w kodzie, a nie celem samym w sobie. Jeśli twoje pojęcie karty jest zdefiniowane wyłącznie przez wartość jej pakietu i rangi, możesz zaimplementować ją jako prostą krotkę lub rekord. Nie ma nieistotnych szczegółów, od których każda inna część kodu mogłaby tworzyć zależność. Czasami po prostu nie masz nic do ukrycia.

Nie stworzyłbyś isVisiblemetody tego Cardtypu, ponieważ bycie widocznym prawdopodobnie nie jest istotne dla twojego pojęcia karty (chyba że masz bardzo specjalne karty, które mogą zmienić się w półprzezroczyste lub nieprzezroczyste ...). Czy powinna to być metoda tego Playertypu? Cóż, prawdopodobnie nie jest to również decydująca cecha graczy. Czy powinien być częścią jakiegoś Viewportrodzaju? Ponownie zależy to od tego, jak definiujesz rzutnię, i czy pojęcie sprawdzania widoczności kart jest integralną częścią definiowania rzutni.

To bardzo możliwe, isVisiblepowinna to być po prostu darmowa funkcja.

Doval
źródło
1
+1 za zdrowy rozsądek zamiast bezmyślnego dronu.
prawej strony
Z linii, które przeczytałem, esej, który połączyłeś, wygląda jak ten jeden solidny tekst, którego od jakiegoś czasu nie miałem.
Arthur Havlicek
@ArthurHavlicek Trudniej jest śledzić, jeśli nie rozumiesz języków używanych w przykładowym kodzie, ale uważam, że jest dość pouczający.
Doval,
9

Oczywiście zgodnie z zasadami OOP obiekty, które są tylko danymi (jak struktury C) są uważane za anty-wzorzec.

Nie są. Obiekty Plain-Old-Data są idealnie poprawnym wzorcem i oczekiwałbym ich w każdym programie, który zajmuje się danymi, które muszą zostać utrwalone lub przesłane między różnymi obszarami twojego programu.

Podczas gdy twoja warstwa danych może buforować pełną Playerklasę podczas odczytu z Playerstabeli, zamiast tego może być po prostu ogólną biblioteką danych, która zwraca POD z polami z tabeli, którą przekazuje do innego obszaru twojego programu, który konwertuje POD gracza do konkretnej Playerklasy.

Użycie obiektów danych, wpisanych lub bez wpisania, może nie mieć sensu w twoim programie, ale to nie czyni z nich anty-wzorca. Jeśli mają sens, użyj ich, a jeśli nie, nie rób tego.

DougM
źródło
5
Nie zgadzaj się z tym, co powiedziałeś, ale to wcale nie odpowiada na pytanie. To powiedziawszy, obwiniam pytanie bardziej niż odpowiedź.
pdr
2
Dokładnie, karty i dokumenty to tylko pojemniki z informacjami, nawet w prawdziwym świecie, a każdy „wzorzec”, który nie może sobie z tym poradzić, musi zostać zignorowany.
JeffO
1
Plain-Old-Data objects are a perfectly valid pattern Nie powiedziałem, że nie, mówię, że to źle, gdy wypełniają całą dolną połowę aplikacji.
RokL
8

Osobiście uważam, że projektowanie oparte na domenie pomaga wyjaśnić ten problem. Pytanie, które zadaję, brzmi: jak opisać grę karcianą ludziom? Innymi słowy, co modeluję? Jeśli rzecz, którą modeluję, zawiera słowo „rzutnia” i koncepcję pasującą do jej zachowania, utworzę obiekt rzutni i sprawię, że zrobi to, co logicznie powinien.

Jeśli jednak nie mam koncepcji widoku w mojej grze, myślę, że potrzebuję tego, ponieważ w przeciwnym razie kod „wydaje się zły”. Dwa razy myślę o dodaniu go do mojego modelu domeny.

Słowo model oznacza, że ​​budujesz reprezentację czegoś. Przestrzegam przed wprowadzaniem klasy, która reprezentuje coś abstrakcyjnego poza rzeczą, którą reprezentujesz.

Zredaguję, aby dodać, że możliwe, że możesz potrzebować koncepcji rzutni w innej części twojego kodu, jeśli potrzebujesz interfejsu z wyświetlaczem. Ale w kategoriach DDD byłby to problem dotyczący infrastruktury i istniałby poza modelem domeny.

RibaldEddie
źródło
Powyższa odpowiedź Lipperta jest lepszym przykładem tej koncepcji.
RibaldEddie,
5

Zwykle nie prowadzę autopromocji, ale faktem jest, że dużo pisałem o problemach związanych z projektowaniem OOP na moim blogu . Podsumowując kilka stron: nie powinieneś zaczynać projektowania od zajęć. Zaczynając od interfejsów lub interfejsów API i kodu kształtu stamtąd masz większe szanse na dostarczenie znaczących abstrakcji, dopasowanie specyfikacji i uniknięcie puchnięcia konkretnych klas za pomocą kodu jednorazowego użytku.

Jak to się ma do Card- Playerproblemu: Tworzenie ViewPortabstrakcję sens, jeśli myślisz Cardi Playerjako dwa niezależne biblioteki (co oznaczałoby Playerjest czasami używane bez Card). Jednak jestem skłonny myśleć o Playerblokadach Cardsi powinienem zapewnić Collection<Card> getVisibleCards ()im dostęp do nich. Oba te rozwiązania ( ViewPorti moje) są lepsze niż zapewnianie isVisiblejako metoda Cardlub Playerpod względem tworzenia zrozumiałych relacji kodu.

Rozwiązanie poza klasą jest o wiele, wiele lepsze dla DocumentId. Motywacja do uzależnienia (w zasadzie liczby całkowitej) od złożonej biblioteki bazy danych jest niewielka.

Arthur Havlicek
źródło
Lubię twojego bloga.
RokL
3

Nie jestem pewien, czy na właściwe pytanie udzielono odpowiedzi na odpowiednim poziomie. Na forum namawiałem mądrych do aktywnego zastanowienia się nad rdzeniem pytania tutaj.

U Mad przywołuje sytuację, w której uważa, że ​​programowanie zgodnie z jego rozumieniem OOP generalnie spowodowałoby, że wiele węzłów liściowych byłoby posiadaczami danych, podczas gdy jego interfejs API wyższego poziomu obejmuje większość zachowań.

Myślę, że temat stał się nieco styczny w kwestii tego, czy isVisible będzie zdefiniowane na karcie vs gracz; był to tylko przykład ilustrowany, choć naiwny.

Naciskałem na doświadczonych tutaj, aby spojrzeć na problem. Myślę, że istnieje dobre pytanie, na które usiłował U Mad. Rozumiem, że popchnąłbyś zasady i odpowiednią logikę do własnego obiektu; ale jak rozumiem, pytanie brzmi

  1. Czy można mieć proste konstrukcje posiadaczy danych (klasy / struktury; nie dbam o to, jak są one modelowane w tym pytaniu), które tak naprawdę nie oferują dużej funkcjonalności?
  2. Jeśli tak, jaki jest najlepszy lub preferowany sposób ich modelowania?
  3. Jeśli nie, w jaki sposób włączamy te części licznika danych do wyższych klas API (w tym zachowanie)

Mój widok:

Myślę, że zadajesz pytanie o ziarnistość, które trudno jest uzyskać w programowaniu obiektowym. Z mojego małego doświadczenia nie uwzględniłbym bytu w moim modelu, który sam w sobie nie obejmowałby żadnego zachowania. Gdybym musiał, prawdopodobnie użyłem konstrukcji o strukturze, która jest zaprojektowana do przechowywania takiej abstrakcji w przeciwieństwie do klasy, która ma ideę enkapsulacji danych i zachowania.

Harsha
źródło
3
Problem polega na tym, że pytanie (i twoja odpowiedź) dotyczy tego, jak robić rzeczy „ogólnie”. Faktem jest, że nigdy nie robimy rzeczy „ogólnie”. Zawsze robimy określone rzeczy. Konieczne jest zbadanie naszych konkretnych rzeczy i porównanie ich z naszymi wymaganiami, aby ustalić, czy nasze konkretne rzeczy są odpowiednie do danej sytuacji.
John Saunders
@JohnSaunders Widzę tutaj twoją mądrość i zgadzam się do pewnego stopnia, ale przed rozwiązaniem problemu konieczne jest również podejście koncepcyjne. W końcu pytanie tutaj nie jest tak otwarte, jak się wydaje. Myślę, że to prawidłowe pytanie OOD, z którym mierzy się każdy projektant OO w początkowych zastosowaniach OOP. Jakie jest twoje zdanie? Jeśli konkrecja pomoże, możemy omówić budowanie wybranego przez Ciebie przykładu.
Harsha,
Nie jestem w szkole od ponad 35 lat. W prawdziwym świecie bardzo mało cenię „podejście koncepcyjne”. W tym przypadku uważam, że doświadczenie jest lepszym nauczycielem niż Meyers.
John Saunders
Naprawdę nie rozumiem klasy dla danych w porównaniu do klasy dla zachowania. Jeśli odpowiednio wyodrębnisz swoje obiekty, nie ma różnicy. Wyobraź sobie Pointz getX()funkcją. Można sobie wyobrazić, że dostaje jeden z jego atrybutów, ale może również odczytać go z dysku lub Internetu. Otrzymywanie i ustawienie to zachowanie, a posiadanie zajęć, które właśnie to robią, jest całkowicie w porządku. Bazy danych tylko pobierają i ustawiają dane fwiw
Arthur Havlicek
@ArthurHavlicek: Wiedza o tym, czego klasa nie zrobi, jest często równie przydatna, jak wiedza o tym, co zrobi. Przydatne jest posiadanie w umowie czegoś, że będzie się zachowywał wyłącznie jako współużytkowany niezmienny posiadacz danych lub jako nic więcej niż niepodlegający współużytkowaniu.
supercat
2

Jedno powszechne źródło nieporozumień w OOP wynika z faktu, że wiele obiektów obejmuje dwa aspekty stanu: rzeczy, o których wiedzą i rzeczy, które o nich wiedzą. Dyskusje o stanie obiektów często ignorują ten drugi aspekt, ponieważ w ramach, w których odwołania do obiektów są rozwiązane, nie ma ogólnego sposobu na określenie, co można wiedzieć o każdym obiekcie, którego odniesienie kiedykolwiek było narażone na świat zewnętrzny.

Sugerowałbym, że prawdopodobnie przydatny byłby CardEntityobiekt, który łączy te aspekty karty w osobne elementy. Jeden element odnosi się do oznaczeń na karcie (np. „Diamentowy król” lub „Lava Blast; gracze mają szansę na unik-3 AC-3 lub odniesią obrażenia 2D6”). Ktoś może odnosić się do unikalnego aspektu stanu, takiego jak pozycja (np. Jest w talii, w ręce Joe lub na stole przed Larrym). Trzeci może odnosić się do tego (może nikt, może jeden gracz, a może wielu graczy). Aby upewnić się, że wszystko jest zsynchronizowane, miejsca, w których może znajdować się karta, nie byłyby zamknięte w postaci prostych pól, a raczej CardSpaceobiekty; aby przenieść kartę na pole, można by podać odniesienie do właściwegoCardSpaceobiekt; następnie usunąłby się ze starej przestrzeni i umieścił w nowej przestrzeni).

Jawne kapsułkowanie „kto wie o X” oddzielnie od „tego, co X wie” powinno pomóc uniknąć wielu nieporozumień. Czasami potrzebna jest ostrożność, aby uniknąć wycieków pamięci, szczególnie w przypadku wielu skojarzeń (np. Jeśli nowe karty mogą powstać, a stare karty znikną, należy zadbać o to, aby karty, które należy porzucić, nie pozostały wiecznie przymocowane do żadnych obiektów o długiej żywotności ), ale jeśli istnienie odniesień do obiektu będzie stanowić istotną część jego stanu, to całkowicie właściwy jest, aby sam obiekt jawnie zawierał takie informacje (nawet jeśli przekazuje on innej klasie zadania faktycznego zarządzania nim).

supercat
źródło
0

Jednak to podejście ograbia zarówno klasę karty, jak i gracza z możliwej funkcji członka.

A jak to źle / źle doradza?

Aby użyć podobnej analogii do przykładu z kartami, rozważ a Car, Drivera musisz ustalić, czy Drivermoże to prowadzić Car.

OK, więc zdecydowałeś, że nie chcesz, abyś Carwiedział, czy Driverma odpowiedni kluczyk, czy też nie, i z jakiegoś nieznanego powodu zdecydowałeś, że nie chcesz, abyś Driverwiedział o Carklasie (nie do końca miałeś ciało to również w twoim pierwotnym pytaniu). Zatem masz klasę pośredniczącą, coś w rodzaju Utilsklasy, która zawiera metodę z regułami biznesowymi w celu zwrócenia booleanwartości dla powyższego pytania.

Myślę, że to w porządku. Klasa pośrednicząca może teraz wymagać jedynie sprawdzenia kluczyków samochodowych, ale można ją ponownie przeanalizować, aby rozważyć, czy kierowca posiada ważne prawo jazdy, pod wpływem alkoholu lub w dystopijnej przyszłości, sprawdzić biometrię DNA. Dzięki enkapsulacji nie ma naprawdę większych problemów ze współistnieniem tych trzech klas.

hjk
źródło