Чи слід уникати спеціальних об'єктів як параметрів?


49

Припустимо, у мене є власний об'єкт, студент :

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

І клас Window , який використовується для показу інформації студента :

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

Це виглядає цілком нормально, але я виявив, що Window не зовсім легко перевірити окремо, тому що для виклику функції потрібен справжній об'єкт Student . Тому я намагаюся змінити showInfo, щоб він не приймав об'єкт Student безпосередньо:

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

щоб простіше було тестувати Window індивідуально:

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

Але я виявив, що в модифікованій версії є й інші проблеми:

  1. Змінення Student (наприклад, додавання нових властивостей) потребує зміни методу-підпису showInfo

  2. Якби Студент мав багато властивостей, метод-підпис Студента був би дуже довгим.

Отже, використовуючи власні об’єкти як параметр або приймати кожну властивість в об'єктах як параметр, який із них можна підтримувати?


40
А ваш «покращений» showInfoвимагає справжньої струни, справжнього поплавця та двох справжніх ints. Як забезпечення реального Stringоб'єкта краще, ніж надання реального Studentоб'єкта?
Барт ван Інген Шенау

28
Одна з головних проблем з передачею параметрів безпосередньо: у вас зараз два intпараметри. На сайті виклику немає підтвердження того, що ви насправді передаєте їх у правильному порядку. Що робити , якщо потрібно поміняти місцями idі age, або firstNameі lastName? Ви представляєте потенційну точку відмови, яку можна дуже важко виявити, поки вона не підірветься вам в обличчя, і ви додаєте її на кожен сайт дзвінка .
Кріс Хейс

38
@ChrisHayes ах, старий showForm(bool, bool, bool, bool, int)метод - я люблю тих ...
Борис Павук

3
@ChrisHayes, принаймні, це не JS ...
Єнс Шадер

2
недооцінене властивість тестів: якщо важко створити / використовувати власні об’єкти в тестах, ваш API може скористатися певною роботою :)
Eevee

Відповіді:


131

Використання спеціального об'єкта для групування пов'язаних параметрів - це фактично рекомендований зразок. Як рефакторинг, він називається ввести об'єкт параметра .

Ваша проблема полягає в іншому місці. По-перше, загальне не Windowповинно нічого знати про Студента. Натомість у вас має бути якесь StudentWindowте, що відомо лише про показ Students. По-друге, абсолютно немає проблем із створенням Studentекземпляра для тестування до StudentWindowтих пір, поки Studentвін не містить жодної складної логіки, яка би суттєво ускладнила тестування StudentWindow. Якщо у нього є така логіка, то Studentслід віддати перевагу створення інтерфейсу та глузування з нього.


14
Варто попередити, що ви можете потрапити в проблеми, якщо новий об’єкт насправді не є логічним угрупуванням. Не намагайтеся підключити кожен параметр, встановлений в один об'єкт; приймати рішення залежно від конкретного випадку. Приклад у питанні, здається, досить чітко є хорошим кандидатом на це. StudentУгруповання має сенс , і, швидше за все, виникають в інших областях програми.
jpmc26

Педантично кажучи, якщо у вас вже є об'єкт, наприклад Student, це був би об'єкт " Зберегти весь
предмет"

4
Також пам’ятайте Закон Деметера . Ви повинні досягти балансу, але tldr - це не робити, a.b.cякщо ваш метод займає a. Якщо ваш метод доходить до того, що вам потрібно мати приблизно більше 4 параметрів або 2 рівня глибокого приєднання власності, його, ймовірно, потрібно врахувати. Також зауважте, що це правило - як і всі інші вказівки, воно вимагає розсуду користувача. Не слідкуйте за цим сліпо.
Комора Ден

7
Перше речення цієї відповіді мені здалося надзвичайно важким для розбору.
Гельріх

5
@Qwerky Я б дуже не погоджувався. Студент дійсно звучить як лист у графіку об'єкта (забороняючи інші тривіальні об’єкти, такі як, можливо, ім'я, датаOfBirth тощо), просто контейнер для стану студента. Немає жодної причини, яку студент повинен бути важко побудувати, тому що це має бути тип запису. Створення тестових пар для студента звучить як рецепт важких для підтримання тестів та / або сильної залежності від якоїсь фантазійної структури ізоляції.
сара

26

Ви кажете, що це

не зовсім легко перевірити індивідуально, тому що для виклику функції потрібен справжній об'єкт Student

Але ви можете просто створити студентський об’єкт для переходу до вашого вікна:

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

Це не набагато складніше дзвонити.


7
Проблема виникає, коли Studentйдеться про a University, який посилається на багато Facultys і Campuss, з Professors і Buildings, жоден з яких showInfoнасправді не використовує, але ви не визначили жодного інтерфейсу, який дозволяє тестам "знати" це і постачати тільки відповідного студента даних, не будуючи всієї організації. Приклад Student- це звичайний об’єкт даних, і, як ви кажете, тести повинні із задоволенням працювати з ним.
Стів Джессоп

4
Проблема виникає, коли Студент звертається до університету, в якому йдеться про багато Факультетів та Кампусів, з професорами та будівлями, без спокою для злих.
abuzittin gillifirca

1
@abuzittingillifirca, "Об'єктна мати" - це одне рішення, також ваш об'єкт студента може бути занадто складним. Можливо, краще просто мати UniversityId та послугу (використовуючи ін'єкцію залежностей), яка дасть об’єкт університету від UniversityId.
Ян

12
Якщо студент дуже складний або його важко ініціалізувати, просто знущайтеся над ним. Тестування набагато потужніше з такими рамками, як Mockito або іншими еквівалентами мови.
Борджаб

4
Якщо showInfo не хвилює університет, просто встановіть його на нуль. Нулі жахливі у виробництві і на бога відправляють тести. Можливість вказати параметр як нуль у тестах повідомляє про наміри і каже, що "ця річ тут не потрібна". Хоча я також розглядаю можливість створити якусь модель перегляду для студентів, що містить лише відповідні дані, враховуючи, що showInfo звучить як метод у класі інтерфейсу користувача.
сара

22

Простіше кажучи:

  • Те, що ви називаєте "користувацьким об'єктом" , зазвичай називається просто об'єктом.
  • Ви не можете уникнути передачі об'єктів як параметрів під час проектування будь-якої нетривіальної програми чи API або використання будь-якого нетривіального API чи бібліотеки.
  • Цілком нормально передавати об'єкти як параметри. Погляньте на API Java, і ви побачите безліч інтерфейсів, які отримують об'єкти як параметри.
  • Класи в бібліотеках, якими ви користуєтесь, там, де їх пишуть просто смертні, як ви і я, тому ті, про які ми пишемо, не є "на замовлення" , вони просто є.

Редагувати:

Як @ Tom.Bowen89 зазначає, що перевірити метод showInfo не набагато складніше:

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

3
  1. У вашому прикладі студента я припускаю, що тривіально викликати конструктор Student, щоб створити студента, щоб перейти до showInfo. Так що проблем немає.
  2. Якщо припустити приклад, що студент навмисно тривіалізується для цього питання, і це складніше побудувати, то ви можете використовувати тестовий подвійний . Існує ряд варіантів тестових пар, макетів, заглушок тощо, про які йдеться у статті Мартіна Фаулера.
  3. Якщо ви хочете зробити функцію showInfo більш загальною, ви можете зробити її переглядом загальнодоступних змінних, або, можливо, загальнодоступні об'єкти передали об'єкт і виконали логіку показу для всіх. Тоді ви можете передати будь-який об’єкт, який відповідав цьому договору, і він працював би як очікувалося. Це було б хорошим місцем для використання інтерфейсу. Наприклад, передайте об’єкт Shovable або ShowInfoable до функції showInfo, яка може показувати не лише інформацію про студентів, але інформацію про будь-який об'єкт, що реалізує інтерфейс (очевидно, що для цих інтерфейсів потрібні кращі назви залежно від того, наскільки конкретний або загальний ви хочете об'єкт, на який ви можете передати. бути і що студент є підкласом).
  4. Часто простіше обходити примітиви, а іноді це потрібно для продуктивності, але чим більше ви можете згрупувати подібні поняття разом, тим зрозумілішим буде ваш код. Єдине, на що слід стежити - це спробувати не перестаратися і закінчити бізнес-фізбузз .

3

Стів МакКоннелл в Code Complete вирішив цю проблему, обговоривши переваги та недоліки передачі об'єктів у методи замість використання властивостей.

Пробачте, якщо я неправильно зрозумів деякі деталі, я працюю з пам'яті, так як минуло вже рік, як я отримав доступ до книги:

Він приходить до висновку, що вам краще не використовувати об’єкт, а надіслати лише ті властивості, які абсолютно необхідні методу. Метод не повинен нічого знати про об'єкт поза властивостями, які він буде використовувати в рамках своїх операцій. Крім того, з часом, якщо об'єкт коли-небудь буде змінений, це може мати непередбачувані наслідки для методу, що використовує об'єкт.

Також він звернувся до того, що якщо ви закінчитесь із методом, який приймає безліч різних аргументів, то це, мабуть, ознака того, що метод робить занадто багато і його слід розбити на більше, менші методи.

Однак іноді, іноді, насправді потрібно багато параметрів. Приклад, який він наводить, буде методом, який створює повну адресу, використовуючи безліч різних властивостей адреси (хоча це можна отримати, використовуючи рядковий масив, коли ви думаєте про це).


7
У мене Код повний 2. У ньому ціла сторінка, присвячена цій проблемі. Висновок полягає в тому, що параметри повинні бути на правильному рівні абстракції. Іноді для цього потрібно передавати цілий об'єкт, іноді лише окремі атрибути.
НАДАЄТЬСЯ

УФО. Повний код посилань - це корисний подвійний плюс. Приємний розгляд дизайну над доцільністю тестування. Віддаю перевагу дизайну, думаю, МакКоннелл сказав би в нашому контексті. Таким чином, відмінним висновком буде "інтеграція об'єкта параметра в дизайн" ( Studentу цьому випадку). Ось як тестування інформує дизайн , повністю приймаючи відповідь на найбільш голоси, зберігаючи цілісність дизайну.
radarbob

2

Набагато простіше писати та читати тести, якщо ви здаєте весь об’єкт:

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() {
        ...
    }
}

Для порівняння

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

рядок може бути записаний, якщо ви передаєте значення окремо, як:

showAStudentWithAFailingGrade(view);

де фактичний виклик методу десь схований

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);
}

Якщо говорити про те, що ви не можете поставити фактичний виклик методу в тесті, це знак того, що для вас API поганий.


1

Вам слід передати певні ідеї:

Простіше тестувати. Якщо об'єкт (и) потрібно редагувати, що потребує найменшого рефакторингу? Чи корисне повторне використання цієї функції для інших цілей? Який найменший об'єм інформації, який мені потрібно надати цій функції, щоб досягти її мети? (Розбивши його - це може дозволити вам повторно використовувати цей код - будьте обережні при падінні в проектному отворі, щоб зробити цю функцію, а потім у вузьких місцях все, щоб виключно використовувати цей об'єкт.)

Усі ці правила програмування - це лише настанови, щоб змусити вас думати в правильному напрямку. Просто не будуйте звіра коду - якщо ви не впевнені і просто потрібно продовжувати, виберіть тут напрямок / свій власний або пропозицію, і якщо ви потрапили в точку, де ви думаєте: «о, я повинен був би зробити це шлях '- ви, мабуть, потім можете повернутися назад і рефакторировать його досить легко. (Наприклад, якщо у вас є клас Вчитель - йому просто потрібен той самий властивість, що і Студент, і ви змінюєте свою функцію, щоб прийняти будь-який об'єкт форми Особи)

Я був би схильний тримати головний об'єкт, що передається - тому що, як я кодую, це легше пояснити, що ця функція виконує.


1

Один загальний шлях до цього - вставити інтерфейс між двома процесами.

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) {

    }
}

Іноді це стає трохи безладним, але в Java стає все дещо охайніше, якщо використовується внутрішній клас.

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;
            }

        };
    }
}

Потім ви можете перевірити Windowклас, просто надавши йому підроблений HasInfoоб’єкт.

Я підозрюю, що це приклад візерунка декораторів .

Додано

Здається, є деяка плутанина, викликана простотою коду. Ось ще один приклад, який може демонструвати техніку краще.

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) {

    }
}

Якщо showInfo хотів відобразити лише ім'я студента, чому б не просто передати ім’я? загортання семантично значущого іменованого поля в абстрактний інтерфейс, що містить рядок, що не має поняття про те, що ця рядок представляє собою ВЕЛИЧЕЗНУ пониження, як з точки зору ремонтопридатності, так і зрозумілості.
сара

@kai - Використання Studentі Stringтут для типу повернення - виключно для демонстрації. Можливо, будуть додаткові параметри, getInfoнаприклад, для того, Paneщоб малювати, якщо малювати. Концепція тут полягає у передачі функціональних компонентів як декораторів оригінального об’єкта .
OldCurmudgeon

у такому випадку ви будете щільно зв’язувати сутність студента з рамкою вашого інтерфейсу, це звучить ще гірше ...
Сара

1
@kai - Зовсім навпаки. Мій інтерфейс знає лише про HasInfoоб’єкти. Studentзнає, як бути одним.
OldCurmudgeon

Якщо ви даєте getInforeturn void, передайте його, Paneщоб намалювати, тоді реалізація (у Studentкласі) раптово поєднується з гойдалкою або будь-чим, що ви використовуєте. Якщо ви змусите його повернути деяку рядок і приймати 0 параметрів, то ваш інтерфейс не буде знати, що робити з рядком без магічних припущень і неявного зв'язку. Якщо ви getInfoдійсно повернете деяку модель перегляду з відповідними властивостями, то ваш Studentклас знову поєднується з логікою презентації. Я не думаю, що будь-яка з цих альтернатив є бажаною
Сара

1

У вас вже багато хороших відповідей, але ось ще кілька пропозицій, які можуть допомогти вам побачити альтернативне рішення:

  • У вашому прикладі показано, що студент (чітко модельний об'єкт) передається у вікно (мабуть, об'єкт рівня перегляду). Посередницький контролер або об’єкт Presenter може бути корисним, якщо у вас його ще немає, що дозволяє ізолювати ваш користувальницький інтерфейс від вашої моделі. Контролер / презентатор повинен надати інтерфейс, який можна використовувати для заміни його для тестування на інтерфейс користувача, а також повинен використовувати інтерфейси для позначення модельних об'єктів та перегляду об'єктів, щоб мати можливість ізолювати його від обох для тестування. Можливо, вам потрібно буде надати якийсь абстрактний спосіб їх створення або завантаження (наприклад, заводські об'єкти, об'єкти сховища тощо).

  • Перенесення відповідних частин об'єктів вашої моделі в об’єкт передачі даних є корисним підходом для взаємодії, коли модель стає занадто складною.

  • Можливо, ваш студент порушує принцип розбиття інтерфейсу. Якщо це так, може бути корисно розділити його на кілька інтерфейсів, з якими простіше працювати.

  • Ледаче завантаження може спростити побудову великих об’єктних графіків.


0

Це насправді гідне питання. Справжня проблема тут - використання загального терміна «об’єкт», який може бути дещо неоднозначним.

Як правило, у класичній мові ООП термін "об'єкт" позначає "екземпляр класу". Екземпляри класу можуть бути досить важкими - публічні та приватні властивості (і ті, що знаходяться між ними), методи, успадкування, залежності тощо. Ви не дуже хотіли б використовувати щось подібне, щоб просто передати деякі властивості.

У цьому випадку ви використовуєте об'єкт як контейнер, який просто містить деякі примітиви. У C ++ такі об'єкти були відомі як structs(і вони все ще існують на таких мовах як C #). Структури, власне, були розроблені саме для використання, про яке ви говорите, - вони групували споріднені об'єкти та примітиви, коли вони мали логічні стосунки.

Однак у сучасних мовах насправді немає різниці між структурою та класом, коли ви пишете код , тож ви добре використовуєте об’єкт. (За лаштунками, однак, є деякі відмінності, про які слід пам’ятати - наприклад, структура - це тип значення, а не тип посилання.) В основному, якщо ви будете просто використовувати ваш об’єкт, це буде легко перевірити вручну. Сучасні мови та інструменти дозволяють вам трохи пом'якшити це (через інтерфейси, глузуючі рамки, введення залежності тощо)


1
Подання посилання не є дорогим, навіть якщо об'єкт має мільярд терабайт, оскільки посилання все ще має лише розмір цілої величини на більшості мов. Вам слід більше турбуватися про те, чи піддається метод прийому занадто великої плодової тканини, і якщо ви поєднуєте речі небажаним способом. Я б розглядав можливість створення шару відображення, який переводить бізнес-об'єкти ( Student) у моделі перегляду ( StudentInfoабо StudentInfoViewModelтощо), але це може не бути необхідним.
сара

Класи та структури дуже різні. Один передається за значенням (тобто спосіб, який отримує, отримує копію ), а інший передається за посиланням (приймач просто отримує вказівник на оригінал). Не розуміти цієї різниці небезпечно.
RubberDuck

@kai Я розумію, що здати довідку не дорого. Що я говорю, що створення функції, яка вимагає повного екземпляра класу, може бути складніше перевірити залежно від залежностей цього класу, його методів тощо - як вам доведеться якось знущатися над цим класом.
обідне м’ясо317

Я особисто проти насмішок майже над чим-небудь, окрім граничних класів, які мають доступ до зовнішніх систем (файлів / мережевих вводу-виводу) або тих, що не є детермінованими (наприклад, випадкові, системні на основі часу тощо). Якщо клас, який я тестую, має залежність, яка не стосується поточної тестованої функції, я вважаю за краще просто пропустити null, де це можливо. Але якщо ви тестуєте метод, який бере об’єкт параметра, якщо цей об’єкт має купу залежностей, я б переймався загальним дизайном. Такі предмети повинні бути легкими.
сара
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.