While this recommendation revolves about arguments, it's not fundamentally about arguments.
The key point is this:
"it is likely that some of those arguments ought to be wrapped into a
class of their own"
Why? Usually, when you have a large number of arguments, some of those arguments will be more closely related than others. E.g., they will be related to the same concept, and in the method itself, there will be some logic that manipulates those clusters of arguments to achieve something. The problem is, that concept is not made explicit, and the logic related to it is not clearly delineated. What you likely have in there is code that intermingles different concepts, or different levels of abstraction. Mixes responsibilities on a local level, if you will.
You've said:
"As far as understand things, classes are supposed to represent
meaningful objects"
Yeah! A class is an explicit representation of some concept. And when you have a large number of parameters, it's worth checking if there's a meaningful/useful concept in there that's currently not represented, and if it should be.
So it's not just about bundling parameters together.
E.g., inside the method, there's probably going to be a code block that only uses a couple of parameters with a comment on top of it explaining what it does. Or maybe there will be a block comprised of one or more if-conditionals. And the details of that block are not going to be the main point of the method. You can probably do the Extract Method refactoring and get a cleaner, easier to understand code in the original method - code that more succinctly expresses what the method actually does. But then you should ask yourself, does the newly extracted method really belong to the same class? Maybe it does, but maybe it would be better to relocate it.
Look at this method; this is a contrived example, but think of something like a 2D game - this checks if two game entities have collided (passed through each other) and produces a result that describes the collision and how to resolve it (how to reposition the two entities). In reality, this would probably need to take in additional parameters, but let's keep it relatively simple.
// Takes two axis-aligned rectangles representing the bounding boxes
// of two entities and returns a CollisionInfo instance.
CollisionInfo ResolveColision(
double left1, double top1, double right1, double bottom1,
double left2, double top2, double right2, double bottom2)
{
// Prepare some variables to store stuff
// (... omitted ...)
// Compute the intersection
// (... a wall of code ...)
// (... a wall of code ...)
// (... a wall of code ...)
// (... a wall of code ...)
// (... a wall of code ...)
// (... a wall of code ...)
// (... a wall of code ...)
// ...
// (... a wall of code ...)
// (... a wall of code ...)
// (... a wall of code ...)
// (... a wall of code ...)
// (... a wall of code ...)
// (... a wall of code ...)
// (... a wall of code ...)
// Figure out how to resolve the collision
// (... omitted ...)
// Create the output data structure
var collisionInfo = // (... omitted ...)
return collisionInfo;
}
If you look at this code, there are hints that there are some concepts in there that lack explicit representation. E.g., the description of the method mentions axis-aligned bounding rectangles. The parameter list is formatted in two rows in a way that looks deliberate, and the suffixes in the names of the parameters indicate that there are really two objects there, not eight. Then (if you're lucky) there's a comment explaining what that wall of code is doing.
Well, let's create a class to represent an axis-aligned bounding rectangle, let's give it an Intersect
method, and let's put that wall of code in there.
This may require some cleanup, because the different parts of the code probably depend on each other in subtle ways (variables may be reused, one part of the code may make assumptions about the previous section, etc.)
But after the separation, I can be a bit more declarative - I just want to tell the code to compute the intersection; I don't care how:
// The method is now essentially self-documenting;
// that documentation comment from before is now redundant
CollisionInfo ResolveColision(BoundingRect rect1, BoundingRect rect2)
{
BoundingRect intersection = rect1.Intersect(rect2);
// Use 'intersection' to figure out how to resolve the collision
// (... omitted ...)
return new CollisionInfo(/* ... omitted ... */);
}
// Sometimes, you may choose to keep the original method as an
// overload, for convenience, or for backward compatibility.
// But this just delegates to the previous method, so it's not a
// problem in terms of readability, maintenance, etc.
CollisionInfo ResolveColision(
double left1, double top1, double right1, double bottom1,
double left2, double top2, double right2, double bottom2)
{
return ResolveCollision(
new BoundingRectangle(left1, top1, right1, bottom1)
new BoundingRectangle(left2, top2, right2, bottom2));
}
Besides, now I can reuse the BoundingRect
class, and its Intersect
method, elsewhere, and I can change the implementation of Intersect
without affecting the code that calls it.
"Many functions in standard libraries have multiple arguments."
The prevalence of something doesn't mean that it's an example of good design or that it should be emulated. Again, sometimes a large(r) number of arguments is the way to go, but first check if that's really true. Look for those implicit concepts, consider ease of use, etc.
P.S. In OOP, sometimes you'll have ordinary, dumb data structures with no behavior. These will also be represented by classes, but they'll have only getters and setters, with a few or no methods. These can appear in a number of places, notably, at the boundaries of the application - where you communicate with the UI, or the web, or the DB, etc. Or maybe in a part of the codebase that's written in a more functional style. So sometimes refactoring a large parameter list into a smaller number of objects will not be due to refining the OO design of your object model, but it will happen in order to confirm to external interfaces, or simply for convenience.
void draw_rect(aabb2f rect)
which satisfies Martin's criteria, we favorvoid draw_rect(float x1, float y1, float x2, float y2)
which doesn't... just for really low-level stuff like this. I've found in trying to propose and create the analogicalaabb2f
and use it widely, that it causes too much strife among the team with one person wanting half-size/center rep, and another min/max, e.g., and difficulty making such bundled representations last over the years without deprecation over more scalar types of parameters.