55

Suppose I have a custom object, Student:

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

And a class, Window, that is used to show information of a Student:

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

It looks quite normal, but I found Window is not quite easy to test individually, because it needs a real Student object to call the function. So I try to modify showInfo so that it does not accept a Student object directly:

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

so that it is easier to test Window individually:

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

But I found the modified version has another problems:

  1. Modify Student (e.g.:add new properties) requires modifying the method-signature of showInfo

  2. If Student had many properties, the method-signature of Student would be very long.

So, using custom objects as parameter or accept each property in objects as parameter, which one is more maintainable?

Tulains Córdova
  • 39,570
  • 13
  • 100
  • 156
ggrr
  • 5,873

10 Answers10

140

Using a custom object to group related parameters is actually a recommended pattern. As a refactoring, it is called Introduce Parameter Object.

Your problem lies elsewhere. First, generic Window should know nothing about Student. Instead, you should have some kind of StudentWindow that knows about only displaying Students. Second, there is absolutely no problem about creating Student instance to test StudentWindow as long as Student doesn't contain any complex logic that would drastically complicate testing of StudentWindow. If it does have that logic then making Student an interface and mocking it should be preferred.

Euphoric
  • 38,149
29

You say it's

not quite easy to test individually, because it needs a real Student object to call the function

But you can just create a student object to pass to your window:

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

It doesn't seem much more complex to call.

Tom Bowen
  • 390
24

In layman's terms:

  • What you call a "custom object" is usually simply called an object.
  • You cannot avoid passing objects as parameters when designing any non-trivial program or API, or using any non-trivial API or library.
  • It's perfectly OK to pass objects as parameters. Take a look at the Java API and you will see lots of interfaces that receive objects as parameters.
  • The classes in the libraries you use where written by mere mortals like you and me, so the ones we write are not "custom", they just are.

Edit:

As @Tom.Bowen89 states it isn't much more complex to test the showInfo method:

showInfo(new Student(8812372,"Peter Parker",16,8.9));
Tulains Córdova
  • 39,570
  • 13
  • 100
  • 156
4

Steve McConnell in Code Complete addressed this very issue, discussing the benefits and drawbacks of passing objects into methods instead of using properties.

Forgive me if I get some of the details wrong, am working from memory as it's been over a year since I had access to the book:

He comes to the conclusion that you are better off not using an object, instead sending only those properties absolutely necessary for the method. The method should not have to know anything about the object outside of the properties it will be using as part of its operations. Also, over time, if the object is ever changed, this could have unintended consequences on the method using the object.

Also he addressed that if you end up with a method which accepts a lot of different arguments, then that is probably a sign that the method is doing too much and should be broken down into more, smaller methods.

However, sometimes, sometimes, you actually do need a lot of parameters. The example he gives would be of a method that builds a full address, using many different address properties (though this could be gotten around by using a string array when you think about it).

3
  1. In your student example I assume it is trivial to call the Student constructor to create a student to pass in to showInfo. So there is no problem.
  2. Assuming the example Student is deliberately trivialised for this question and it is more difficult to construct, then you could use a test double. The are a number of options for test doubles, mocks, stubs etc. that are talked about in Martin Fowler's article to choose from.
  3. If you want to make the showInfo function more generic you could have it iterate over the public variables, or maybe the public accessors of the object passed in and perform the show logic for all of them. Then you could pass in any object that conformed to that contract and it would work as expected. This would be a good place to use an interface. For example pass in a Showable or ShowInfoable object to the showInfo function that can show not just students info but any object's info that implements the interface (obviously those interfaces need better names depending on how specific or generic you want the object you can pass in to be and what a Student is a sub class of).
  4. It is often easier to pass around primitives, and sometimes necessary for performance, but the more you can group similar concepts together the more understandable your code will generally be. The only thing to watch out for is to try not to over do it and end up with enterprise fizzbuzz.
Encaitar
  • 3,085
2

It is much easier to write and read tests if you pass the whole object:

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

For comparison,

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

line could be written, if you pass values separately, as:

showAStudentWithAFailingGrade(view);

where actual method call is buried somewhere like

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

To be to the point, that you cannot put the actual method call in the test is a sign that you API is bad.

1

You should pass what makes sense, some ideas:

Easier to test. If the object(s) need to be edited, what requires the least refactoring? Is re-using this function for other purposes useful? What is the least amount of information I need to give this function to do it's purpose? (By breaking it up--it may let you re-use this code--be wary of falling down the design hole of making this function and then bottlenecking everything to exclusively use this object.)

All these programming rules are just guides to get you thinking in the right direction. Just don't build a code beast--if you're unsure and just need to proceed, pick a direction/your own or a suggestion here, and if you hit a point where you think 'oh, I should have made it this way'--you probably can then go back and refactor it pretty easily. (For instance if you have Teacher class--it just needs the same property set as Student, and you change your function to accept any object of the Person form)

I would be most inclined to keep the main object being passed--because how I code it's going to more easily explain what this function is doing.

Lilly
  • 11
1

One common route around this is to insert an interface between the two processes.

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

    }
}

This gets a little messy sometimes but things get a little tidier in Java if you use an inner class.

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

        };
    }
}

You can then test the Window class by just giving it a fake HasInfo object.

I suspect this is an example of the Decorator Pattern.

Added

There seems to be some confusion caused by the simplicity of the code. Here's another example that may demonstrate the technique better.

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

    }
}
1

You have a lot of good answers already, but here are a few more suggestions that may allow you to see an alternative solution:

  • Your example shows a Student (clearly a model object) being passed to a Window (apparently a view-level object). An intermediary Controller or Presenter object may be beneficial if you don't already have one, allowing you to isolate your user interface from your model. The controller/presenter should provide an interface that can be used to replace it for UI testing, and should use interfaces to refer to model objects and view objects to be able to isolate it from both of those for testing. You may need to provide some abstract way of creating or loading these (eg Factory objects, Repository objects, or similar).

  • Transfering relevant parts of your model objects into a Data Transfer Object is a useful approach for interfacing when your model becomes too complex.

  • It may be that your Student violates the Interface Segregation Principle. If so, it could be beneficial to split it into multiple interfaces that are easier to work with.

  • Lazy Loading can make construction of large object graphs easier.

Jules
  • 17,880
  • 2
  • 38
  • 65
0

This is actually a decent question. The real issue here is the use of the generic term "object", which can be a bit ambiguous.

Generally, in a classical OOP language, the term "object" has come to mean "class instance". Class instances can be pretty heavy - public and private properties (and those in between), methods, inheritance, dependencies, etc. You wouldn't really want to use something like that to simply pass in some properties.

In this case, you're using an object as a container that simply hold some primitives. In C++, objects such as these were known as structs (and they still exist in languages like C#). Structs were, in fact, designed exactly for the usage of which you speak - they grouped related objects and primitives together when they had a logical relationship.

However, in modern languages, there's really no difference between a struct and a class when you're writing the code, so you're okay using an object. (Behind the scenes, however, there are some differences which you should be aware of - for instance, a struct is a value type, not a reference type.) Basically, as long as you keep your object simple, it'll be easy to test manually. Modern languages and tools allow you to mitigate this quite a bit, though (via interfaces, mocking frameworks, dependency injection, etc.)