Chapter 5. Keep Unit Interfaces Small

Bunches of data that hang around together really ought to be made into their own object.

Martin Fowler

Guideline:

  • Limit the number of parameters per unit to at most 4.

  • Do this by extracting parameters into objects.

  • This improves maintainability because keeping the number of parameters low makes units easier to understand and reuse.

There are many situations in the daily life of a programmer where long parameter lists seem unavoidable. In the rush of getting things done, you might add a few parameters more to that one method in order to make it work for exceptional cases. In the long term, however, such a way of working will lead to methods that are hard to maintain and hard to reuse. To keep your code maintainable it is essential to avoid long parameter lists, or unit interfaces, by limiting the number of parameters they have.

A typical example of a unit with many parameters is the render method in the BoardPanel class of JPacman. This method renders a square and its occupants (e.g., a ghost, a pellet) in a rectangle given by the x,y,w,h parameters.

/**
 * Renders a single square on the given graphics context on the specified
 * rectangle.
 *
 * @param square
 *            The square to render.
 * @param g
 *            The graphics context to draw on.
 * @param x
 *            The x position to start drawing.
 * @param y
 *            The y position to start drawing.
 * @param w
 *            The width of this square (in pixels).
 * @param h
 *            The height of this square (in pixels).
 */
private void render(Square square, Graphics g, int x, int y, int w, int h) {
    square.getSprite().draw(g, x, y, w, h);
    for (Unit unit : square.getOccupants()) {
        unit.getSprite().draw(g, x, y, w, h);
    }
}

This method exceeds the parameter limit of 4. Especially the last four arguments, all of type int, make the method harder to understand and its usage more error-prone than necessary. It is not unthinkable that after a long day of writing code, even an experienced developer could mix up the x,y,w and h parameters—a mistake that the compiler and possibly even the unit tests will not catch.

Because the x,y,w, and h variables are related (they define a rectangle with a 2D anchor point, a width and a height), and the render method does not manipulate these variables independently, it makes sense to group them into an object of type Rectangle. The next code snippets show the Rectangle class and the refactored render method:

public class Rectangle {
    private final Point position;
    private final int width;
    private final int height;

    public Rectangle(Point position, int width, int height) {
        this.position = position;
        this.width = width;
        this.height = height;
    }

    public Point getPosition() {
        return position;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }
}
/**
 * Renders a single square on the given graphics context on the specified
 * rectangle.
 *
 * @param square
 *            The square to render.
 * @param g
 *            The graphics context to draw on.
 * @param r
 *            The position and dimension for rendering the square.
 */
private void render(Square square, Graphics g, Rectangle r) {
    Point position = r.getPosition();
    square.getSprite().draw(g, position.x, position.y, r.getWidth(),
        r.getHeight());
    for (Unit unit : square.getOccupants()) {
        unit.getSprite().draw(g, position.x, position.y, r.getWidth(),
            r.getHeight());
    }
}

Now the render method has only three parameters instead of six. Next to that, in the whole system we now have the Rectangle class available to work with. This allows us to also create a smaller interface for the draw method:

private void render(Square square, Graphics g, Rectangle r) {
    Point position = r.getPosition();
    square.getSprite().draw(g, r);
    for (Unit unit : square.getOccupants()) {
        unit.getSprite().draw(g, r);
    }
}

The preceding refactorings are an example of the Introduce Parameter Object refactoring pattern. Avoiding long parameter lists, as shown in the previous example, improves the readability of your code. In the next section, we explain why small interfaces contribute to the overall maintainability of a system.

Motivation

As we already discussed in the introduction, there are good reasons to keep interfaces small and to introduce suitable objects for the parameters you keep passing around in conjunction. Methods with small interfaces keep their context simple and thus are easier to understand. Furthermore, they are easier to reuse and modify because they do not depend on too much external input.

Small Interfaces Are Easier to Understand and Reuse

As the codebase grows, the core classes become the API upon which a lot of other code in the system builds. In order to keep the volume of the total codebase low (see also Chapter 9) and the speed of development high, it is important that the methods in the core classes have a clear and small interface. Suppose you want to store a ProductOrder object in the database: would you prefer a ProductOrderDao.store(ProductOrder order) method or a ProductOrderDao.store(ProductOrder order, String databaseUser, String databaseName, boolean validateBeforeStore, boolean closeDbConnection) method?

Methods with Small Interfaces Are Easier to Modify

Large interfaces do not only make your methods obscure, but in many cases also indicate multiple responsibilities (especially when you feel that you really cannot group your objects together anymore). In this sense, interface size correlates with unit size and unit complexity. So it is pretty obvious that methods with large interfaces are hard to modify. If you have, say, a method with eight parameters and a lot is going on in the method body, it can be difficult to see where you can split your method into distinct parts. However, once you have done so, you will have several methods with their own responsibility, and moreover, each method will have a small number of parameters! Now it will be much easier to modify each of these methods, because you can more easily locate exactly where your modification needs to be done.

How to Apply the Guideline

By the time you have read this, you should be convinced that having small interfaces is a good idea. How small should an interface be? In practice, an upper bound of four seems reasonable: a method with four parameters is still reasonably clear, but a method with five parameters is already getting difficult to read and has too many responsibilities.

So how can you ensure small interfaces? Before we show you how you can fix methods with large interfaces, keep in mind that large interfaces are not the problem, but rather are indicators of the actual problem—a poor data model or ad hoc code modification. So, you can view interface size as a code smell, to see whether your data model needs improvement.

Important

Large interfaces are usually not the main problem; rather, they are a code smell that indicates a deeper maintainability problem.

Let us say you have a buildAndSendMail method that takes a list of nine parameters in order to construct and send an email message. However, if you looked at just the parameter list, it would not be very clear what would happen in the method body:

public void buildAndSendMail(MailMan m, String firstName, String lastName,
    String division, String subject, MailFont font, String message1,
    String message2, String message3) {
    // Format the email address
    String mId = firstName.charAt(0) + "." + lastName.substring(0, 7) + "@"
        + division.substring(0, 5) + ".compa.ny";
    // Format the message given the content type and raw message
    MailMessage mMessage = formatMessage(font,
        message1 + message2 + message3);
    // Send message
    m.send(mId, subject, mMessage);
}

The buildAndSendMail method clearly has too many responsibilities; the construction of the email address does not have much to do with sending the actual email. Furthermore, you would not want to confuse your fellow programmer with five parameters that together will make up a message body! We propose the following revision of the method:

public void buildAndSendMail(MailMan m, MailAddress mAddress,
    MailBody mBody) {
    // Build the mail
    Mail mail = new Mail(mAddress, mBody);
    // Send the mail
    m.sendMail(mail);
}

private class Mail {
    private MailAddress address;
    private MailBody body;

    private Mail(MailAddress mAddress, MailBody mBody) {
        this.address = mAddress;
        this.body = mBody;
    }
}

private class MailBody {
    String subject;
    MailMessage message;

    public MailBody(String subject, MailMessage message) {
        this.subject = subject;
        this.message = message;
    }
}

private class MailAddress {
    private String mId;

    private MailAddress(String firstName, String lastName,
        String division) {
        this.mId = firstName.charAt(0) + "." + lastName.substring(0, 7)
            + "@"
            + division.substring(0, 5) + ".compa.ny";
    }
}

The buildAndSendMail method is now considerably less complex. Of course, you now have to construct the email address and message body before you invoke the method. But if you want to send the same message to several addresses, you only have to build the message once, and similarly for the case where you want to send a bunch of messages to one email address. In conclusion, we have now separated concerns, and while we did so we introduced some nice, structured classes.

The examples presented in this chapter all group parameters into objects. Such objects are often called data transfer objects or parameter objects. In the examples, these new objects actually represent meaningful concepts from the domain. A point, a width, and a height represent a rectangle, so grouping these in a class called Rectangle makes sense. Likewise, a first name, a last name, and a division make an address, so grouping these in a class called MailAddress makes sense, too. It is not unlikely that these classes will see a lot of use in the codebase because they are useful generalizations, not just because they may decrease the number of parameters of a method.

What if we have a number of parameters that do not fit well together? We can always make a parameter object out of them, but probably, it will be used only once. In such cases, another approach is often possible, as illustrated by the following example.

Suppose we are creating a library that can draw charts, such as bar charts and pie charts, on a java.awt.Graphics canvas. To draw a nice-looking chart, you usually need quite a bit of information, such as the size of the area to draw on, configuration of the category axis and value axis, the actual dataset to chart, and so forth. One way to supply this information to the charting library is like this:

public static void drawBarChart(Graphics g,
    CategoryItemRendererState state,
    Rectangle graphArea,
    CategoryPlot plot,
    CategoryAxis domainAxis,
    ValueAxis rangeAxis,
    CategoryDataset dataset) {
    // ..
}

This method already has seven parameters, three more than the guideline presented in this chapter allows. Moreover, any call to drawBarChart needs to supply values for all seven parameters. What if the charting library provided default values wherever possible? One way to implement this is to use method overloading and define, for instance, a two-parameter version of drawBarChart:

public static void drawBarChart(Graphics g, CategoryDataset dataset) {
    Charts.drawBarChart(g,
        CategoryItemRendererState.DEFAULT,
        new Rectangle(new Point(0, 0), 100, 100),
        CategoryPlot.DEFAULT,
        CategoryAxis.DEFAULT,
        ValueAxis.DEFAULT,
        dataset);
}

This covers the case where we want to use defaults for all parameters whose data types have a default value defined. However, that is just one case. Before you know it, you are defining more than a handful of alternatives like these. And the version with seven parameters is still there.

Another way to solve this is to use the Replace Method with Method Object refactoring technique presented in Chapter 2. This refactoring technique is primarily used to make methods shorter, but it can also be used to reduce the number of method parameters.

To apply the Replace Method with Method Object technique to this example, we define a BarChart class like this:

public class BarChart {
    private CategoryItemRendererState state = CategoryItemRendererState.DEFAULT;
    private Rectangle graphArea = new Rectangle(new Point(0, 0), 100, 100);
    private CategoryPlot plot = CategoryPlot.DEFAULT;
    private CategoryAxis domainAxis = CategoryAxis.DEFAULT;
    private ValueAxis rangeAxis = ValueAxis.DEFAULT;
    private CategoryDataset dataset = CategoryDataset.DEFAULT;

    public BarChart draw(Graphics g) {
        // ..
        return this;
    }

    public ValueAxis getRangeAxis() {
        return rangeAxis;
    }

    public BarChart setRangeAxis(ValueAxis rangeAxis) {
        this.rangeAxis = rangeAxis;
        return this;
    }

    // More getters and setters.

}

The static method drawBarChart from the original version is replaced by the (nonstatic) method draw in this class. Six of the seven parameters of drawBarChart have been turned into private members of BarChart class, and have public getters and setters. All of these have default values. We have chosen to keep parameter g (of type java.awt.Graphics) as a parameter of draw. This is a sensible choice: draw always needs a Graphics object, and there is no sensible default value. But it is not necessary: we could also have made g into the seventh private member and supplied a getter and setter for it.

We made another choice: all setters return this to create what is called a fluent interface. The setters can then be called in a cascading style, like so:

private void showMyBarChart() {
    Graphics g = this.getGraphics();
    BarChart b = new BarChart()
        .setRangeAxis(myValueAxis)
        .setDataset(myDataset)
        .draw(g);
}

In this particular call of draw, we provide values for the range axis, dataset, and g, and use default values for the other members of BarChart. We could have used more default values or fewer, without having to define additional overloaded draw methods.

Common Objections to Keeping Unit Interfaces Small

It may take some time to get rid of all large interfaces. Typical objections to this effort are discussed next.

Objection: Parameter Objects with Large Interfaces

“The parameter object I introduced now has a constructor with too many parameters.”

If all went well, you have grouped a number of parameters into an object during the refactoring of a method with a large interface. It may be the case that your object now has a lot of parameters because they apparently fit together. This usually means that there is a finer distinction possible inside the object. Remember the first example, where we refactored the render method? Well, the defining parameters of the rectangle were grouped together, but instead of having a constructor with four arguments we actually put the x and y parameters together in the Point object. So, in general, you should not refuse to introduce a parameter object, but rather think about the structure of the object you are introducing and how it relates to the rest of your code.

Refactoring Large Interfaces Does Not Improve My Situation

“When I refactor my method, I am still passing a lot of parameters to another method.”

Getting rid of large interfaces is not always easy. It usually takes more than refactoring one method. Normally, you should continue splitting responsibilities in your methods, so that you access the most primitive parameters only when you need to manipulate them separately. For instance, the refactored version of the render method needs to access all parameters in the Rectangle object because they are input to the draw method. But it would be better, of course, to also refactor the draw method to access the x,y,w, and h parameters inside the method body. In this way, you have just passed a Rectangle in the render method, because you do not actually manipulate its class variables before you begin drawing!

Frameworks or Libraries Prescribe Interfaces with Long Parameter Lists

“The interface of a framework we’re using has nine parameters. How can I implement this interface without creating a unit interface violation?”

Sometimes frameworks/libraries define interfaces or classes with methods that have long parameter lists. Implementing or overriding these methods will inevitably lead to long parameter lists in your own code. These types of violations are impossible to prevent, but their impact can be limited. To limit the impact of violations caused by third-party frameworks or libraries, it is best to isolate these violations—for instance, by using wrappers or adapters. Selecting a different framework/library is also a viable alternative, although this can have a large impact on other parts of the codebase.

See Also

Methods with multiple responsibilities are more likely when the methods are large and complex. Therefore, make sure that you understand the guidelines for achieving short and simple units. See Chapters 2 and 3.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset