Czy generalnie wysyłasz obiekty lub ich zmienne składowe do funkcji?

31

Co jest ogólnie przyjętą praktyką między tymi dwoma przypadkami:

function insertIntoDatabase(Account account, Otherthing thing) {
    database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue());
}

lub

function insertIntoDatabase(long accountId, long thingId, double someValue) {
    database.insertMethod(accountId, thingId, someValue);
}

Innymi słowy, czy ogólnie lepiej jest przekazywać całe obiekty, czy tylko pola, których potrzebujesz?

AJJ
źródło
5
Zależy to całkowicie od tego, do czego służy funkcja i jak odnosi się (lub nie odnosi) do danego obiektu.
MetaFight
To jest problem. Nie umiem powiedzieć, kiedy użyję jednego lub drugiego. Wydaje mi się, że zawsze mogę zmienić kod, aby dostosować oba podejścia.
AJJ
1
W kategoriach API (i wcale nie patrząc na implementacje), ta pierwsza jest abstrakcyjna i zorientowana na domenę (co jest dobre), podczas gdy druga nie jest (co jest złe).
Erik Eidt
1
Pierwszym podejściem byłoby bardziej trójwymiarowe OO. Ale powinno być jeszcze bardziej poprzez wyeliminowanie bazy danych słów z metody. Powinien to być „Store” lub „Persist” i czynić albo konto, albo coś (nie oba). Jako klient tej warstwy nie powinieneś być świadomy nośnika pamięci. Podczas pobierania konta należy podać identyfikator lub kombinację wartości właściwości (nie wartości pól), aby zidentyfikować pożądany obiekt. Lub / i zastosuj metodę wyliczania, która przechodzi wszystkie konta.
Martin Maat
1
Zazwyczaj oba byłyby błędne (lub raczej mniej niż optymalne). Sposób serializacji obiektu do bazy danych powinien być właściwością (funkcją składową) obiektu, ponieważ zazwyczaj zależy on bezpośrednio od zmiennych składowych obiektu. W przypadku zmiany elementów obiektu konieczna będzie również zmiana metody serializacji. Działa to lepiej, jeśli jest częścią obiektu
tofro

Odpowiedzi:

24

Żadna z nich nie jest ogólnie lepsza od drugiej. Jest to wezwanie do osądu, które należy wykonać indywidualnie dla każdego przypadku.

Ale w praktyce, kiedy jesteś w stanie podjąć tę decyzję, to dlatego, że możesz zdecydować, która warstwa w ogólnej architekturze programu powinna rozbijać obiekt na prymitywy, więc powinieneś pomyśleć o całym wywołaniu stosu , a nie tylko tej jednej metody, w której się obecnie znajdujesz. Prawdopodobnie zerwanie musi być gdzieś zrobione i nie miałoby sensu (lub byłoby niepotrzebnie podatne na błędy), aby zrobić to więcej niż jeden raz. Pytanie brzmi, gdzie powinno być to jedno miejsce.

Najłatwiejszym sposobem podjęcia tej decyzji jest zastanowienie się, jaki kod powinien lub nie powinien zostać zmieniony, jeśli obiekt zostanie zmieniony . Rozwińmy nieco twój przykład:

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account, data);
}
function insertIntoDatabase(Account account, Otherthing data) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(account.getId(), data.getId(), data.getSomeValue());
}

vs

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account.getId(), data.getId(), data.getSomeValue());
}
function insertIntoDatabase(long accountId, long dataId, double someValue) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(accountId, dataId, someValue);
}

W pierwszej wersji kod interfejsu użytkownika ślepo przekazuje dataobiekt i to do kodu bazy danych można wyodrębnić z niego przydatne pola. W drugiej wersji kod interfejsu użytkownika dzieli dataobiekt na przydatne pola, a kod bazy danych odbiera je bezpośrednio, nie wiedząc, skąd pochodzą. Kluczową implikacją jest to, że jeśli struktura dataobiektu miałaby się w jakiś sposób zmienić, pierwsza wersja wymagałaby zmiany tylko kodu bazy danych, podczas gdy druga wersja wymagałaby zmiany tylko kodu interfejsu użytkownika . To, która z tych dwóch wartości jest poprawna, zależy w dużej mierze od tego, jakie dane datazawiera obiekt, ale zwykle jest to bardzo oczywiste. Na przykład jeślidatajest ciągiem dostarczonym przez użytkownika, takim jak „20/05/1999”, powinno być do kodu interfejsu użytkownika, aby przekonwertować go na odpowiedni Datetyp przed przekazaniem.

Ixrec
źródło
8

Nie jest to wyczerpująca lista, ale weź pod uwagę niektóre z następujących czynników przy podejmowaniu decyzji, czy obiekt powinien zostać przekazany do metody jako argument:

Czy obiekt jest niezmienny? Czy funkcja jest „czysta”?

Skutki uboczne są ważną kwestią do rozważenia przy utrzymywaniu kodu. Gdy widzisz kod z wieloma zmiennymi obiektami stanowymi przesyłanymi w dowolnym miejscu, kod ten jest często mniej intuicyjny (w taki sam sposób, jak globalne zmienne stanu często mogą być mniej intuicyjne), a debugowanie często staje się trudniejsze i czasochłonne trawiący.

Zasadniczo staraj się zapewnić, tak dalece, jak jest to racjonalnie możliwe, że wszelkie obiekty, które przekazujesz metodzie, są wyraźnie niezmienne.

Unikaj (ponownie, o ile jest to racjonalnie możliwe) jakiegokolwiek projektu, w którym oczekuje się zmiany stanu argumentu w wyniku wywołania funkcji - jednym z najsilniejszych argumentów za tym podejściem jest zasada najmniejszego zdziwienia ; tzn. ktoś czytający twój kod i widziący argument przekazany do funkcji „mniej prawdopodobne” spodziewa się zmiany jej stanu po powrocie funkcji.

Ile argumentów ma już metoda?

Metody z nadmiernie długimi listami argumentów (nawet jeśli większość z tych argumentów ma wartości „domyślne”) zaczynają wyglądać jak zapach kodu. Czasami takie funkcje są jednak konieczne i możesz rozważyć utworzenie klasy, której jedynym celem jest działanie jak obiekt parametru .

Takie podejście może wymagać niewielkiej ilości dodatkowego mapowania kodu typu „kocioł” z obiektu „źródłowego” na obiekt parametru, ale jest to dość niski koszt zarówno pod względem wydajności, jak i złożoności, a ponadto istnieje szereg korzyści w zakresie oddzielenia i niezmienność obiektu.

Czy przekazywany obiekt należy wyłącznie do „warstwy” w aplikacji (na przykład ViewModel lub jednostka ORM?)

Pomyśl o separacji problemów (SoC) . Czasami zadając sobie pytanie, czy obiekt „należy” do tej samej warstwy lub modułu, w którym istnieje twoja metoda (np. Ręcznie zwijana biblioteka otoki API lub podstawowa warstwa logiki biznesowej itp.), Może poinformować, czy obiekt ten naprawdę powinien zostać przekazany do tego metoda.

SoC jest dobrym fundamentem do pisania czystego, luźno sprzężonego, modułowego kodu. na przykład idealnie nie należy przekazywać obiektu encji ORM (mapowanie między kodem a schematem bazy danych) w warstwie biznesowej lub, co gorsza, w warstwie prezentacji / interfejsu użytkownika.

W przypadku przekazywania danych między „warstwami” zwykle przekazanie do metody parametrów zwykłych danych jest zwykle lepsze niż przekazanie obiektu z „niewłaściwej” warstwy. Chociaż prawdopodobnie dobrym pomysłem jest posiadanie osobnych modeli, które istnieją na „właściwej” warstwie, na którą można zamiast tego zmapować mapę.

Czy sama funkcja jest po prostu zbyt duża i / lub złożona?

Gdy funkcja potrzebuje wielu elementów danych, warto zastanowić się, czy ta funkcja przyjmuje zbyt wiele obowiązków; poszukaj potencjalnych możliwości refaktoryzacji przy użyciu mniejszych obiektów i krótszych, prostszych funkcji.

Czy funkcja powinna być obiektem polecenia / zapytania?

W niektórych przypadkach związek między danymi a funkcją może być bliski; w takich przypadkach zastanów się, czy odpowiedni byłby obiekt polecenia lub obiekt zapytania .

Czy dodanie parametru obiektu do metody zmusza zawierającą klasę do przyjęcia nowych zależności?

Czasami najsilniejszym argumentem dla argumentów „Zwykłe stare dane” jest po prostu to, że klasa odbierająca jest już starannie zamknięta, a dodanie parametru obiektu do jednej z jej metod zanieczyściłoby klasę (lub jeśli klasa jest już zanieczyszczona, wtedy będzie pogorszyć istniejącą entropię)

Czy naprawdę potrzebujesz ominąć cały obiekt, czy potrzebujesz tylko niewielkiej części interfejsu tego obiektu?

Zastanów się nad zasadą segregacji interfejsu w odniesieniu do twoich funkcji - tzn. Kiedy przekazujesz obiekt, powinno ono zależeć tylko od części interfejsu tego argumentu, którego on (funkcja) faktycznie potrzebuje.

Ben Cottrell
źródło
5

Kiedy tworzysz funkcję, domyślnie deklarujesz jakąś umowę z kodem, który ją wywołuje. „Ta funkcja pobiera te informacje i zamienia je w inną rzecz (prawdopodobnie z efektami ubocznymi)”.

Czy więc umowa powinna logicznie dotyczyć obiektów (niezależnie od tego, jak są one zaimplementowane), czy też pól, które akurat są częścią tych innych obiektów. W każdym razie dodajesz sprzężenie, ale jako programista musisz zdecydować, do kogo należy.

Ogólnie rzecz biorąc , jeśli jest niejasne, faworyzuj najmniejsze dane niezbędne do działania funkcji. Często oznacza to przekazywanie tylko pól, ponieważ funkcja nie potrzebuje innych rzeczy znalezionych w obiektach. Ale czasami przyjęcie całego obiektu jest bardziej poprawne, ponieważ powoduje mniejszy wpływ, gdy rzeczy nieuchronnie się zmienią w przyszłości.

Telastyn
źródło
Dlaczego nie zmienisz nazwy metody na insertAccountIntoDatabase lub przekażesz inny typ ?. Przy pewnej liczbie argumentów użycie obj ułatwia odczytanie kodu. W twoim przypadku wolę pomyśleć, czy nazwa metody wyjaśnia, co zamierzam wstawić, zamiast tego, jak to zrobię.
Laiv
3

To zależy.

Aby rozwinąć, parametry, które akceptuje twoja metoda, powinny semantycznie odpowiadać temu, co próbujesz zrobić. Rozważ EmailInvitertrzy możliwe implementacje invitemetody:

void invite(String emailAddressString) {
  invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
  ...
}
void invite(User user) {
  invite(user.getEmailAddress());
}

Przekazywanie miejsca, w Stringktórym powinieneś przekazać, EmailAddressjest wadliwe, ponieważ nie wszystkie ciągi znaków są adresami e-mail. EmailAddressKlasa lepiej semantycznie odpowiada za zachowanie metody. Jednak przekazywanie Userjest również wadliwe, ponieważ dlaczego, u licha, powinno się EmailInviterograniczać do zapraszania użytkowników? Co z firmami? Co jeśli czytasz adresy e-mail z pliku lub wiersza poleceń i nie są one powiązane z użytkownikami? Listy mailingowe? I tak dalej.

Istnieje kilka znaków ostrzegawczych, których można użyć jako wskazówek. Jeśli używasz prostego jak typ wartości Stringlub intale nie wszystkie ciągi lub ints są ważne czy jest coś „specjalne” o nich, należy używać bardziej sensowne typu. Jeśli używasz obiektu, a jedyne, co robisz, to wywoływanie gettera, powinieneś zamiast tego przekazać obiekt bezpośrednio do gettera. Te wytyczne nie są ani twarde, ani szybkie, ale niewiele jest.

Jacek
źródło
0

Clean Code zaleca posiadanie jak najmniejszej liczby argumentów, co oznacza, że ​​Object byłby zwykle lepszym podejściem i myślę, że ma to jakiś sens. dlatego

insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));

jest bardziej czytelnym połączeniem niż

insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );
jakiś facet
źródło
nie mogę się zgodzić. Obie nie są synonimami. Tworzenie 2 nowych instancji obiektów tylko w celu przekazania ich do metody nie jest dobre. Użyłbym insertIntoDatabase (myAccount, myOtherthing) zamiast jednej z twoich opcji.
jwenting
0

Omiń obiekt, a nie jego stan składowy. Jest to zgodne z obiektowymi zasadami enkapsulacji i ukrywania danych. Ujawnienie wnętrzności obiektu w różnych interfejsach metod, w których nie jest to konieczne, narusza podstawowe zasady OOP.

Co się stanie, jeśli zmienisz pola w Otherthing? Może zmienisz typ, dodasz pole lub usuniesz pole. Teraz wszystkie metody, takie jak te wymienione w pytaniu, muszą zostać zaktualizowane. Jeśli ominiesz obiekt, nie będzie żadnych zmian interfejsu.

Jedynym momentem, w którym powinieneś napisać metodę akceptującą pola na obiekcie, jest napisanie metody pobierania obiektu:

public User getUser(String primaryKey) {
  return ...;
}

W momencie wykonywania tego wywołania kod wywołujący nie ma jeszcze odwołania do obiektu, ponieważ celem wywołania tej metody jest uzyskanie obiektu.


źródło
1
„Co się stanie, jeśli zmienisz pola Otherthing?” (1) Byłoby to naruszeniem zasady otwartej / zamkniętej. (2) nawet jeśli przekażesz cały obiekt, kod w nim, a następnie uzyska dostęp do elementów tego obiektu (a jeśli nie, to dlaczego przekażesz ten obiekt?) Nadal się złamie ...
David Arno
@DavidArno, moja odpowiedź nie polega na tym, że nic się nie złamie, ale mniej się złamie. Nie zapominaj też o pierwszym akapicie: niezależnie od tego, co się psuje, wewnętrzny stan obiektu powinien zostać wyodrębniony za pomocą interfejsu obiektu. Przekazywanie stanu wewnętrznego jest pogwałceniem zasad OOP.
0

Z punktu widzenia łatwości konserwacji argumenty powinny być wyraźnie odróżnialne, najlepiej na poziomie kompilatora.

// this has exactly one way to call it
insertIntoDatabase(Account ..., Otherthing ...)

// the parameter order can be confused in practice
insertIntoDatabase(long ..., long ...)

Pierwszy projekt prowadzi do wczesnego wykrywania błędów. Drugi projekt może prowadzić do subtelnych problemów w czasie wykonywania, które nie pojawiają się w testach. Dlatego należy preferować pierwszy projekt.

Joeri Sebrechts
źródło
0

Spośród nich preferuję pierwszą metodę:

function insertIntoDatabase(Account account, Otherthing thing) { database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue()); }

Powodem jest to, że zmiany dokonane w dowolnym obiekcie w dół drogi, o ile zmiany zachowują te pobierające, więc zmiana jest przezroczysta na zewnątrz obiektu, masz mniej kodu do zmiany i przetestowania oraz mniejsze prawdopodobieństwo zakłócenia działania aplikacji.

To tylko mój proces myślowy, oparty głównie na tym, jak lubię pracować i konstruować rzeczy tego rodzaju i które na dłuższą metę okazują się łatwe do zarządzania i konserwacji.

Nie zamierzam wdawać się w konwencje nazewnictwa, ale zwrócę uwagę, że chociaż w tej metodzie jest słowo „baza danych”, ten mechanizm przechowywania może się zmienić. Z pokazanego kodu nic nie wiąże tej funkcji z używaną platformą do przechowywania baz danych - a nawet jeśli jest to baza danych. Po prostu założyliśmy, ponieważ jest w nazwie. Ponownie, zakładając, że te pobierające są zawsze zachowane, zmiana sposobu / miejsca przechowywania tych obiektów będzie łatwa.

Chciałbym jednak ponownie przemyśleć funkcję i dwa obiekty, ponieważ masz funkcję, która jest zależna od dwóch struktur obiektów, a konkretnie zastosowanych modułów pobierających. Wygląda również na to, że ta funkcja wiąże te dwa obiekty w jedną kumulatywną rzecz, która się utrwala. Moje wnętrzności mówią mi, że trzeci obiekt może mieć sens. Musiałbym dowiedzieć się więcej o tych obiektach i ich związku w rzeczywistości i przewidywanej mapie drogowej. Ale moje jelita pochylają się w tym kierunku.

W obecnym stanie kodu pojawia się pytanie „Gdzie powinna lub powinna być ta funkcja?” Czy jest to część konta, czy innego? Dokąd to zmierza?

Wydaje mi się, że istnieje już „baza danych” trzeciego obiektu, a ja skłaniam się ku umieszczeniu tej funkcji w tym obiekcie, a potem staje się zadaniem tych obiektów, aby móc obsługiwać konto i inny element, przekształcić, a następnie zachować wynik .

Jeśli posuniesz się tak daleko, aby ten trzeci obiekt był zgodny ze wzorcem mapowania relacyjno-obiektowego (ORM), tym lepiej. Byłoby to bardzo oczywiste dla każdego, kto pracuje z kodem, aby zrozumieć „Ach, tutaj konto i OtherThing zostają zmiażdżone i trwają”.

Ale może również mieć sens wprowadzenie czwartego obiektu, który obsługuje zadanie łączenia i przekształcania konta i innego elementu, ale nie obsługuje mechaniki utrzymywania się. Zrobiłbym to, jeśli przewidujesz znacznie więcej interakcji z tymi dwoma obiektami lub między nimi, ponieważ w tym czasie chciałbym, aby bity trwałości zostały rozłożone na obiekt, który zarządza tylko trwałością.

Zrobiłbym zdjęcia, aby zachować projekt w taki sposób, aby dowolne konto, inny obiekt lub trzeci obiekt ORM można było zmienić bez konieczności zmiany pozostałych trzech. Chyba że istnieje dobry powód, aby tego nie robić, chciałbym, aby Account i OtherThing były niezależne i nie musiały znać się od siebie nawzajem.

Oczywiście, gdybym wiedział, że tak będzie w pełnym kontekście, mógłbym całkowicie zmienić swoje pomysły. Znowu tak właśnie myślę, kiedy widzę takie rzeczy i jak szczupły.

Thomas Carlisle
źródło
0

Oba podejścia mają swoje zalety i wady. To, co jest lepsze w scenariuszu, zależy w dużej mierze od danego przypadku użycia.


Pro Wiele parametrów, Con Referencje obiektu:

  • Dzwoniący nie jest związany z określoną klasą , może przekazywać wartości z różnych źródeł
  • Stan obiektu jest bezpieczny przed niespodziewaną modyfikacją w trakcie wykonywania metody.

Referencje Pro Object:

  • Wyczyść interfejs, że metoda jest powiązana z typem referencyjnym Object, co utrudnia przypadkowe przekazanie niepowiązanych / niepoprawnych wartości
  • Zmiana nazwy pola / gettera wymaga zmian przy wszystkich wywołaniach metody, a nie tylko w jej implementacji
  • Jeśli nowa właściwość zostanie dodana i musi zostać przekazana, nie są wymagane żadne zmiany w podpisie metody
  • Metoda może mutować stan obiektu
  • Przekazywanie zbyt wielu zmiennych podobnych prymitywnych typów powoduje, że wywołujące dezorientację wywołuje zamieszanie (problem ze wzorem konstruktora)

Czego więc należy użyć i kiedy wiele zależy od przypadków użycia

  1. Przekaż poszczególne parametry: Ogólnie rzecz biorąc, jeśli metoda nie ma nic wspólnego z typem obiektu, lepiej przekazać listę poszczególnych parametrów , aby można ją było zastosować do szerszego grona odbiorców.
  2. Wprowadź nowy obiekt modelu: jeśli lista parametrów staje się duża (więcej niż 3), lepiej wprowadzić nowy obiekt modelu należący do nazwanego API (preferowany wzorzec konstruktora)
  3. Przekaż odniesienie do obiektu: jeśli metoda jest powiązana z obiektami domeny, to lepiej jest z punktu widzenia łatwości konserwacji i czytelności przekazać odniesienia do obiektu.
Rahul
źródło
0

Z jednej strony masz konto i obiekt Otherthing. Z drugiej strony masz możliwość wstawienia wartości do bazy danych, biorąc pod uwagę identyfikator konta i identyfikator Otherthing. To dwie podane rzeczy.

Możesz napisać metodę, biorąc pod uwagę Account i Otherthing jako argumenty. Po stronie pro, osoba dzwoniąca nie musi znać żadnych szczegółów na temat konta i innych elementów. Z drugiej strony, odbiorca musi wiedzieć o metodach Konta i Innych. Ponadto nie ma sposobu, aby wstawić cokolwiek innego do bazy danych niż wartość obiektu Otherthing i nie ma sposobu na użycie tej metody, jeśli masz identyfikator obiektu konta, ale nie sam obiekt.

Lub możesz napisać metodę przyjmującą dwa argumenty i wartość jako argumenty. Z drugiej strony, osoba dzwoniąca musi znać szczegóły konta i inne. I może zdarzyć się sytuacja, w której potrzebujesz więcej szczegółów konta lub innego niż tylko identyfikatora, aby wstawić do bazy danych, w którym to przypadku to rozwiązanie jest całkowicie bezużyteczne. Z drugiej strony, miejmy nadzieję, że nie jest wymagana znajomość konta i innych rzeczy w callee, i jest większa elastyczność.

Twoja opinia: czy potrzebna jest większa elastyczność? Często nie jest to kwestia jednego połączenia, ale byłoby spójne w całym oprogramowaniu: albo używasz identyfikatorów konta, albo używasz obiektów. Mieszanie go daje ci najgorsze z obu światów.

W C ++ możesz mieć metodę przyjmującą dwa identyfikatory id plus wartość oraz metodę wbudowaną uwzględniającą Account i Otherthing, więc masz obie drogi bez narzutu.

gnasher729
źródło