Mam trochę dyskusji z przyjacielem, czy te dwie praktyki są tylko dwiema stronami tej samej monety, czy też jedna z nich jest naprawdę lepsza.
Mamy funkcję, która pobiera parametr, wypełnia jego element członkowski, a następnie zwraca go:
Item predictPrice(Item item)
Uważam, że ponieważ działa na tym samym przekazywanym obiekcie, nie ma sensu zwracać przedmiotu. W rzeczywistości, jeśli cokolwiek, z perspektywy dzwoniącego, myli sprawy, ponieważ można oczekiwać, że zwróci nowy przedmiot, którego nie ma.
Twierdzi, że to nie robi różnicy, i nie miałoby to nawet znaczenia, gdyby stworzył nowy przedmiot i zwrócił go. Zdecydowanie się nie zgadzam z następujących powodów:
Jeśli masz wiele odniesień do przekazywanych elementów (lub wskaźników lub cokolwiek innego), przydzielenie nowego obiektu i zwrócenie go ma istotne znaczenie, ponieważ odniesienia te będą niepoprawne.
W językach, które nie są zarządzane, funkcja alokująca nową instancję twierdzi, że jest właścicielem pamięci, a zatem musielibyśmy zaimplementować metodę czyszczenia, która jest w pewnym momencie wywoływana.
Przydział na stercie jest potencjalnie drogi, dlatego ważne jest, czy wywoływana funkcja to robi.
Dlatego uważam, że bardzo ważne jest, aby móc zobaczyć za pomocą sygnatury metod, czy modyfikuje obiekt, czy przydziela nowy. W rezultacie uważam, że ponieważ funkcja modyfikuje jedynie przekazywany obiekt, sygnatura powinna być:
void predictPrice(Item item)
W każdej bazie kodu (co prawda bazach C i C ++, nie Java, w której pracujemy), z którymi pracowałem, powyższy styl był zasadniczo przestrzegany i utrzymywany przez znacznie bardziej doświadczonych programistów. Twierdzi, że ponieważ moja próbka baz kodów i współpracowników jest niewielka ze wszystkich możliwych baz kodów i współpracowników, a zatem moje doświadczenie nie jest prawdziwym wskaźnikiem tego, czy ktoś jest lepszy.
Jakieś myśli?
źródło
Item
jestclass Item ...
i nietypedef ...& Item
, lokalnyitem
jest już kopiąOdpowiedzi:
To naprawdę jest kwestia opinii, ale ze względu na to, co warto, zwrócenie zmodyfikowanego przedmiotu jest mylące, jeśli przedmiot został zmodyfikowany na miejscu. Osobno, jeśli
predictPrice
zamierza zmodyfikować przedmiot, powinien on mieć nazwę wskazującą, że to zrobi (podobnie jaksetPredictPrice
niektóre inne).Wolałbym (w kolejności)
To
predictPrice
była metodaItem
(modulo dobry powód, aby tego nie było, co może być w twoim ogólnym projekcie), która alboZwrócono przewidywaną cenę lub
Miał
setPredictedPrice
zamiast tego imięTo
predictPrice
się nie zmieniłoItem
, ale zwróciło przewidywaną cenęTo
predictPrice
byłavoid
metoda o nazwiesetPredictedPrice
To
predictPrice
zwróciłothis
(dla łączenia łańcuchowego z innymi metodami w dowolnej instancji, której jest częścią) (i zostało wywołanesetPredictedPrice
)źródło
a = doSomethingWith(b)
zmodyfikowałemb
i zwróciłem go wraz z modyfikacjami, które wysadziły mój kod później, myśląc, że wie, cob
miało w tym. :-)W paradygmacie projektowania obiektowego rzeczy nie powinny modyfikować obiektów poza samym obiektem. Wszelkie zmiany stanu obiektu należy wprowadzać metodami na obiekcie.
Zatem
void predictPrice(Item item)
jako funkcja członka innej klasy jest niepoprawna . Mogło to być akceptowalne w czasach C, ale w Javie i C ++ modyfikacje obiektu implikują głębsze sprzężenie z obiektem, co prawdopodobnie prowadziłoby do innych problemów projektowych na późniejszym etapie (po zmianie klasy i zmianie jej pól, teraz należy zmienić na „replacePrice” w innym pliku.Zwracając nowy obiekt, nie ma powiązanych skutków ubocznych, przekazany parametr nie ulega zmianie. Ty (metoda przewidywana) nie wiesz, gdzie jeszcze ten parametr jest używany. Czy
Item
gdzieś jest klucz do skrótu? czy robiąc to, zmieniłeś kod skrótu? Czy ktoś inny trzyma się tego, oczekując, że się nie zmieni?Te problemy projektowe zdecydowanie sugerują, że nie powinieneś modyfikować obiektów (w wielu przypadkach argumentowałbym za niezmiennością), a jeśli tak, zmiany stanu powinny być ograniczone i kontrolowane przez sam obiekt, a nie coś jeszcze poza klasą.
Przyjrzyjmy się, co się stanie, jeśli ktoś bawi się polami czegoś w haszu. Weźmy trochę kodu:
ideone
Przyznaję, że nie jest to najlepszy kod (bezpośredni dostęp do pól), ale służy on zademonstrowaniu problemu ze zmiennymi danymi używanymi jako klucz do HashMap, lub w tym przypadku po prostu umieszczanym w HashSet.
Dane wyjściowe tego kodu to:
Stało się tak, że hashCode użyty podczas wstawiania jest tam, gdzie obiekt znajduje się w haszu. Zmiana wartości użytych do obliczenia hashCode nie powoduje ponownego obliczenia samego skrótu. Istnieje niebezpieczeństwo, że dowolny zmienny obiekt będzie kluczem do skrótu.
Wracając do pierwotnego aspektu pytania, wywoływana metoda nie „wie”, w jaki sposób używany jest obiekt, który otrzymuje jako parametr. Podanie „pozwala mutować obiekt” jako jedyny sposób na to oznacza, że istnieje wiele subtelnych błędów, które mogą się wkraść. Jak utrata wartości w haszu ... chyba że dodasz więcej i hasz zostanie zmieniony - zaufaj mi , to paskudny błąd do wyśledzenia (straciłem wartość, dopóki nie dodałem 20 kolejnych elementów do mapy hashMap, a potem nagle pojawia się ponownie).
Powiązane: Nadpisywanie i zwracanie wartości argumentu użytego jako warunek instrukcji if, wewnątrz tej samej instrukcji if
źródło
predictPrice
nie powinno się bezpośrednio bawić zItem
członkami, ale co jest złego w wywoływaniu takich metodItem
? Jeśli jest to jedyny modyfikowany obiekt, być może możesz argumentować, że powinna to być metoda modyfikowanego obiektu, ale innym razem modyfikowanych jest wiele obiektów (które zdecydowanie nie powinny być tej samej klasy), szczególnie w raczej metody wysokiego poziomu.Zawsze stosuję praktykę nie mutowania obiektu innej osoby. To znaczy, tylko mutujące obiekty należące do klasy / struct / w której jesteś.
Biorąc pod uwagę następującą klasę danych:
To jest wporządku:
I to jest bardzo jasne:
Ale to jest zbyt błotniste i tajemnicze jak na mój gust:
źródło
Składnia jest różna w różnych językach i nie ma sensu pokazywanie fragmentu kodu, który nie jest specyficzny dla danego języka, ponieważ oznacza zupełnie różne rzeczy w różnych językach.
W Javie
Item
jest typem odniesienia - jest to typ wskaźników do obiektów, które są instancjamiItem
. W C ++ ten typ jest zapisany jakoItem *
(składnia tego samego jest różna w różnych językach). Tak więcItem predictPrice(Item item)
w Javie jest równoważny zItem *predictPrice(Item *item)
C ++. W obu przypadkach jest to funkcja (lub metoda), która pobiera wskaźnik do obiektu i zwraca wskaźnik do obiektu.W C ++
Item
(bez czegokolwiek innego) jest typem obiektu (zakładając, żeItem
jest to nazwa klasy). Wartość tego typu „to” obiekt iItem predictPrice(Item item)
deklaruje funkcję, która pobiera obiekt i zwraca obiekt. Ponieważ&
nie jest używany, jest przekazywany i zwracany przez wartość. Oznacza to, że obiekt jest kopiowany po przekazaniu i kopiowany po zwróceniu. Java nie ma odpowiednika, ponieważ Java nie ma typów obiektów.Więc co to jest? Wiele rzeczy, które zadajesz w pytaniu (np. „Wierzę, że działa na tym samym obiekcie, który jest przekazywany ...”) zależy od zrozumienia, co dokładnie jest przekazywane i zwracane tutaj.
źródło
Konwencja, do której się przyzwyczaiłem, opiera się na preferowaniu stylu, który jest jasny ze składni. Jeśli funkcja zmienia wartość, wolisz przekazać wskaźnik
Ten styl powoduje:
Nazywając to widzisz:
przewidywanie ceny (& my_item);
i wiesz, że zostanie zmodyfikowany przez twoje przywiązanie do stylu.
źródło
Chociaż nie mam silnych preferencji, głosowałbym, że zwracanie obiektu prowadzi do większej elastyczności interfejsu API.
Zauważ, że ma sens tylko wtedy, gdy metoda ma swobodę zwracania innego obiektu. Jeśli twój javadoc mówi,
@returns the same value of parameter a
to jest bezużyteczne. Ale jeśli mówi javadoc@return an instance that holds the same data than parameter a
, to zwrócenie tej samej instancji lub innej jest szczegółem implementacji.Na przykład w moim bieżącym projekcie (Java EE) mam warstwę biznesową; do przechowywania w DB (i przypisywania automatycznego identyfikatora) pracownikowi, powiedzmy pracownikowi, który mam
Oczywiście nie ma różnicy w tej implementacji, ponieważ JPA zwraca ten sam obiekt po przypisaniu identyfikatora. Teraz, jeśli przejdę na JDBC
Teraz w ????? Mam trzy opcje.
Zdefiniuj seter dla Id, a ja zwrócę ten sam obiekt. Brzydkie, bo
setId
będą dostępne wszędzie.Wykonaj ciężkie odbicie i ustaw identyfikator w
Employee
obiekcie. Brudne, ale przynajmniej ogranicza się docreateEmployee
zapisu.Podaj konstruktor, który kopiuje oryginał
Employee
i akceptuje równieżid
zestaw.Odzyskaj obiekt z bazy danych (może być potrzebny, jeśli niektóre wartości pól są obliczane w bazie danych lub jeśli chcesz wypełnić tabelę).
Teraz dla 1. i 2. możesz zwracać tę samą instancję, ale dla 3. i 4. zwracasz nowe instancje. Jeśli z podmiotu ustanowionego w interfejsie API ta „rzeczywista” wartość zostanie zwrócona przez metodę, masz więcej swobody.
Jedynym prawdziwym argumentem, który mogę temu przeciwstawić, jest to, że przy użyciu implementacji 1. lub 2. osoby korzystające z interfejsu API mogą przeoczyć przypisanie wyniku, a ich kod może ulec awarii, jeśli zmienisz implementację na 3. lub 4.).
W każdym razie, jak widać, moje preferencje opierają się na innych osobistych preferencjach (takich jak zakaz ustawiania identyfikatorów), więc może nie będzie to dotyczyło wszystkich.
źródło
Podczas kodowania w Javie należy spróbować dopasować użycie każdego obiektu typu zmiennego do jednego z dwóch wzorców:
Dokładnie jedna jednostka jest uważana za „właściciela” obiektu zmiennego i traktuje stan tego obiektu jako część swojego własnego. Inne podmioty mogą przechowywać referencje, ale powinny traktować referencje jako identyfikujące obiekt, który jest własnością innej osoby.
Pomimo tego, że obiekt jest modyfikowalny, nikt, kto posiada odwołanie do niego, nie może go mutować, co czyni instancję efektywnie niezmienną (zauważ, że z powodu dziwactw w modelu pamięci Javy nie ma dobrego sposobu, aby sprawić, by obiekt niezmiennie niezmienny miał wątek bezpieczna niezmienna semantyka bez dodawania dodatkowego poziomu pośredniego do każdego dostępu). Każdy byt, który enkapsuluje stan za pomocą odwołania do takiego obiektu i chce zmienić stan enkapsulowany, musi utworzyć nowy obiekt, który zawiera odpowiedni stan.
Nie lubię, gdy metody mutują podstawowy obiekt i zwracają odniesienie, ponieważ dają wrażenie dopasowania drugiego wzorca powyżej. Jest kilka przypadków, w których takie podejście może być pomocne, ale kod, który ma mutować obiekty, powinien wyglądać tak, jak powinien. kod podobny
myThing = myThing.xyz(q);
wygląda o wiele mniej, niż mutuje obiekt zidentyfikowany przez,myThing
niż po prostumyThing.xyz(q);
.źródło