Czy powinniśmy unikać niestandardowych obiektów jako parametrów?


49

Załóżmy, że mam niestandardowy obiekt, Student :

public class Student{
    public int _id;
    public String name;
    public int age;
    public float score;
}

Oraz klasa Window , która służy do wyświetlania informacji o Uczniu :

public class Window{
    public void showInfo(Student student);
}

Wygląda całkiem normalnie, ale stwierdziłem, że Window nie jest dość łatwy do przetestowania indywidualnie, ponieważ do wywołania funkcji potrzebny jest prawdziwy obiekt Studenta . Próbuję więc zmodyfikować showInfo, aby nie akceptował bezpośrednio obiektu Studenta :

public void showInfo(int _id, String name, int age, float score);

aby łatwiej było testować Window indywidualnie:

showInfo(123, "abc", 45, 6.7);

Ale odkryłem, że zmodyfikowana wersja ma inne problemy:

  1. Zmodyfikuj ucznia (np .: dodaj nowe właściwości) wymaga zmodyfikowania podpisu metody showInfo

  2. Gdyby Student miał wiele właściwości, sygnatura metody Studenta byłaby bardzo długa.

Tak więc, używając niestandardowych obiektów jako parametru lub przyjmując każdą właściwość w obiektach jako parametr, która z nich jest łatwiejsza do utrzymania?


40
A twoje „ulepszone” showInfowymaga prawdziwego Stringa, prawdziwego float i dwóch prawdziwych int. W jaki sposób dostarczanie prawdziwego Stringobiektu jest lepsze niż dostarczanie prawdziwego Studentobiektu?
Bart van Ingen Schenau

28
Jeden poważny problem z bezpośrednim przekazywaniem parametrów: masz teraz dwa intparametry. W witrynie połączeń nie ma weryfikacji, czy faktycznie przekazujesz je w odpowiedniej kolejności. Co jeśli zamienisz idi age, lub firstNamei lastName? Wprowadzasz potencjalny punkt awarii, który może być bardzo trudny do wykrycia, dopóki nie pojawi się na twojej twarzy, i dodajesz go na każdej stronie połączeń .
Chris Hayes,

38
@ChrisHayes ah, stara showForm(bool, bool, bool, bool, int)metoda - Kocham tych ...
Boris the Spider

3
@ChrisHayes przynajmniej nie jest JS ...
Jens Schauder

2
niedoceniana właściwość testów: jeśli trudno jest tworzyć / używać własnych obiektów w testach, Twój interfejs API może zużyć trochę pracy :)
Eevee

Odpowiedzi:


131

Używanie obiektu niestandardowego do grupowania powiązanych parametrów jest w rzeczywistości zalecanym wzorcem. W ramach refaktoryzacji nazywa się to Obiektem parametrów .

Twój problem leży gdzie indziej. Po pierwsze, ogólne nie Windowpowinny wiedzieć nic o uczniu. Zamiast tego powinieneś mieć coś, StudentWindowco wie tylko o wyświetlaniu Students. Po drugie, absolutnie nie ma problemu z tworzeniem Studentinstancji do testowania, StudentWindowo ile Studentnie zawiera ona złożonej logiki, która drastycznie skomplikowałaby testowanie StudentWindow. Jeśli ma taką logikę Student, należy preferować tworzenie interfejsu i kpiny.


14
Warto ostrzec, że możesz wpaść w kłopoty, jeśli nowy obiekt nie jest tak naprawdę logicznym zgrupowaniem. Nie próbuj wyrzucać każdego zestawu parametrów do jednego obiektu; decydować indywidualnie dla każdego przypadku. Przykład w pytaniu wydaje się dość dobrym kandydatem. StudentUgrupowanie ma sens i jest prawdopodobne, aby pojawić się w innych obszarach aplikacji.
jpmc26

Pedantycznie rzecz biorąc, jeśli już masz obiekt, np Student. Byłby to Zachowaj cały obiekt
abuzittin gillifirca

4
Pamiętaj także o prawie Demetera . Jest równowaga do znalezienia, ale tldr nie robi, a.b.cjeśli twoja metoda bierze a. Jeśli twoja metoda dojdzie do punktu, w którym musisz mieć mniej więcej 4 parametry lub 2 poziomy głębokości przystąpienia do nieruchomości, prawdopodobnie należy ją rozważyć. Należy również pamiętać, że jest to wskazówka - podobnie jak wszystkie inne wytyczne, wymaga uznania użytkownika. Nie podążaj za nim na ślepo.
Dan Pantry

7
Pierwsze zdanie tej odpowiedzi było dla mnie niezwykle trudne do przeanalizowania.
helrich

5
@ Qwerky Bardzo się nie zgadzam. Uczeń NAPRAWDĘ brzmi jak liść na wykresie obiektowym (z wyjątkiem innych trywialnych obiektów, takich jak Imię, Data i Urodziny itp.), Po prostu pojemnik na stan ucznia. Nie ma żadnego powodu, dla którego Uczeń powinien być trudny do zbudowania, ponieważ powinien to być typ rekordu. Tworzenie podwójnych testów dla ucznia brzmi jak przepis na trudne do utrzymania testy i / lub silną zależność od fantazyjnych ram izolacji.
sara

26

Mówisz, że to

testowanie pojedynczo nie jest łatwe, ponieważ do wywołania funkcji potrzebny jest prawdziwy obiekt Studenta

Ale możesz po prostu utworzyć obiekt studencki, aby przejść do okna:

showInfo(new Student(123,"abc",45,6.7));

Dzwonienie nie wydaje się bardziej skomplikowane.


7
Problem pojawia się, gdy Studentodnosi się do a University, który odnosi się do wielu Facultys i Campuss, z Professorsi i Buildings, z których żaden showInfotak naprawdę nie korzysta, ale nie zdefiniowałeś żadnego interfejsu, który pozwalałby testom na „poznanie” tego i dostarczenie tylko odpowiedniego ucznia dane, bez budowania całej organizacji. Przykładem Studentjest zwykły obiekt danych i, jak mówisz, testy powinny być z nim zadowolone.
Steve Jessop

4
Problem pojawia się, gdy student odnosi się do uniwersytetu, który odnosi się do wielu wydziałów i kampusów, z profesorami i budynkami, nie ma odpoczynku dla niegodziwych.
abuzittin gillifirca

1
@abuzittingillifirca, „Object Mother” to jedno rozwiązanie, również przedmiot studencki może być zbyt złożony. Lepiej jest mieć UniversityId i usługę (wykorzystującą wstrzykiwanie zależności), która da obiekt University od UniversityId.
Ian

12
Jeśli Student jest bardzo skomplikowany lub trudno go zainicjować, po prostu zrób to. Testowanie jest znacznie wydajniejsze dzięki frameworkom takim jak Mockito lub odpowiedniki w innych językach.
Borjab

4
Jeśli showInfo nie dba o uniwersytet, to po prostu ustaw go na null. Wartości zerowe są okropne w produkcji i testy wysyłane przez Boga. Możliwość określenia parametru jako null w teście komunikuje zamiar i mówi, że „to coś nie jest tutaj potrzebne”. Chociaż zastanowiłbym się również nad stworzeniem jakiegoś modelu widoku dla uczniów zawierającego tylko odpowiednie dane, biorąc pod uwagę, że showInfo brzmi jak metoda w klasie interfejsu użytkownika.
sara

22

W kategoriach laika:

  • To, co nazywacie „obiektem niestandardowym”, jest zwykle po prostu nazywane obiektem.
  • Nie można uniknąć przekazywania obiektów jako parametrów podczas projektowania dowolnego nietrywialnego programu lub interfejsu API lub korzystania z dowolnego nietrywialnego interfejsu API lub biblioteki.
  • Przekazywanie obiektów jako parametrów jest całkowicie OK. Spójrz na API Java, a zobaczysz wiele interfejsów, które odbierają obiekty jako parametry.
  • Klasy w bibliotekach, których używasz, pisane były przez zwykłych śmiertelników, takich jak ty i ja, więc te, które piszemy, nie są „niestandardowe” , po prostu są.

Edytować:

Jak stwierdza @ Tom.Bowen89 , testowanie metody showInfo nie jest dużo bardziej skomplikowane:

showInfo(new Student(8812372,"Peter Parker",16,8.9));

3
  1. W twoim przykładzie studenckim zakładam, że to proste, aby wywołać konstruktora studenta, aby utworzyć ucznia, który przejdzie do showInfo. Więc nie ma problemu.
  2. Zakładając przykład Uczeń jest celowo trywializowany dla tego pytania i trudniej go skonstruować, wtedy możesz użyć podwójnego testu . Istnieje wiele opcji podwójnych testów, próbnych prób, skrótów itp., O których mowa w artykule Martina Fowlera do wyboru.
  3. Jeśli chcesz, aby funkcja showInfo była bardziej ogólna, możesz iterować ją po zmiennych publicznych, a może publiczni akcesory obiektu przekazali i wykonali logikę show dla wszystkich z nich. Następnie możesz przekazać dowolny obiekt zgodny z tą umową i będzie działać zgodnie z oczekiwaniami. To byłoby dobre miejsce do korzystania z interfejsu. Na przykład przekaż obiekt Showalny lub ShowInfoable do funkcji showInfo, która może wyświetlać nie tylko informacje o studentach, ale także informacje o dowolnym obiekcie implementującym interfejs (oczywiście interfejsy te wymagają lepszych nazw w zależności od tego, jak konkretny lub ogólny chcesz obiekt, do którego możesz przekazać być i czym Student jest podklasą).
  4. Często łatwiej jest ominąć prymityw, a czasem jest to konieczne dla wydajności, ale im więcej można grupować podobne pojęcia, tym bardziej zrozumiały będzie twój kod. Jedyną rzeczą, na którą należy uważać, jest próba rezygnacji z tego i ukończenia korporacyjnego fizzbuzz .

3

Steve McConnell w Code Complete zajął się tym problemem, omawiając zalety i wady przekazywania obiektów do metod zamiast korzystania z właściwości.

Wybacz mi, jeśli pomylę niektóre szczegóły, pracuję z pamięci, ponieważ minął ponad rok, odkąd miałem dostęp do książki:

Dochodzi do wniosku, że lepiej nie używać obiektu, zamiast tego wysyłać tylko te właściwości, które są absolutnie niezbędne dla metody. Metoda nie powinna wiedzieć nic o obiekcie poza właściwościami, których będzie używał w ramach swoich operacji. Z czasem, jeśli obiekt zostanie kiedykolwiek zmieniony, może to mieć niezamierzone konsekwencje dla metody używającej obiektu.

Zwrócił również uwagę, że jeśli skończysz na metodzie, która akceptuje wiele różnych argumentów, to prawdopodobnie jest to znak, że metoda robi zbyt wiele i powinna zostać podzielona na więcej, mniejszych metod.

Czasami jednak czasami potrzebujesz wielu parametrów. Podany przez niego przykład dotyczy metody, która buduje pełny adres przy użyciu wielu różnych właściwości adresu (choć można to obejść, używając tablicy ciągów, gdy się nad tym zastanowić).


7
Mam kod ukończony 2. Jest cała strona poświęcona temu problemowi. Wniosek jest taki, że parametry powinny znajdować się na właściwym poziomie abstrakcji. Czasami wymaga to przekazania całego obiektu, a czasem tylko pojedynczych atrybutów.
PRZYJDŹ OD

UV. Kod referencyjny zakończony jest podwójny plus dobry. Niezłe rozważenie projektu w porównaniu z celowością testowania. Wolę projektowanie, myślę, że McConnell powiedziałby w naszym kontekście. Zatem doskonałym wnioskiem byłoby „zintegrowanie obiektu parametru w projekcie” ( Studentw tym przypadku). I w ten sposób testowanie wpływa na projekt , całkowicie obejmując większość głosów przy zachowaniu integralności projektu.
radarbob

2

Znacznie łatwiej jest pisać i czytać testy, jeśli zdasz cały obiekt:

public class AStudentView {
    @Test 
    public void displays_failing_grade_warning_when_a_student_with_a_failing_grade_is_shown() {
        StudentView view = aStudentView();
        view.show(aStudent().withAFailingGrade().build());
        Assert.that(view, displaysFailingGradeWarning());
    }

    private Matcher<StudentView> displaysFailingGradeWarning() {
        ...
    }
}

Dla porownania,

view.show(aStudent().withAFailingGrade().build());

wiersz można zapisać, jeśli wartości są przekazywane osobno, jako:

showAStudentWithAFailingGrade(view);

gdzie rzeczywiste wywołanie metody jest gdzieś zakopane

private showAStudentWithAFailingGrade(StudentView view) {
    int someId = .....
    String someName = .....
    int someAge = .....
    // why have been I peeking and poking values I don't care about
    decimal aFailingGrade = .....
    view.show(someId, someName, someAge, aFailingGrade);
}

Mówiąc o tym, że nie można umieścić rzeczywistego wywołania metody w teście, jest to znak, że interfejs API jest zły.


1

Powinieneś przekazać to, co ma sens, kilka pomysłów:

Łatwiej przetestować. Jeśli trzeba edytować obiekty, co wymaga najmniejszego refaktoryzacji? Czy ponowne użycie tej funkcji do innych celów jest przydatne? Jaka jest najmniejsza ilość informacji potrzebnych do nadania tej funkcji, aby spełniała swoje zadanie? (Po rozbiciu go - może to pozwolić na ponowne użycie tego kodu - należy uważać, aby nie upaść przy projektowaniu tej funkcji, a następnie wąskim gardłem wszystkiego, aby korzystać wyłącznie z tego obiektu).

Wszystkie te zasady programowania są jedynie wskazówkami, które pomogą ci myśleć we właściwym kierunku. Po prostu nie buduj bestii kodowej - jeśli nie masz pewności i chcesz kontynuować, wybierz kierunek / swój własny lub sugestię tutaj, a jeśli osiągniesz punkt, w którym myślisz „och, powinienem to zrobić sposób ”- prawdopodobnie możesz łatwo wrócić i przebudować go z łatwością. (Na przykład, jeśli masz klasę Nauczyciela - potrzebuje tylko tej samej właściwości, co Student, i zmieniasz funkcję, aby zaakceptować dowolny obiekt formularza Osoba)

Byłbym najbardziej skłonny do przekazywania głównego obiektu - ponieważ jak koduję, łatwiej wyjaśnić, co robi ta funkcja.


1

Jedną wspólną drogą wokół tego jest wstawienie interfejsu między dwoma procesami.

public class Student {

    public int id;
    public String name;
    public int age;
    public float score;
}

interface HasInfo {
    public String getInfo();
}

public class StudentInfo implements HasInfo {
    final Student student;

    public StudentInfo(Student student) {
        this.student = student;
    }

    @Override
    public String getInfo() {
        return student.name;
    }

}

public class Window {

    public void showInfo(HasInfo info) {

    }
}

Czasami robi się to trochę nieporządnie, ale w Javie jest trochę łatwiej, jeśli używasz wewnętrznej klasy.

interface HasInfo {
    public String getInfo();
}

public class Student {

    public int id;
    public String name;
    public int age;
    public float score;

    public HasInfo getInfo() {
        return new HasInfo () {
            @Override
            public String getInfo() {
                return name;
            }

        };
    }
}

Następnie możesz przetestować Windowklasę, podając jej fałszywy HasInfoobiekt.

Podejrzewam, że jest to przykład wzoru dekoratora .

Dodany

Wygląda na pewne zamieszanie spowodowane prostotą kodu. Oto kolejny przykład, który może lepiej zademonstrować tę technikę.

interface Drawable {

    public void Draw(Pane pane);
}

/**
 * Student knows nothing about Window or Drawable.
 */
public class Student {

    public int id;
    public String name;
    public int age;
    public float score;
}

/**
 * DrawsStudents knows about both Students and Drawable (but not Window)
 */
public class DrawsStudents implements Drawable {

    private final Student subject;

    public DrawsStudents(Student subject) {
        this.subject = subject;
    }

    @Override
    public void Draw(Pane pane) {
        // Draw a Student on a Pane
    }

}

/**
 * Window only knows about Drawables.
 */
public class Window {

    public void showInfo(Drawable info) {

    }
}

Jeśli showInfo chce tylko wyświetlać nazwisko ucznia, dlaczego nie przekazać go? zawijanie semantycznie znaczącego nazwanego pola w abstrakcyjnym interfejsie zawierającym ciąg bez pojęcia, co reprezentuje ciąg, wydaje się OGROMNYM obniżeniem poziomu, zarówno pod względem łatwości konserwacji, jak i zrozumiałości.
sara

@kai - Użycie Studenti Stringtutaj dla typu zwrotu służy wyłącznie do celów demonstracyjnych. Prawdopodobnie byłyby dodatkowe parametry, getInfotakie jak Panerysowanie, jeśli rysowanie. Chodzi tutaj o przekazanie funkcjonalnych komponentów jako dekoratorów oryginalnego obiektu .
OldCurmudgeon

w takim przypadku byłbyś ściśle związany z uczniem z architekturą interfejsu użytkownika, co brzmi jeszcze gorzej ...
sara

1
@kai - Wręcz przeciwnie. Mój interfejs użytkownika zna tylko HasInfoobiekty. Studentumie być jednym.
OldCurmudgeon

Jeśli dasz getInfokomendę return void pass, a Panenarysujesz ją, wówczas implementacja (w Studentklasie) nagle zostanie sprzężona z swingiem lub czymkolwiek, czego używasz. Jeśli sprawisz, że zwróci jakiś ciąg znaków i przyjmie 0 parametrów, twój interfejs użytkownika nie będzie wiedział, co zrobić z łańcuchem bez magicznych założeń i niejawnego łączenia. Jeśli sprawisz, że getInforzeczywiście zwrócisz jakiś model widoku z odpowiednimi właściwościami, wtedy twoja Studentklasa ponownie zostanie sprzężona z logiką prezentacji. Nie sądzę, aby którakolwiek z tych alternatyw była pożądana
sara

1

Masz już wiele dobrych odpowiedzi, ale oto kilka innych sugestii, które mogą pozwolić ci zobaczyć alternatywne rozwiązanie:

  • Twój przykład pokazuje, że Student (wyraźnie obiekt modelowy) jest przekazywany do Okna (najwyraźniej obiekt na poziomie widoku). Pośredni obiekt Controller lub Presenter może być korzystny, jeśli jeszcze go nie masz, umożliwiając odizolowanie interfejsu użytkownika od modelu. Kontroler / prezenter powinien zapewnić interfejs, którego można użyć do zastąpienia go do testowania interfejsu użytkownika, i powinien używać interfejsów do odwoływania się do obiektów modelu i wyświetlania obiektów, aby móc odizolować go od obu do testowania. Może być konieczne zapewnienie abstrakcyjnego sposobu ich tworzenia lub ładowania (np. Obiekty fabryczne, obiekty repozytorium lub podobne).

  • Przeniesienie odpowiednich części obiektów modelu do obiektu przenoszenia danych jest użytecznym podejściem do łączenia, gdy model staje się zbyt złożony.

  • Być może Twój Uczeń narusza Zasadę Segregacji Interfejsu. Jeśli tak, korzystne może być podzielenie go na wiele interfejsów, z którymi łatwiej jest pracować.

  • Leniwe ładowanie może ułatwić tworzenie wykresów dużych obiektów.


0

To właściwie przyzwoite pytanie. Prawdziwym problemem jest tutaj użycie terminu „obiekt”, który może być nieco niejednoznaczny.

Zasadniczo w klasycznym języku OOP termin „obiekt” zaczął oznaczać „instancję klasy”. Instancje klas mogą być dość ciężkie - własności publiczne i prywatne (i te pośrednie), metody, dziedziczenie, zależności itp. Naprawdę nie chciałbyś używać czegoś takiego do przekazywania niektórych właściwości.

W tym przypadku używasz obiektu jako kontenera, który po prostu przechowuje niektóre prymitywy. W C ++ takie obiekty były znane jako structs(i nadal istnieją w językach takich jak C #). W rzeczywistości konstrukcje zostały zaprojektowane dokładnie na użytek, o którym mówisz - zgrupowały powiązane obiekty i prymitywy, gdy miały logiczną relację.

Jednak we współczesnych językach tak naprawdę nie ma różnicy między strukturą a klasą podczas pisania kodu , więc możesz używać obiektu. (Za kulisami istnieją jednak pewne różnice, o których powinieneś wiedzieć - na przykład struct jest typem wartości, a nie typem odniesienia). Zasadniczo, dopóki utrzymasz prosty obiekt, będzie to łatwe przetestować ręcznie. Współczesne języki i narzędzia pozwalają jednak nieco to złagodzić (poprzez interfejsy, kpiny, wstrzykiwanie zależności itp.)


1
Przekazanie referencji nie jest drogie, nawet jeśli obiekt ma wielkość miliarda terabajtów, ponieważ referencja wciąż ma rozmiar int w większości języków. Powinieneś bardziej martwić się, czy metoda odbierania jest narażona na zbyt duży interfejs API i czy łączysz rzeczy w niepożądany sposób. Zastanowiłbym się nad utworzeniem warstwy mapowania, która tłumaczy obiekty biznesowe ( Student) na modele widoków ( StudentInfolub StudentInfoViewModelitp.), Ale może nie być to konieczne.
sara

Klasy i struktury są bardzo różne. Jeden jest przekazywany przez wartość (co oznacza, że ​​metoda otrzymująca otrzymuje kopię ), a drugi jest przekazywany przez referencję (odbiornik otrzymuje wskaźnik do oryginału). Niezrozumienie tej różnicy jest niebezpieczne.
RubberDuck

@ kai Rozumiem, że przekazanie referencji nie jest drogie. Mówię o tym, że utworzenie funkcji wymagającej instancji pełnej klasy może być trudniejsze do przetestowania w zależności od zależności tej klasy, jej metod itp. - ponieważ musiałbyś w jakiś sposób wyśmiewać tę klasę.
lunchmeat317

Ja osobiście nie kpię z wszystkiego, z wyjątkiem klas granicznych, które uzyskują dostęp do systemów zewnętrznych (IO plików / sieci) lub tych, które nie są deterministyczne (np. Losowe, systemowe oparte na czasie itp.). Jeśli testowana klasa ma zależność, która nie jest istotna dla testowanej obecnie funkcji, wolę, jeśli to możliwe, przekazać wartość null. Ale jeśli testujesz metodę, która pobiera obiekt parametru, jeśli ten obiekt ma wiele zależności, martwiłbym się ogólnym projektem. Takie przedmioty powinny być lekkie.
sara
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.