Bunches of data that hang around together really ought to be made into their own object.
Martin Fowler
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.
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.
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?
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.
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.
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
=
new
(
mAddress
,
mBody
);
// Send the mail
m
.
sendMail
(
);
}
private
class
{
private
MailAddress
address
;
private
MailBody
body
;
private
(
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.
It may take some time to get rid of all large interfaces. Typical objections to this effort are discussed next.
“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.
“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!
“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.