W starszym kodzie czasami widzę klasy, które są niczym innym jak opakowaniami danych. coś jak:
class Bottle {
int height;
int diameter;
Cap capType;
getters/setters, maybe a constructor
}
Rozumiem OO, że klasy są strukturami danych i metodami operowania na nich. Wydaje się, że wyklucza to obiekty tego typu. Dla mnie są one niczym więcej structs
i poniekąd pokonaniem celu OO. Nie sądzę, żeby to było złe, chociaż może to być zapach kodu.
Czy istnieje przypadek, w którym takie przedmioty byłyby konieczne? Jeśli jest to często używane, czy sprawia, że projekt jest podejrzany?
java
code-quality
object-oriented
code-smell
Michael K.
źródło
źródło
Odpowiedzi:
Zdecydowanie nie jest to zło i nie pachnie kodem w mojej głowie. Kontenery danych są ważnymi obywatelami OO. Czasami chcesz obudować powiązane informacje razem. O wiele lepiej jest mieć taką metodę
niż
Użycie klasy pozwala również dodać dodatkowy parametr do Bottle bez modyfikowania każdego wywołującego DoStuffWithBottle. Możesz też podklasować Butelkę i, w razie potrzeby, zwiększyć czytelność i organizację kodu.
Istnieją również zwykłe obiekty danych, które można na przykład zwrócić w wyniku zapytania do bazy danych. Uważam, że w tym przypadku terminem tym jest „Obiekt transferu danych”.
W niektórych językach istnieją również inne uwagi. Na przykład w języku C # klasy i struktury zachowują się inaczej, ponieważ struktury są typem wartości, a klasy są typami referencyjnymi.
źródło
DoStuffWith
powinno być metodą z tejBottle
klasy w OOP (i powinna być niezmienna najprawdopodobniej też). To, co napisałeś powyżej, nie jest dobrym wzorcem w języku OO (chyba że korzystasz ze starszego API). Jest to jednak poprawny projekt w środowisku innym niż OO.Klasy danych są poprawne w niektórych przypadkach. DTO są dobrym przykładem wspomnianym przez Annę Lear. Ogólnie jednak powinieneś traktować je jako zalążek klasy, której metody jeszcze nie wyrosły. A jeśli napotykasz wiele z nich w starym kodzie, potraktuj je jako silny zapach kodu. Są często używane przez starych programistów C / C ++, którzy nigdy nie dokonali przejścia na programowanie OO i są oznaką programowania proceduralnego. Ciągłe poleganie na użytkownikach pobierających i ustawiających (lub jeszcze gorzej, na bezpośredni dostęp członków nieprywatnych) może wpędzić cię w kłopoty, zanim się zorientujesz. Rozważ przykład zewnętrznej metody, która potrzebuje informacji
Bottle
.Oto
Bottle
klasa danych):Oto
Bottle
niektóre zachowania):Pierwsza metoda narusza zasadę Tell-Don't-Ask, a utrzymując
Bottle
głupotę, daliśmy do zrozumienia domniemaną wiedzę na temat butelek, takich jak to, co sprawia, że jeden z nich pękaCap
, przechodząc w logikę spozaBottle
klasy. Musisz być na palcach, aby zapobiec tego rodzaju „wyciekom”, gdy zwykle polegasz na łapaczach.Druga metoda pyta
Bottle
tylko o to, co musi wykonać, iBottle
decyduje, czy jest krucha, czy większa niż określony rozmiar. Rezultatem jest znacznie luźniejsze sprzężenie między metodą a implementacją Bottle. Przyjemnym efektem ubocznym jest czystsza i bardziej wyrazista metoda.Rzadko będziesz korzystać z tak wielu pól obiektu bez pisania logiki, która powinna znajdować się w klasie z tymi polami. Dowiedz się, czym jest ta logika, a następnie przenieś ją tam, gdzie należy.
źródło
Jeśli jest to coś, czego potrzebujesz, to dobrze, ale proszę, proszę, zrób to tak
zamiast czegoś podobnego
Proszę.
źródło
Jak powiedziała @Anna, zdecydowanie nie jest zły. Pewnie możesz umieścić operacje (metody) w klasach, ale tylko jeśli chcesz . Nie mają do.
Pozwólcie mi na drobiazg z pomysłem, że trzeba umieścić operacje w klasach, wraz z ideą, że klasy są abstrakcjami. W praktyce zachęca to programistów do
Twórz więcej klas niż potrzeba (redundantna struktura danych). Gdy struktura danych zawiera więcej komponentów niż jest to konieczne, jest unormalizowana, a zatem zawiera niespójne stany. Innymi słowy, kiedy jest zmieniany, musi być zmieniany w więcej niż jednym miejscu, aby zachować spójność. Niewykonanie wszystkich skoordynowanych zmian powoduje niespójność i jest błędem.
Rozwiąż problem 1, wprowadzając metody powiadomień , aby w przypadku zmodyfikowania części A próbować propagować niezbędne zmiany w częściach B i C. Jest to główny powód, dla którego zaleca się stosowanie metod uzyskiwania i ustawiania akcesoriów. Ponieważ jest to zalecana praktyka, wydaje się, że usprawiedliwia problem 1, powodując więcej problemu 1 i więcej rozwiązania 2. Powoduje to nie tylko błędy związane z niepełnym wdrożeniem powiadomień, ale także z problemem obniżania wydajności niekontrolowanych powiadomień. Nie są to obliczenia nieskończone, tylko bardzo długie.
Te pojęcia są nauczane jako dobre rzeczy, na ogół przez nauczycieli, którzy nie musieli pracować w milionowych liniach aplikacji potworów, które były pełne tych problemów.
Oto, co próbuję zrobić:
Zachowaj dane tak znormalizowane, jak to możliwe, aby po wprowadzeniu zmian w danych dokonano ich w jak najmniejszej liczbie punktów kodowych, aby zminimalizować prawdopodobieństwo wprowadzenia niespójnego stanu.
Kiedy dane muszą być unormalizowane, a redundancja jest nieunikniona, nie używaj powiadomień, aby zachować spójność. Raczej toleruj przejściową niespójność. Rozwiąż niespójność z okresowymi przeglądami danych w procesie, który tylko to robi. Centralizuje to odpowiedzialność za utrzymanie spójności, unikając przy tym problemów z wydajnością i poprawnością, na które podatne są powiadomienia. Powoduje to, że kod jest znacznie mniejszy, bezbłędny i wydajny.
źródło
Tego rodzaju klasy są bardzo przydatne w przypadku średnich / dużych aplikacji z kilku powodów:
Podsumowując, z mojego doświadczenia wynika, że zwykle są bardziej przydatne niż irytujące.
źródło
W przypadku projektowania gier, narzut związany z tysiącami wywołań funkcji i detektorów zdarzeń może czasem sprawić, że warto mieć klasy przechowujące tylko dane i mieć inne klasy, które przechodzą przez wszystkie klasy tylko danych, aby wykonać logikę.
źródło
Zgadzam się z Anną Lear,
Czasami ludzie zapominają przeczytać Konwencje kodowania Java z 1999 r., Które wyraźnie pokazują, że tego rodzaju programowanie jest w porządku. Jeśli tego unikniesz, Twój kod będzie pachniał! (zbyt wielu pobierających / ustawiających)
Z konwencji kodu Java 1999: Jednym z przykładów odpowiednich zmiennych instancji publicznej jest przypadek, w którym klasa jest zasadniczo strukturą danych, bez zachowania. Innymi słowy, jeśli użyłbyś struct zamiast klasy (jeśli Java obsługuje struct), to należy upublicznić zmienne instancji klasy. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177
Przy prawidłowym użyciu POD (zwykłe stare struktury danych) są lepsze niż POJO, podobnie jak POJO są często lepsze niż EJB.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures
źródło
Struktury mają swoje miejsce, nawet w Javie. Powinieneś ich używać tylko wtedy, gdy są spełnione następujące dwie rzeczy:
W takim przypadku należy upublicznić pola i pominąć metody pobierające / ustawiające. Gettery i settery i tak są niezgrabne, a Java jest głupia, ponieważ nie ma właściwości takich jak przydatny język. Ponieważ obiekt podobny do struktury i tak nie powinien mieć żadnych metod, pola publiczne są najbardziej sensowne.
Jeśli jednak jedno z nich nie ma zastosowania, masz do czynienia z prawdziwą klasą. Oznacza to, że wszystkie pola powinny być prywatne. (Jeśli absolutnie potrzebujesz pola w bardziej dostępnym zakresie, użyj gettera / settera.)
Aby sprawdzić, czy domniemana struktura ma zachowanie, sprawdź, kiedy pola są używane. Jeśli wydaje się to naruszać powiedz, nie pytaj , musisz przenieść to zachowanie na swoją klasę.
Jeśli niektóre Twoje dane nie powinny ulec zmianie, musisz uczynić wszystkie te pola ostatecznymi. Możesz rozważyć uczynienie swojej klasy niezmienną . Jeśli potrzebujesz zweryfikować swoje dane, podaj weryfikację w ustawiaczach i konstruktorach. (Przydatną sztuczką jest zdefiniowanie prywatnego setera i zmodyfikowanie pola w klasie za pomocą tylko tego setera).
Twój przykład butelki prawdopodobnie nie przejdzie obu testów. Możesz mieć (wymyślony) kod, który wygląda następująco:
Zamiast tego powinno być
Gdybyś zmienił wysokość i średnicę, czy byłaby to ta sama butelka? Prawdopodobnie nie. Te powinny być ostateczne. Czy wartość ujemna jest odpowiednia dla średnicy? Czy Twoja butelka musi być wyższa niż szeroka? Czy czapka może być zerowa? Nie? Jak to sprawdzasz? Załóżmy, że klient jest głupi lub zły. ( Nie można odróżnić. ) Musisz sprawdzić te wartości.
Tak może wyglądać Twoja nowa klasa Bottle:
źródło
IMHO często nie ma wystarczającej liczby takich klas w systemach mocno zorientowanych obiektowo. Muszę to dokładnie zakwalifikować.
Oczywiście, jeśli pola danych mają szeroki zakres i widoczność, może to być wyjątkowo niepożądane, jeśli w bazie danych znajdują się setki lub tysiące miejsc, które manipulują takimi danymi. To prosi o kłopoty i trudności z utrzymaniem niezmienników. Ale jednocześnie nie oznacza to, że każda klasa w całej bazie kodów korzysta z ukrywania informacji.
Ale w wielu przypadkach takie pola danych będą miały bardzo wąski zakres. Bardzo prostym przykładem jest prywatna
Node
klasa struktury danych. Często może znacznie uprościć kod, zmniejszając liczbę interakcji między obiektami, jeśli powiedziano, żeNode
mogą po prostu składać się z surowych danych. Który służy jako oddzielenie od mechanizmu alternatywnego wersji może wymagać dwukierunkowe sprzężenie z, powiedzmy,Tree->Node
iNode->Tree
, w przeciwieństwie do po prostuTree->Node Data
.Bardziej złożonym przykładem mogą być systemy składające się z bytów, stosowane często w silnikach gier. W takich przypadkach komponenty są często tylko surowymi danymi i klasami takimi jak ta, którą pokazałeś. Jednak ich zasięg / widoczność jest zwykle ograniczona, ponieważ zazwyczaj istnieje tylko jeden lub dwa systemy, które mogą uzyskać dostęp do tego konkretnego typu elementu. W rezultacie nadal utrzymywanie niezmienników w tych systemach jest dość łatwe, a ponadto takie systemy mają bardzo mało
object->object
interakcji, dzięki czemu bardzo łatwo jest zrozumieć, co się dzieje na poziomie ptaka.W takich przypadkach możesz skończyć z czymś podobnym w zakresie interakcji (ten diagram wskazuje interakcje, a nie sprzężenie, ponieważ schemat sprzęgania może zawierać abstrakcyjne interfejsy dla drugiego obrazu poniżej):
... w przeciwieństwie do tego:
... a pierwszy typ systemu jest o wiele łatwiejszy do utrzymania i uzasadnienia pod względem poprawności, mimo że zależności faktycznie płyną w kierunku danych. Dostajesz znacznie mniej sprzężenia, głównie dlatego, że wiele rzeczy można przekształcić w surowe dane zamiast obiektów oddziałujących ze sobą, tworząc bardzo złożony wykres interakcji.
źródło
W świecie OOP istnieje nawet wiele dziwnych stworzeń. Kiedyś stworzyłem klasę, która jest abstrakcyjna i nie zawiera niczego, tylko po to, by być wspólnym rodzicem innych dwóch klas ... to klasa Port:
Więc SceneMember jest klasą podstawową, wykonuje całą pracę, która musi pojawić się na scenie: dodawać, usuwać, ustawiać identyfikator itp. Komunikat to połączenie między portami, ma swoje własne życie. InputPort i OutputPort zawierają własne funkcje we / wy.
Port jest pusty. Jest on po prostu używany do grupowania InputPort i OutputPort razem, powiedzmy, na liście portów. Jest pusty, ponieważ SceneMember zawiera wszystkie elementy, które działają jako członkowie, a InputPort i OutputPort zawierają zadania określone przez port. InputPort i OutputPort są tak różne, że nie mają wspólnych funkcji (oprócz SceneMember). Port jest pusty. Ale to legalne.
Może to antypattern , ale mi się podoba.
(To jest jak słowo „kolega”, które jest używane zarówno dla „żony”, jak i „męża”. Nigdy nie używasz słowa „kolega” dla konkretnej osoby, ponieważ znasz płeć jego / jej i to nie ma znaczenia jeśli jest w związku małżeńskim lub nie, więc zamiast tego używasz „kogoś”, ale nadal istnieje i jest używany w rzadkich, abstrakcyjnych sytuacjach, np. w tekście prawnym.)
źródło