我们应该避免将自定义对象作为参数吗?


49

假设我有一个自定义对象Student

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的方法签名将非常长。

因此,使用自定义对象作为参数或接受对象中的每个属性作为参数,哪个更可维护?


40
而您的“改进”则showInfo需要一个真实的字符串,一个真实的浮点数和两个真实的整数。提供真实String物体比提供真实物体更好Student吗?
Bart van Ingen Schenau '16

28
直接传递参数的一个主要问题:现在有两个int参数。在呼叫站点,无法验证您是否确实以正确的顺序传递了它们。如果换成什么idage,或firstNamelastName?您将要引入一个潜在的故障点,直到它在您的脸上炸开,否则很难发现,并且将其添加到每个呼叫站点
克里斯·海斯

38
@ChrisHayes啊,旧showForm(bool, bool, bool, bool, int)方法-我喜欢那些...
蜘蛛鲍里斯(Boris)

3
@ChrisHayes至少不是它的JS ...
Jens Schauder

2
测试的一种未被重视的属性:如果很难在测试中创建/使用您自己的对象,则您的API可能会使用一些工作:)
Eevee 2016年

Answers:


131

推荐使用自定义对象对相关参数进行分组。作为重构,它称为Introduce Parameter Object

您的问题出在其他地方。首先,泛型Window对学生一无所知。相反,您应该StudentWindow对仅显示有所了解Students。其次,只要不包含任何会使的测试彻底复杂的复杂逻辑,创建Student实例进行测试就绝对没有问题。如果它确实具有该逻辑,则应首选接口并进行模拟。StudentWindowStudentStudentWindowStudent


14
值得警告的是,如果新对象不是真正的逻辑分组,您可能会遇到麻烦。不要试图将每个参数集都塞进单个对象中;根据具体情况决定。问题中的示例似乎很显然是一个很好的选择。该Student组是有道理的,并有可能在应用程序中的其他领域出现。
jpmc26

从脚步上讲,如果您已经有了该对象,例如Student,它将是一个保存整个对象
abuzittin gillifirca

4
还记得Demeter定律。有个平衡点要解决,但a.b.c如果您的方法需要tldr,则不要这样做a。如果您的方法达到需要大约4个以上参数或2级属性访问级别的地步,则可能需要将其排除在外。另请注意,这是一个准则-与所有其他准则一样,它需要用户自行决定。不要盲目跟随它。
丹·潘特里

7
我发现此答案的第一句话非常难以解析。
赫尔里希

5
@Qwerky我非常不同意。学生听起来确实像对象图中的一片叶子(除非有其他琐碎的对象,例如Name,DateOfBirth等),只是一个学生状态的容器。没有任何理由很难构造学生,因为它应该是记录类型。为学生创建测试双打听起来像是难以维持测试和/或严重依赖某些幻想隔离框架的秘诀。
2016年

26

你说是

单独测试不太容易,因为它需要一个真正的Student对象来调用该函数

但是您可以只创建一个学生对象以传递到您的窗口:

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

调用起来似乎并不复杂。


7
问题出在当Student指的是a时University,指的是许多Facultys和Campuss,其中Professors和Buildings都没有showInfo实际使用,但是您尚未定义任何允许测试“知道”并仅提供相关学生的接口数据,而无需建立整个组织。该示例Student是一个纯数据对象,就像您所说的,测试应该很高兴使用它。
史蒂夫·杰索普

4
问题出在学生指的是一所大学,而该大学指的是许多学院和校园,设有教授和建筑物,而邪恶的人则无法休息。
abuzittin gillifirca's

1
@abuzittingillifirca,“对象母亲”是一种解决方案,您的学生对象也可能太复杂。最好拥有UniversityId,并提供一个服务(使用依赖项注入),该服务将从UniversityId中提供University对象。
伊恩

12
如果Student非常复杂或难以初始化,请对其进行模拟。使用Mockito或其他等效语言的框架,测试的功能要强大得多。
Borjab '16

4
如果showInfo不关心大学,则只需将其设置为null。空值在生产中是可怕的,而在测试中却是天赐良机。能够在测试中将参数指定为null可以传达意图,并表示“此处不需要此内容”。尽管我也考虑为仅包含相关数据的学生创建某种视图模型,但考虑到showInfo听起来像UI类中的一种方法。
萨拉

22

用外行的话来说:

  • 您所谓的“自定义对象”通常简称为对象。
  • 在设计任何非平凡的程序或API或使用任何非平凡的API或库时,都无法避免将对象作为参数传递。
  • 将对象作为参数传递完全可以。看一下Java API,您将看到许多接口将对象作为参数接收。
  • 您使用的库中的类是由像您和我这样的凡人编写的,因此我们编写的类不是“ custom”,而是“ custom”

编辑:

正如@ Tom.Bowen89指出的那样,测试showInfo方法并不复杂:

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

3
  1. 在您的学生示例中,我认为调用Student构造函数来创建要传递给showInfo的学生是微不足道的。因此没有问题。
  2. 假设示例Student被故意琐碎地处理了这个问题,并且构造起来比较困难,那么您可以使用double检验。在Martin Fowler的文章中,有许多关于测试双打,模拟,存根等的选项可供选择。
  3. 如果要使showInfo函数更通用,则可以遍历公共变量,或者遍历传入的对象的公共访问器,并对所有变量执行show逻辑。然后,您可以传递符合该合同的任何对象,并且它将按预期工作。这将是使用界面的好地方。例如,将Showable或ShowInfoable对象传递给showInfo函数,该函数不仅可以显示学生信息,还可以显示实现该接口的任何对象的信息(显然,这些接口需要更好的名称,具体取决于您希望将对象传递给哪个特定对象或通用对象是,以及学生是其子类)。
  4. 通常,传递原语通常更容易,有时对于性能而言必要的,但是将相似概念组合在一起的次数越多,代码通常就越容易理解。唯一需要提防的事情是尽量不要过度做,最终会引起企业的嗡嗡声

3

Code Complete中的Steve McConnell解决了这个问题,讨论了将对象传递给方法而不是使用属性的优缺点。

如果我弄错了一些细节,请原谅我,这是从记忆中开始的工作,因为我已经有超过一年的时间了:

他得出的结论是,最好不要使用对象,而只发送该方法绝对必要的那些属性。该方法除了在操作中将要使用的属性之外,不必对对象有任何了解。同样,随着时间的流逝,如果曾经更改过对象,则可能会对使用该对象的方法产生意想不到的后果。

他还指出,如果最终得到的方法接受很多不同的参数,那么这可能表明该方法做得太多,应该分解为更多,更小的方法。

但是,有时有时确实需要很多参数。他给出的示例将是一种使用许多不同的地址属性来构建完整地址的方法(尽管您可以考虑使用字符串数组来解决此问题)。


7
我有完整的代码2。其中有一个专门讨论此问题的页面。结论是参数应处于正确的抽象级别。有时,这需要传递整个对象,有时仅传递单个属性。

紫外线 引用代码完成是双重加倍的好。设计优于测试权宜之计。我认为McConnell在我们的情况下会说更喜欢设计。因此,一个很好的结论是(Student在这种情况下)“将参数对象集成到设计中” 。这就是测试如何告知设计的方式,同时完全支持最投票的答案,同时保持设计的完整性。
–radbobo

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对象来测试该类。

我怀疑这是Decorator模式的示例。

添加

代码的简单性似乎引起了一些混乱。这是另一个可以更好地演示该技术的示例。

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只想显示学生的姓名,为什么不只传递姓名呢?在一个包含字符串的抽象接口中包装一个语义上有意义的命名字段,不包含关于字符串表示什么的线索,从可维护性和可理解性两方面来说都感觉像是巨大的降级。
2016年

@kai- 在返回类型中使用Studentand String这里纯粹是为了演示。如果绘图,可能会有其他参数,getInfo例如Pane。这里的概念是将功能组件作为原始对象的装饰器传递
OldCurmudgeon '16

在这种情况下,您可能会将学生实体紧密耦合到您的UI框架,这听起来更糟……
sara 2016年

1
@kai-恰恰相反。我的UI仅了解HasInfo对象。Student知道如何成为一个。
OldCurmudgeon '16

如果您给getInforeturn void传递了一个Pane绘制对象,则该实现(在Student类中)突然耦合到swing或您正在使用的任何对象。如果使它返回一些字符串并采用0参数,那么如果没有魔术假设和隐式耦合,您的UI将不知道如何处理该字符串。如果您getInfo实际上返回了具有相关属性的某些视图模型,那么您的Student类将再次与表示逻辑耦合。我认为这些替代方法均不理想
萨拉

1

您已经有很多不错的答案,但是这里有一些其他建议可以使您看到替代解决方案:

  • 您的示例显示了将Student(显然是模型对象)传递给Window(显然是视图级对象)的情况。如果您还没有中间Controller或Presenter对象,则可以从中将用户界面与模型隔离开来。控制器/演示者应提供一个可用于代替它进行UI测试的接口,并应使用该接口引用模型对象和视图对象,以便能够将其与两个对象隔离以进行测试。您可能需要提供一些抽象的方式来创建或加载这些方式(例如Factory对象,Repository对象或类似对象)。

  • 当模型变得过于复杂时,将模型对象的相关部分转移到数据传输对象中是一种有用的接口方法。

  • 您的学生可能违反了接口隔离原则。如果是这样,将其拆分为更易于使用的多个接口可能是有益的。

  • 延迟加载可以使大型对象图的构建更加容易。


0

这实际上是一个体面的问题。这里真正的问题是使用通用术语“对象”,这可能有点含糊。

通常,在经典的OOP语言中,术语“对象”已表示“类实例”。类实例可能非常繁重-公共和私有属性(以及介于它们之间的属性),方法,继承,依赖关系等。您实际上并不想使用类似的东西来简单地传递一些属性。

在这种情况下,您正在使用一个对象作为仅包含一些基元的容器。在C ++中,诸如此类的对象被称为structs(它们仍然以C#等语言存在)。实际上,结构是专为您所说的用途而设计的-当它们具有逻辑关系时,它们会将相关的对象和基元分组在一起。

但是,在现代语言中,编写代码时,结构和类之间实际上没有任何区别,因此可以使用对象。(但是,在幕后,您应该意识到一些差异,例如,结构是值类型,而不是引用类型。)基本上,只要您保持对象简单,就很容易手动测试。不过,现代语言和工具可以使您减轻很多(通过接口,模拟框架,依赖项注入等)。


1
即使对象的大小达到十亿兆字节,传递引用也并不昂贵,因为在大多数语言中,引用仍然只是int的大小。您应该更多地担心接收方法是否暴露给太大的api,以及是否以不希望的方式耦合对象。我会考虑制作一个将业务对象(Student)转换为视图模型(StudentInfo或其他StudentInfoViewModel)的映射层,但这可能不是必需的。
萨拉

类和结构非常不同。一个通过值传递(意味着接收它的方法获取一个副本),另一个通过引用传递(接收者仅获取指向原始对象的指针)。不了解这种差异是危险的。
RubberDuck

@kai我知道通过引用并不昂贵。我的意思是,根据类的依赖关系,其方法等,创建需要完整类实例的函数可能更难测试-因为您必须以某种方式模拟该类。
Lunchmeat317

我个人反对模拟几乎所有东西,除了访问外部系统(文件/网络IO)的边界类或非确定性的边界类(例如,随机的,基于系统时间的等等)。如果我正在测试的类具有与当前正在测试的功能无关的依赖关系,我更愿意在可能的地方传递null。但是,如果您正在测试采用参数对象的方法,则该对象具有大量依赖关系,那么我将担心整体设计。此类对象应轻巧。
萨拉
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.