Czy pobierający powinien zgłosić wyjątek, jeśli jego obiekt ma nieprawidłowy stan?

55

Często napotykam ten problem, szczególnie w Javie, nawet jeśli uważam, że jest to ogólny problem z OOP. To znaczy: zgłoszenie wyjątku ujawnia problem projektowy.

Załóżmy, że mam klasę, która ma String namepole i String surnamepole.

Następnie używa tych pól do utworzenia pełnego imienia i nazwiska osoby w celu wyświetlenia go na jakimś dokumencie, np. Na fakturze.

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

Teraz chcę wzmocnić zachowanie mojej klasy, zgłaszając błąd, jeśli displayCompleteNameOnInvoicezostanie wywołany przed przypisaniem nazwy. To dobry pomysł, prawda?

Mogę dodać kod wywołujący wyjątki do getCompleteNamemetody. Ale w ten sposób naruszam „niejawną” umowę z użytkownikiem klasy; ogólnie, pobierający nie powinni zgłaszać wyjątków, jeśli ich wartości nie są ustawione. Ok, to nie jest standardowy getter, ponieważ nie zwraca pojedynczego pola, ale z punktu widzenia użytkownika rozróżnienie może być zbyt subtelne, aby o tym myśleć.

Albo mogę rzucić wyjątek z wnętrza displayCompleteNameOnInvoice. Ale żeby to zrobić, powinienem przetestować bezpośrednio namelub surnamepola, a tym samym naruszyłbym abstrakcję reprezentowaną przez getCompleteName. Ta metoda odpowiada za sprawdzenie i utworzenie pełnej nazwy. Może nawet zdecydować, opierając decyzję na innych danych, że w niektórych przypadkach wystarczy surname.

Tak więc jedyną możliwością wydaje się zmienić semantyczną metodę getCompleteNamena composeCompleteName, co sugeruje bardziej „aktywne” zachowanie, a wraz z nim zdolność do zgłaszania wyjątku.

Czy to lepsze rozwiązanie projektowe? Zawsze szukam najlepszej równowagi między prostotą a poprawnością. Czy istnieje odniesienie do projektu dla tego problemu?

AgostinoX
źródło
8
„Ogólnie rzecz biorąc, pobierający nie powinni zgłaszać wyjątków, jeśli ich wartości nie są ustawione”. - Co zatem mają zrobić?
Rotem,
2
@scriptin Następnie, zgodnie z tą logiką, displayCompleteNameOnInvoicemoże po prostu rzucić wyjątek, jeśli getCompleteNamezwraca null, prawda?
Rotem,
2
@Rotem może. Pytanie dotyczy tego, czy powinno.
scriptin
22
Na ten temat programiści fałszerstwa wierzą w nazwy. Bardzo dobrze czytają, dlaczego „imię” i „nazwisko” są bardzo wąskimi pojęciami.
Lars Viklund,
9
Jeśli twój obiekt może mieć niepoprawny stan, to już spieprzyłeś.
user253751,

Odpowiedzi:

151

Nie pozwól, aby twoja klasa została zbudowana bez przypisania nazwy.

DeadMG
źródło
9
To ciekawe, ale dość radykalne rozwiązanie. Wiele razy twoje klasy muszą być bardziej płynne, pozwalając na stany, które stają się problemem tylko wtedy, gdy i kiedy zostaną wywołane określone metody, w przeciwnym razie skończysz z mnóstwem małych klas, które wymagają zbyt wielu wyjaśnień, aby je zastosować.
AgostinoX,
71
To nie jest komentarz. Jeśli zastosuje się do porady zawartej w odpowiedzi, nie będzie już miał opisanego problemu. To jest odpowiedź.
DeadMG,
14
@AgostinoX: Powinieneś uczynić swoje zajęcia płynnymi tylko wtedy, gdy jest to absolutnie konieczne. W przeciwnym razie powinny być jak najbardziej niezmienne.
DeadMG,
25
@gnat: robisz tutaj świetną robotę, kwestionując jakość każdego pytania i każdej odpowiedzi na PSE, ale czasami jesteś trochę zbyt grzeczny.
Doc Brown
9
Jeśli mogą być ważnie opcjonalne, nie ma powodu, aby rzucał w pierwszej kolejności.
DeadMG,
75

Odpowiedź dostarczona przez DeadMG prawie ją uwydatnia, ale pozwólcie, że sformułuję to nieco inaczej: Unikaj posiadania obiektów z nieprawidłowym stanem. Obiekt powinien być „cały” w kontekście wykonywanego zadania. Lub nie powinno istnieć.

Więc jeśli chcesz płynności, użyj czegoś takiego jak Wzorzec Konstruktora . Lub cokolwiek innego, co stanowi osobną reifikację obiektu w budowie w przeciwieństwie do „rzeczy rzeczywistej”, w której ten drugi ma zagwarantowany prawidłowy stan i jest tym, który faktycznie ujawnia operacje zdefiniowane dla tego stanu (np getCompleteName.).

back2dos
źródło
7
So if you want fluidity, use something like the builder pattern- płynność lub niezmienność (chyba że w jakiś sposób to masz na myśli). Jeśli ktoś chce stworzyć niezmienne obiekty, ale musi odroczyć ustawianie niektórych wartości, wówczas wzorcem do naśladowania jest ustawianie ich jeden po drugim na obiekcie konstruktora i tworzenie niezmiennego dopiero po zebraniu wszystkich wartości wymaganych do poprawnego stan ...
Konrad Morawski
1
@KonradMorawski: Jestem zdezorientowany. Wyjaśniasz lub zaprzeczasz mojemu oświadczeniu? ;)
back2dos
3
Próbowałem to uzupełnić ...
Konrad Morawski
40

Nie masz obiektu ze złym stanem.

Konstruktor lub konstruktor ma na celu ustawienie stanu obiektu na coś spójnego i użytecznego. Bez tej gwarancji pojawiają się problemy z nieprawidłowo zainicjowanym stanem w obiektach. Problem ten się komplikuje, jeśli masz do czynienia z współbieżnością i coś może uzyskać dostęp do obiektu, zanim całkowicie go skonfigurujesz.

Pytanie w tej części brzmi zatem: „dlaczego pozwalasz, aby imię lub nazwisko było zerowe?” Jeśli nie jest to coś, co jest prawidłowe w klasie, aby działało poprawnie, nie pozwól na to. Poproś konstruktora lub konstruktora, który poprawnie zweryfikuje utworzenie obiektu, a jeśli nie jest poprawny, podnieś problem w tym momencie. Możesz także skorzystać z jednej z istniejących @NotNulladnotacji , aby pomóc w komunikowaniu, że „to nie może być zerowe” i wymusić to w kodowaniu i analizie.

Dzięki tej gwarancji znacznie łatwiej jest zrozumieć kod i to, co robi, i nie trzeba rzucać wyjątków w dziwnych miejscach ani nadmiernie sprawdzać funkcji gettera.

Gettery, które robią więcej.

Jest na ten temat całkiem sporo. Masz ile logiki w getterach i co powinno być dozwolone w getterach i seterach? stąd i pobierający i ustawiający wykonujący dodatkową logikę na przepełnieniu stosu. Jest to problem, który pojawia się wielokrotnie w projektowaniu klas.

Rdzeń tego pochodzi z:

ogólnie, pobierający nie powinni zgłaszać wyjątków, jeśli ich wartości nie są ustawione

I masz rację. Nie powinni. Powinni wyglądać „głupio” na resztę świata i robić to, czego się spodziewają. Wprowadzenie zbyt dużej logiki prowadzi do problemów, w których naruszane jest prawo najmniejszego zdziwienia. I spójrzmy prawdzie w oczy, naprawdę nie chcesz owijać użytkowników, try catchponieważ wiemy, jak bardzo lubimy to robić w ogóle.

Są też sytuacje, w których musisz użyć getFoometody, takiej jak framework JavaBeans, i gdy masz coś z wywołania EL oczekującego na uzyskanie komponentu bean (tak, że <% bar.foo %>faktycznie wywołuje getFoo()klasę - odkładając na bok „jeśli komponent powinien tworzyć kompozycję, czy powinien które należy pozostawić w polu widzenia? ”, ponieważ łatwo można wymyślić sytuacje, w których jedna lub druga może wyraźnie być właściwą odpowiedzią)

Uświadom sobie również, że możliwe jest określenie danego gettera w interfejsie lub bycie częścią wcześniej ujawnionego publicznego interfejsu API dla klasy, która jest refaktoryzowana (poprzednia wersja miała właśnie „totalName”, która została zwrócona i przerwała się refaktoryzacja) na dwa pola).

Pod koniec dnia...

Rób to, co jest najłatwiejsze do uzasadnienia. Spędzisz więcej czasu na utrzymywaniu tego kodu niż na jego projektowaniu (chociaż im mniej czasu poświęcisz na projektowanie, tym więcej czasu poświęcisz na utrzymanie). Idealnym wyborem jest zaprojektowanie go w taki sposób, aby jego utrzymanie nie zajęło tyle czasu, ale nie siedź też i nie zastanawiaj się nad tym przez kilka dni - chyba że naprawdę jest to wybór projektowy, który będzie miał implikacje dni później .

Próba trzymania się semantycznej czystości istoty pobudzającej private T foo; T getFoo() { return foo; }spowoduje w pewnym momencie kłopoty. Czasami kod po prostu nie pasuje do tego modelu, a zniekształcenia, przez które przechodzimy, aby próbować go utrzymać w ten sposób, po prostu nie mają sensu ... i ostatecznie utrudniają zaprojektowanie.

Zaakceptuj czasami, że ideały projektu nie mogą być zrealizowane tak, jak chcesz w kodzie. Wytyczne są wytycznymi, a nie kajdanami.

Społeczność
źródło
2
Oczywiście mój przykład był tylko pretekstem do poprawy stylu kodowania poprzez rozumowanie, a nie kwestią blokowania. Ale jestem dość zakłopotany znaczeniem, jakie ty i inni przywiązujesz do konstruktorów. Teoretycznie dotrzymują obietnicy. W praktyce stają się kłopotliwe w utrzymywaniu z dziedziczeniem, nieużyteczne, gdy wyodrębnisz interfejs z klasy, nieprzystosowany przez javabeans i inne frameworki, takie jak wiosna, jeśli nie popełniam błędu. Pod parasolem idei „klasowej” żyje wiele różnych kategorii przedmiotów. Obiekt wartości może mieć konstruktor sprawdzający, ale jest to tylko jeden typ.
AgostinoX,
2
Umiejętność rozumowania kodu jest kluczem do umiejętności pisania kodu za pomocą interfejsu API lub do zachowania kodu. Jeśli klasa ma konstruktor, który pozwala jej być w nieprawidłowym stanie, znacznie trudniej jest przemyśleć „cóż, co to robi?” ponieważ teraz musisz wziąć pod uwagę więcej sytuacji niż projektant rozważanej lub zamierzonej klasy. To dodatkowe obciążenie koncepcyjne wiąże się z karą związaną z większą liczbą błędów i dłuższym czasem pisania kodu. Z drugiej strony, jeśli spojrzysz na kod i powiesz „imię i nazwisko nigdy nie będą miały wartości zerowej”, pisanie kodu przy ich użyciu staje się znacznie łatwiejsze.
2
Niekompletne niekoniecznie oznacza nieprawidłowe. Faktura bez paragonu może być w toku, a przedstawiony przez nią publiczny interfejs API odzwierciedlałby, że taki stan jest prawidłowy. Jeśli surnamelub namewartość null jest poprawnym stanem dla obiektu, odpowiednio go obsłuż. Ale jeśli nie jest to poprawny stan powodujący, że metody w klasie generują wyjątki, należy zapobiegać temu, aby znajdował się w tym stanie od momentu, w którym został zainicjowany. Możesz ominąć niekompletne obiekty, ale problematyczne jest przekazywanie tych, które są nieprawidłowe. Jeśli nazwisko zerowe jest wyjątkowe, postaraj się temu zapobiec wcześniej niż później.
2
Powiązany post na blogu do przeczytania: Projektowanie inicjalizacji obiektu i zwróć uwagę na cztery podejścia do inicjalizacji zmiennej instancji. Jednym z nich jest umożliwienie, aby znajdował się w nieprawidłowym stanie, choć zauważ, że zgłasza wyjątek. Jeśli próbujesz tego uniknąć, to podejście nie jest poprawne i będziesz musiał pracować z innymi podejściami - które polegają na zapewnieniu prawidłowego obiektu przez cały czas. Z bloga: „Obiekty, które mogą mieć nieprawidłowe stany, są trudniejsze w użyciu i trudniejsze do zrozumienia niż te, które są zawsze ważne”. i to jest klucz.
5
„Rób to, co jest najłatwiejsze do uzasadnienia.” To. To
Ben,
5

Nie jestem facetem od Javy, ale wydaje się, że jest to zgodne z obiema ograniczeniami, które przedstawiłeś.

getCompleteNamenie zgłasza wyjątku, jeśli nazwy są niezainicjowane, i displayCompleteNameOnInvoicetak jest.

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}
Rotem
źródło
Aby wyjaśnić, dlaczego jest to poprawne rozwiązanie: W ten sposób osoba dzwoniąca getCompleteName()nie musi dbać o to, czy właściwość jest rzeczywiście przechowywana, czy obliczana w locie.
Bart van Ingen Schenau
18
Aby wyjaśnić, dlaczego to rozwiązanie jest szaleństwem, musisz sprawdzić nieważność każdego połączenia getCompleteName, co jest ohydne.
DeadMG,
Nie, @DeadMG, musisz to sprawdzić tylko w przypadkach, w których wyjątek powinien zostać zgłoszony, jeśli getCompleteName zwróci null. Tak właśnie powinno być. To rozwiązanie jest poprawne.
Dawood ibn Kareem
1
@DeadMG Tak, ale nie wiemy, jakie INNE zastosowania są getCompleteName()w pozostałej części klasy, a nawet w innych częściach programu. W niektórych przypadkach zwrot nullmoże być dokładnie wymagany. Ale chociaż istnieje JEDNA metoda, której specyfikacją jest „wyrzucenie wyjątku w tym przypadku”, to DOKŁADNIE to, co powinniśmy zakodować.
Dawood ibn Kareem
4
Mogą wystąpić sytuacje, w których jest to najlepsze rozwiązanie, ale nie mogę sobie wyobrazić. W większości przypadków wydaje się to zbyt skomplikowane: jedna metoda rzuca niekompletne dane, inna zwraca null. Obawiam się, że dzwoniący prawdopodobnie użyje go nieprawidłowo.
śleske,
4

Wygląda na to, że nikt nie odpowiada na twoje pytanie.
W przeciwieństwie do tego, w co ludzie wierzą, „unikanie nieprawidłowych obiektów” nie jest często praktycznym rozwiązaniem.

Odpowiedź to:

Tak, powinno.

Prosty przykład: FileStream.Lengthw C # / .NET , który rzuca, NotSupportedExceptionjeśli plik nie jest widoczny.

Mehrdad
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
wałek klonowy
1

Masz całą sprawdzanie nazwy do góry nogami.

getCompleteName()nie musi wiedzieć, które funkcje będą z niego korzystać. Zatem brak połączenia namepodczas rozmowy telefonicznej displayCompleteNameOnInvoice()nie getCompleteName()jest obowiązkiem.

Ale namejest to wyraźny warunek wstępny displayCompleteNameOnInvoice(). Dlatego powinien on przejąć odpowiedzialność za sprawdzenie stanu (lub powinien przekazać odpowiedzialność za sprawdzenie innej metody, jeśli wolisz).

sampathsris
źródło