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 Card
klasa musi określić widoczność dla graczy.
Jednym ze sposobów jest udział boolean isVisible(Player p)
w Card
zajęciach.
Innym jest mieć boolean isVisible(Card c)
na Player
zajęciach.
W szczególności nie podoba mi się pierwsze podejście, ponieważ zapewnia wiedzę o Player
klasie wyższego poziomu klasie niższej Card
.
Zamiast tego wybrałem trzecią opcję, w której mamy Viewport
klasę, która, biorąc pod uwagę a Player
i listę kart, określa, które karty są widoczne.
Jednak takie podejście pozbawia zarówno Card
i Player
zajęcia możliwej funkcji składowej. Po to zrobić dla innych rzeczy niż widoczności kart, pozostaje ci Card
i Player
klas, 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 Viewport
powyż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ę, DocumentId
która jest niezmienna, ma tylko jednego BigDecimal id
członka i moduł pobierający dla tego członka. Teraz musisz gdzieś mieć metodę, która daje DocumentId
zwrot Document
tego identyfikatora z bazy danych.
Czy ty:
- Dodaj
Document getDocument(SqlSession)
metodę doDocumentId
klasy, 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ącDocumentId
klasę martwą, bez zachowania, klasę strukturalną.
źródło
Odpowiedzi:
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
Card
wiedzieć, dla kogoPlayer
jest widoczne?A dlaczego wdrażać
Card.isVisibleTo(Player p)
, ale niePlayer.isVisibleTo(Card c)
? Lub odwrotnie?Tak, możesz spróbować wymyślić jakąś zasadę, jak to zrobiłeś - na przykład
Player
bę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
isVisibleTo
na obuCard
iPlayer
klasy, który moim zdaniem jest nie-nie. Dlaczego tak? Ponieważ już sobie wyobrażam ten haniebny dzień, kiedyplayer1.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
,Deal
czyGame
.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
Card
instancji dla każdego asa pik?I może nadal realizować
isVisibleTo
naCard
, ale przekazać obiekt kontekstu do niego i uczynićCard
peł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ą
struct
w C #.Widzieć
http://msdn.microsoft.com/en-us/library/ah19swz4.aspx
http://msdn.microsoft.com/en-us/library/0taef578.aspx
na przykład. Jest to język bardzo zorientowany obiektowo, ale nikt nie robi z niego wielkiego problemu.
źródło
Interface iObj = (Interface)obj;
, zachowanieiObj
nie ma wpływustruct
lubclass
statusuobj
(oprócz tego, że będzie to box kopia w tym przydziału jeśli jeststruct
).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.
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.
Oba są okropne; nie rób żadnego z nich. Ani gracz, ani karta nie są odpowiedzialni za wdrożenie zasad gry w brydża!
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?
Dobry!
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:
A następnie prosi gracza o akcję.
I tak dalej.
Oddziel mechanizmy od zasad . Zasady gry powinny być zawarte w obiekcie zasad , a nie w kartach . Karty są tylko mechanizmem.
źródło
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
isVisible
relacja 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łegoi nie ma nic złego w tym,
Card
że nie ma żadnych metod pozarank
isuit
akcesoriów.źródło
fibonacci
metody na instancjiinteger
. 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ąfibonacci
metodę dostosowywania wydajności .Fibonacci
jest podklasą klasy abstrakcyjnejSequence
, sekwencja jest wykorzystywana przez dowolny zestaw liczb i jest odpowiedzialna za przechowywanie nasion, stanu, buforowania i iteratora.To trudne pytanie, ponieważ opiera się na kilku błędnych przesłankach:
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
i
tego 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ś
isVisible
metody tegoCard
typu, 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 tegoPlayer
typu? Cóż, prawdopodobnie nie jest to również decydująca cecha graczy. Czy powinien być częścią jakiegośViewport
rodzaju? 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,
isVisible
powinna to być po prostu darmowa funkcja.źródło
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ą
Player
klasę podczas odczytu zPlayers
tabeli, 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 konkretnejPlayer
klasy.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.
źródło
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.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.
źródło
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
-Player
problemu: TworzenieViewPort
abstrakcję sens, jeśli myśliszCard
iPlayer
jako dwa niezależne biblioteki (co oznaczałobyPlayer
jest czasami używane bezCard
). Jednak jestem skłonny myśleć oPlayer
blokadachCards
i powinienem zapewnićCollection<Card> getVisibleCards ()
im dostęp do nich. Oba te rozwiązania (ViewPort
i moje) są lepsze niż zapewnianieisVisible
jako metodaCard
lubPlayer
pod 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.źródło
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
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.
źródło
Point
zgetX()
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 fwiwJedno 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
CardEntity
obiekt, 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 raczejCardSpace
obiekty; aby przenieść kartę na pole, można by podać odniesienie do właściwegoCardSpace
obiekt; 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).
źródło
A jak to źle / źle doradza?
Aby użyć podobnej analogii do przykładu z kartami, rozważ a
Car
,Driver
a musisz ustalić, czyDriver
może to prowadzićCar
.OK, więc zdecydowałeś, że nie chcesz, abyś
Car
wiedział, czyDriver
ma odpowiedni kluczyk, czy też nie, i z jakiegoś nieznanego powodu zdecydowałeś, że nie chcesz, abyśDriver
wiedział oCar
klasie (nie do końca miałeś ciało to również w twoim pierwotnym pytaniu). Zatem masz klasę pośredniczącą, coś w rodzajuUtils
klasy, która zawiera metodę z regułami biznesowymi w celu zwróceniaboolean
wartoś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.
źródło