Czy jest jakiś powód, aby używać klas „zwykłych starych danych”?


43

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 structsi 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?


1
To nie do końca odpowiada na twoje pytanie, ale wydaje się jednak istotne: stackoverflow.com/questions/36701/struct-like-objects-in-java
Adam Lear

2
Myślę, że termin, którego szukasz, to POD (Plain Old Data).
Gaurav,

9
Jest to typowy przykład programowania strukturalnego. Niekoniecznie zły, po prostu nie zorientowany obiektowo.
Konrad Rudolph,

1
nie powinno to być na przepełnieniu stosu?
Muad'Dib,

7
@ Muad'Diba: technicznie, to jest o programistów. Kompilator nie dba o to, czy używasz zwykłych starych struktur danych. Twój procesor prawdopodobnie to lubi (w sposób „Uwielbiam zapach danych prosto z pamięci podręcznej”). To ludzie, którzy się rozłączają, „czy to czyni moją metodologię mniej czystą?” pytania.
Shog9

Odpowiedzi:


67

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ę

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

niż

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

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.


25
Nie DoStuffWithpowinno być metodą z tej Bottleklasy 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.
Konrad Rudolph,

11
@Javier: Zatem Java też nie jest najlepszą odpowiedzią. Nacisk Javy na OO jest przytłaczający, język w zasadzie nie jest dobry w niczym innym.
Konrad Rudolph,

11
@JohnL: Oczywiście upraszczałem. Ale ogólnie obiekty w OO hermetyzują stan , a nie dane. To dobre, ale ważne rozróżnienie. Istotą OOP jest właśnie to, że nie ma za dużo danych. Jest wysyłania wiadomości między państwami, które tworzą nowy stan. Nie widzę, jak możesz wysyłać wiadomości do obiektu bez metod lub z niego. (I to jest oryginalna definicja OOP, więc nie uważam, że jest wadliwa.)
Konrad Rudolph,

13
@Konrad Rudolph: Właśnie dlatego wyraźnie skomentowałem tę metodę. Zgadzam się, że metody, które wpływają na instancje Butelka, powinny iść w tej klasie. Ale jeśli inny obiekt musi zmodyfikować swój stan na podstawie informacji w Butelce, to myślę, że mój projekt byłby całkiem poprawny.
Adam Lear

10
@Konrad, nie zgadzam się, że doStuffWithBottle powinien przejść do klasy butelek. Dlaczego butelka powinna wiedzieć, jak sobie z tym poradzić? doStuffWithBottle wskazuje, że coś innego zrobi coś z butelką. Jeśli butelka miałaby to w sobie, byłoby to szczelne połączenie. Gdyby jednak klasa Bottle miała metodę isFull (), byłoby to całkowicie odpowiednie.
Nemi

25

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 Bottleklasa danych):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Oto Bottleniektóre zachowania):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Pierwsza metoda narusza zasadę Tell-Don't-Ask, a utrzymując Bottległupotę, daliśmy do zrozumienia domniemaną wiedzę na temat butelek, takich jak to, co sprawia, że ​​jeden z nich pęka Cap, przechodząc w logikę spoza Bottleklasy. Musisz być na palcach, aby zapobiec tego rodzaju „wyciekom”, gdy zwykle polegasz na łapaczach.

Druga metoda pyta Bottletylko o to, co musi wykonać, i Bottledecyduje, 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.


1
Nie mogę uwierzyć, że ta odpowiedź nie ma głosów (cóż, teraz masz jeden). To może być prosty przykład, ale gdy OO jest wystarczająco nadużywane, dostajesz klasy usług, które zamieniają się w koszmary zawierające mnóstwo funkcji, które powinny były zostać zamknięte w klasach.
Alb

„przez starych programistów C / C ++, którzy nigdy nie dokonali przejścia (sic) do OO”? Programiści C ++ zwykle są ładni OO, ponieważ jest to język OO, tj. Cały sens używania C ++ zamiast C.
nappyfalcon 15.04.18

7

Jeśli jest to coś, czego potrzebujesz, to dobrze, ale proszę, proszę, zrób to tak

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

zamiast czegoś podobnego

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

Proszę.


14
Jedynym problemem jest to, że nie ma sprawdzania poprawności. Wszelkie logiki dotyczące tego, co jest prawidłową Butelką, powinny znajdować się w klasie Butelki. Jednak korzystając z proponowanej implementacji mogę mieć butelkę o ujemnej wysokości i średnicy - nie ma sposobu na egzekwowanie reguł biznesowych na obiekcie bez sprawdzania poprawności za każdym razem, gdy obiekt jest używany. Korzystając z drugiej metody, mogę upewnić się, że jeśli mam obiekt Bottle, to był on, jest i zawsze będzie prawidłowym obiektem Bottle zgodnie z moją umową.
Thomas Owens

6
Jest to jeden obszar, w którym .NET ma niewielką przewagę nad właściwościami, ponieważ można dodać moduł dostępu do właściwości z logiką sprawdzania poprawności, z taką samą składnią, jak przy uzyskiwaniu dostępu do pola. Można również zdefiniować właściwość, w której klasy mogą uzyskać wartość właściwości, ale nie można jej ustawić.
JohnL

3
@ user9521 Jeśli masz pewność, że kod nie spowoduje błędu krytycznego przy „złych” wartościach, wybierz metodę. Jeśli jednak potrzebujesz dodatkowej weryfikacji lub możliwości korzystania z leniwego ładowania lub innych kontroli podczas odczytu lub zapisu danych, użyj jawnych programów pobierających i ustawiających. Osobiście staram się zachować prywatność moich zmiennych i dla uzyskania spójności używam metod pobierających i ustawiających. W ten sposób wszystkie moje zmienne są traktowane tak samo, niezależnie od walidacji i / lub innych „zaawansowanych” technik.
Jonathan

2
Zaletą korzystania z konstruktora jest to, że znacznie ułatwia uczynienie klasy niezmienną. Jest to niezbędne, jeśli chcesz pisać wielowątkowy kod.
Fortyrunner,

7
Gdybym to możliwe, sprawiłbym, że pola były ostateczne. IMHO Wolałbym, aby pola były domyślnie ostateczne i zawierały słowa kluczowe dla zmiennych pól. np. var
Peter Lawrey

6

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

  1. 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.

  2. 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ć:

  1. 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.

  2. 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.


3

Tego rodzaju klasy są bardzo przydatne w przypadku średnich / dużych aplikacji z kilku powodów:

  • dość łatwo jest stworzyć kilka przypadków testowych i upewnić się, że dane są spójne.
  • przechowuje wszelkiego rodzaju zachowania, które wiążą się z tymi informacjami, dzięki czemu czas śledzenia błędów danych jest krótszy
  • Używanie ich powinno sprawić, że metoda args będzie lekka.
  • Podczas korzystania z ORM klasy te zapewniają elastyczność i spójność. Dodanie złożonego atrybutu, który jest obliczany na podstawie prostych informacji znajdujących się już w klasie, powoduje zapisanie jednej prostej metody. Jest to o wiele bardziej zwinne i produktywne niż sprawdzanie bazy danych i upewnianie się, że wszystkie bazy danych są załatane nowymi modyfikacjami.

Podsumowując, z mojego doświadczenia wynika, że ​​zwykle są bardziej przydatne niż irytujące.


3

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ę.


3

Zgadzam się z Anną Lear,

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ć metodę taką jak ...

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


3

Struktury mają swoje miejsce, nawet w Javie. Powinieneś ich używać tylko wtedy, gdy są spełnione następujące dwie rzeczy:

  • Musisz tylko agregować dane, które nie mają żadnego zachowania, np. Przekazać jako parametr
  • Nie ma znaczenia, jakie wartości mają agregowane dane

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:

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

Zamiast tego powinno być

double volume = bottle.calculateVolumeAsCylinder();

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:

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}

0

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 Nodeklasa struktury danych. Często może znacznie uprościć kod, zmniejszając liczbę interakcji między obiektami, jeśli powiedziano, że Nodemogą 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->Nodei Node->Tree, w przeciwieństwie do po prostu Tree->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->objectinterakcji, 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):

wprowadź opis zdjęcia tutaj

... w przeciwieństwie do tego:

wprowadź opis zdjęcia tutaj

... 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.


-1

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:

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends 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.)


1
Dlaczego twoje porty muszą rozszerzyć program SceneMember? dlaczego nie stworzyć klas portów do działania na SceneMembers?
Michael K

1
Dlaczego więc nie użyć standardowego wzorca interfejsu znaczników? Zasadniczo identyczny z pustą abstrakcyjną klasą podstawową, ale jest to o wiele bardziej powszechny idiom.
TMN

1
@Michael: Tylko z powodów teoretycznych zarezerwowałem Port do przyszłego użytku, ale ta przyszłość jeszcze nie dotarła i być może nigdy nie będzie. Nie zdawałem sobie sprawy, że w ogóle nie mają wspólnego, w przeciwieństwie do ich nazwisk. Zapłacę odszkodowanie za każdego, kto poniósł jakąkolwiek stratę w tej pustej klasie ...
ern0

1
@TMN: Typy SceneMember (pochodne) mają metodę getMemberType (), istnieje kilka przypadków, gdy Scena jest skanowana w poszukiwaniu podzbioru obiektów SceneMember.
ern0

4
To nie odpowiada na pytanie.
Eva
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.