Должны ли мы избегать пользовательских объектов в качестве параметров?

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. Изменение Студента (например, добавление новых свойств) требует изменения метода-подписи showInfo

  2. Если бы у Student было много свойств, метод-подпись Student был бы очень длинным.

Итак, если использовать пользовательские объекты в качестве параметра или принять каждое свойство в объектах в качестве параметра, какое из них более приемлемо?

ggrr
источник
40
А для твоего «улучшения» showInfoнужна настоящая строка, реальный float и два настоящих целых. Как обеспечить реальный 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такая логика, то создание интерфейса и насмешка должны быть предпочтительными.

Euphoric
источник
14
Стоит предупредить, что вы можете попасть в неприятности, если новый объект на самом деле не является логической группировкой. Не пытайтесь вводить каждый набор параметров в один объект; принять решение в каждом конкретном случае. Пример в вопросе, кажется, довольно ясно, является хорошим кандидатом на это. StudentГруппировка имеет смысл , и, скорее всего, возникают в других областях приложения.
jpmc26
Говоря педантично, если у вас уже есть объект, например Student, это будет весь объект Preserve
abuzittin gillifirca
4
Также помните Закон Деметры . Есть баланс, который нужно достичь, но tldr не делай, a.b.cесли твой метод берет a. Если ваш метод доходит до того, что вам нужно иметь примерно более 4 параметров или 2 уровня глубины присоединения свойства, его, вероятно, необходимо учесть. Также обратите внимание, что это руководство - как и все другие рекомендации, оно требует усмотрения пользователя. Не следуй этому вслепую.
Дэн Пантри
7
Я нашел первое предложение этого ответа чрезвычайно трудно разобрать.
Хелрих
5
@Qwerky Я бы очень сильно не согласился. Студент ДЕЙСТВИТЕЛЬНО звучит как лист на графе объектов (за исключением других тривиальных объектов, таких как, например, Name, DateOfBirth и т. Д.), Просто контейнер для состояния студента. Нет никаких оснований полагать, что ученика трудно создать, потому что это должен быть тип записи. Создание двойников теста для ученика звучит как рецепт для трудного поддержания тестов и / или сильной зависимости от какой-то необычной структуры изоляции.
Сара
26

Вы говорите, что это

не так-то просто проверить индивидуально, потому что для вызова функции нужен настоящий объект Student

Но вы можете просто создать объект ученика для передачи в ваше окно:

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

Это не кажется более сложным, чтобы позвонить.

Tom.Bowen89
источник
7
Проблема возникает, когда Studentссылается на a University, который относится ко многим Facultys и Campuss, с Professors и Buildings, ни один из которых на showInfoсамом деле не используется, но вы не определили какой-либо интерфейс, который позволяет тестам «знать» это и предоставлять только соответствующему ученику данные, без построения всей организации. Пример Studentпредставляет собой простой объект данных, и, как вы говорите, тесты должны быть рады работать с ним.
Стив Джессоп
4
Проблема возникает, когда Студент обращается к университету, который относится ко многим факультетам и кампусам, с профессорами и зданиями, а не для нечестивых.
Abuzittin Gillifirca
1
@abuzittingillifirca, «Мать объекта» - это одно из решений, также объект вашего ученика может быть слишком сложным. Может быть, лучше просто иметь UniversityId и сервис (использующий внедрение зависимостей), который будет предоставлять объект University из UniversityId.
Ян
12
Если ученик очень сложный или его трудно инициализировать, просто издевайтесь над ним. Тестирование намного эффективнее с такими фреймворками, как Mockito или эквивалентами других языков.
Боржаб,
4
Если showInfo не заботится об университете, просто установите его в null. Нули ужасны в производстве и божья коровка в тестах. Возможность указать параметр как нулевой в тестах сообщает намерение и говорит, что «эта вещь здесь не нужна». Хотя я бы также подумал о создании некоторой модели представления для студентов, содержащей только соответствующие данные, учитывая, что showInfo звучит как метод в классе пользовательского интерфейса.
Сара
22

С точки зрения непрофессионала:

  • То, что вы называете «пользовательским объектом» , обычно просто называется объектом.
  • Вы не можете избежать передачи объектов в качестве параметров при разработке любой нетривиальной программы или API или использовании любого нетривиального API или библиотеки.
  • Это нормально, чтобы передавать объекты в качестве параметров. Взгляните на Java API, и вы увидите множество интерфейсов, которые получают объекты в качестве параметров.
  • Классы в библиотеках, которые вы используете, написаны простыми смертными, такими как вы и я, так что те, которые мы пишем, не "обычай" , а просто так.

Редактировать:

Как утверждает @ Tom.Bowen89, протестировать метод showInfo не намного сложнее:

showInfo(new Student(8812372,"Peter Parker",16,8.9));
Тулаинс Кордова
источник
3
  1. В вашем примере со студентом я предполагаю, что тривиально вызвать конструктор Student, чтобы создать студента для передачи в showInfo. Так что проблем нет.
  2. Если предположить, что пример Student намеренно упрощен для этого вопроса, и его сложнее построить, тогда вы можете использовать двойной тест . Есть несколько вариантов тестовых двойников, макетов, заглушек и т. Д., О которых говорится в статье Мартина Фаулера.
  3. Если вы хотите сделать функцию showInfo более общей, вы можете сделать так, чтобы она перебирала открытые переменные или, возможно, передавала открытые методы доступа к объекту и выполняла логику показа для всех них. Затем вы можете передать любой объект, который соответствует этому контракту, и он будет работать, как ожидалось. Это было бы хорошим местом для использования интерфейса. Например, передайте объект Showable или ShowInfoable в функцию showInfo, которая может отображать не только информацию об учениках, но и информацию о любом объекте, который реализует интерфейс (очевидно, эти интерфейсы нуждаются в лучших именах в зависимости от того, насколько конкретным или универсальным вы хотите передать объект, в который вы можете передать быть и что ученик подкласс).
  4. Часто проще обойти примитивы, а иногда это необходимо для производительности, но чем больше вы можете группировать сходные концепции, тем более понятным будет ваш код. Единственное, на что нужно обратить внимание, это попытаться не переусердствовать и в конечном итоге получить корпоративный fizzbuzz .
Encaitar
источник
3

Стив Макконнелл из Code Complete рассмотрел эту проблему, обсудив преимущества и недостатки передачи объектов в методы вместо использования свойств.

Простите, если я ошибаюсь в некоторых деталях, я работаю по памяти, так как прошло больше года с тех пор, как у меня был доступ к книге:

Он приходит к выводу, что лучше не использовать объект, а отправлять только те свойства, которые абсолютно необходимы для метода. Метод не должен знать ничего об объекте за пределами свойств, которые он будет использовать как часть своих операций. Кроме того, со временем, если объект когда-либо изменяется, это может иметь непредвиденные последствия для метода, использующего объект.

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

Однако иногда, иногда вам действительно нужно много параметров. В качестве примера он привел бы метод, который строит полный адрес, используя множество различных свойств адреса (хотя это можно обойти, используя строковый массив, когда вы об этом думаете).

user1666620
источник
7
У меня есть полный код 2. Там есть целая страница, посвященная этой проблеме. Вывод таков: параметры должны быть на правильном уровне абстракции. Иногда это требует передачи всего объекта, иногда только отдельных атрибутов.
Приходите с
UV. Ссылка на кодовый код - это в два раза лучше. Хорошее рассмотрение дизайна над целесообразностью тестирования. Предпочитаю дизайн, думаю Макконнелл сказал бы в нашем контексте. Таким образом, превосходным выводом было бы «интегрировать объект параметров в проект» ( Studentв данном случае). И именно так тестирование определяет дизайн , полностью принимая ответ с наибольшим количеством голосов при сохранении целостности дизайна.
радар Боб
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 плох.

Abuzittin Gillifirca
источник
1

Вы должны передать то, что имеет смысл, некоторые идеи:

Проще проверить. Если объект (ы) необходимо отредактировать, что требует наименьшего рефакторинга? Полезно ли повторно использовать эту функцию для других целей? Какое наименьшее количество информации мне нужно, чтобы эта функция выполнялась? (Разбивая его - он может позволить вам повторно использовать этот код - будьте осторожны, чтобы не упасть в конструктивную дыру создания этой функции, а затем ограничить все возможности исключительно для использования этого объекта.)

Все эти правила программирования - всего лишь руководство, которое поможет вам мыслить в правильном направлении. Просто не создавайте зверя с кодом - если вы не уверены и вам просто нужно продолжить, выберите направление / ваше собственное или предложение здесь, и если вы достигли точки, когда вы думаете: «О, я должен был сделать это так» путь »- тогда вы, вероятно, сможете вернуться и довольно легко изменить его. (Например, если у вас есть класс Teacher - ему просто нужно установить то же свойство, что и для Student, и вы измените свою функцию для принятия любого объекта формы Person)

Я был бы наиболее склонен к тому, чтобы основной объект передавался - потому что, как я кодирую, легче объяснить, что делает эта функция.

Лилли
источник
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) {

    }
}
OldCurmudgeon
источник
Если showInfo хочет отображать только имя студента, почему бы просто не передать имя? оборачивание семантически значимого именованного поля в абстрактный интерфейс, содержащее строку, не имеющую понятия о том, что представляет собой строка, ощущается как ОГРОМНОЕ понижение, как с точки зрения удобства сопровождения, так и с точки зрения понятности.
Сара
@kai - Использование Studentи Stringздесь для типа возврата только для демонстрации. Скорее всего, будут дополнительные параметры, getInfoнапример, Paneдля рисования при рисовании. Идея заключается в том, чтобы передать функциональные компоненты в качестве декораторов исходного объекта .
OldCurmudgeon
в этом случае вы будете тесно привязывать студенческую сущность к вашей структуре пользовательского интерфейса, что звучит еще хуже ...
Сара
1
@kai - как раз наоборот. Мой интерфейс знает только об HasInfoобъектах. Studentзнает как быть.
OldCurmudgeon
Если вы дадите getInforeturn void, передайте его a Paneдля рисования, тогда реализация (в Studentклассе) внезапно соединится с swing или чем-то, что вы используете. Если вы заставите его вернуть некоторую строку и принять 0 параметров, то ваш пользовательский интерфейс не будет знать, что делать со строкой без магических предположений и неявного связывания. Если вы getInfoдействительно вернете некоторую модель представления с соответствующими свойствами, тогда ваш Studentкласс снова будет связан с логикой представления. Я не думаю, что любая из этих альтернатив желательна
Сара
1

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

  • Ваш пример показывает, что Student (явно объект модели) передается в Window (очевидно, объект уровня представления). Промежуточный объект Controller или Presenter может быть полезен, если у вас его еще нет, что позволяет вам изолировать ваш пользовательский интерфейс от вашей модели. Контроллер / презентатор должен предоставлять интерфейс, который можно использовать для его замены для тестирования пользовательского интерфейса, и должен использовать интерфейсы для обращения к объектам модели и просмотра объектов, чтобы иметь возможность изолировать его от обоих для тестирования. Вам может понадобиться какой-то абстрактный способ их создания или загрузки (например, объекты Factory, объекты Repository или аналогичные).

  • Перенос соответствующих частей ваших объектов модели в объект передачи данных является полезным подходом для взаимодействия, когда ваша модель становится слишком сложной.

  • Может случиться так, что ваш ученик нарушает принцип разделения интерфейса. Если это так, может быть полезно разделить его на несколько интерфейсов, с которыми проще работать.

  • Ленивая загрузка может упростить построение больших графов объектов.

Жюль
источник
0

Это на самом деле достойный вопрос. Настоящая проблема здесь заключается в использовании общего термина «объект», который может быть немного двусмысленным.

Как правило, в классическом языке ООП термин «объект» стал означать «экземпляр класса». Экземпляры классов могут быть довольно тяжелыми - общедоступные и частные свойства (и промежуточные), методы, наследование, зависимости и т. Д. На самом деле вы не захотите использовать что-то подобное для простой передачи некоторых свойств.

В этом случае вы используете объект в качестве контейнера, который просто содержит некоторые примитивы. В C ++ такие объекты были известны как structs(и они все еще существуют в таких языках, как C #). Структуры, на самом деле, были разработаны именно для использования, о котором вы говорите - они группировали связанные объекты и примитивы вместе, когда у них были логические отношения.

Однако в современных языках нет никакой разницы между структурой и классом, когда вы пишете код , так что вы можете использовать объект. (Однако за кулисами есть некоторые отличия, о которых вы должны знать - например, структура - это тип значения, а не ссылочный тип.) В основном, если вы сохраняете свой объект простым, это будет легко проверить вручную. Современные языки и инструменты позволяют вам немного смягчить это (через интерфейсы, макеты, внедрение зависимостей и т. Д.)

lunchmeat317
источник
1
Передача ссылки не требует больших затрат, даже если размер объекта составляет миллиард терабайт, поскольку в большинстве языков ссылка все еще имеет размер int. Вам следует больше беспокоиться о том, не подвержен ли метод приема слишком большому API, и связываете ли вы вещи нежелательным образом. Я хотел бы рассмотреть создание слоя отображения, который переводит бизнес-объекты ( Student) в модели представлений ( StudentInfoили StudentInfoViewModelт. Д.), Но это может быть необязательно.
Сара
Классы и структуры очень разные. Один передается по значению (то есть метод, получающий его, получает копию ), а другой передается по ссылке (получатель просто получает указатель на оригинал). Непонимание этой разницы опасно.
RubberDuck
@ Kai Я понимаю, что передача ссылки не дорого. Я хочу сказать, что создание функции, требующей полного экземпляра класса, может быть более сложным для тестирования в зависимости от зависимостей этого класса, его методов и т. Д., Поскольку вам придется каким-то образом насмехаться над этим классом.
lunchmeat317
Лично я против насмешек практически над чем угодно, кроме граничных классов, которые обращаются к внешним системам (файловый / сетевой ввод-вывод) или недетерминированных (например, случайных, основанных на системном времени и т. Д.). Если у класса, который я тестирую, есть зависимость, которая не относится к текущей тестируемой функции, я предпочитаю просто пропустить null, где это возможно. Но если вы тестируете метод, который принимает объект параметра, если у этого объекта есть куча зависимостей, я бы беспокоился об общем проекте. Такие предметы должны быть легкими.
Сара