Arguments and Results, James Noble. Part 1.
Summary and annotations of paper "Arguments and Results" by James Noble. Part 1: Three patterns to simplify functions with complex arguments.
This week I read “Arguments and Results” by James Noble. I felt it was a good follow up read to “On The Criteria To Be Used in Decomposing Systems into Modules”. Both the papers are from Michael Feathers’ (Author of Working Effectively with Legacy Code) famous “10 Papers Every Developer Should Read (at least twice)”
“We’ve come to value experiential learning much more, and we’ve regained a strong pragmatic focus, but I think it would be a shame if we lost sight of some of the deeper things which people have learned over the past 50 years. Rediscovering them would be painful, and (to me) not knowing them would be a shame.”
“Arguments and Results” is about patterns that can be used to tackle complexity in APIs (or as Noble calls them “protocols of an object“). It primarily focuses on their arguments and return results; hence the title of the paper. The patterns listed in the paper are not exhaustive by any means, but are quite extensively applicable.
Two primary motivations behind these patterns:
To make software simple and concise: “Since smaller, simpler programs are generally easier to read and write, the patterns are concerned with the complexity or size of a design, such as the number of messages an object understands”
To constrain inevitable complexity from spreading out: Over time, complexity in software is inevitable. But we can constrain the complexity to few places instead of letting it leak to other parts.
In this part 1, I’m going to only focus on “arguments” and in part 2 I’ll cover the patterns related to “results”.
We’re going to look at three patterns to tackle complexity with respect to arguments of a function/object:
Argument Objects: Or how to make implicit concepts explicit
Selector Objects: Or how to simplify methods that do similar things.
Curried Objects: How to simplify extremely complicated APIs by currying and partial application.
Arguments Object
In simple terms, this is about extracting common and related arguments into respective value objects.
A function that accepts large number of arguments is more prone to modifications. Primarily because it already does too many things. It doesn't hurt to let it do one more thing, and then some more. Moreover, “adding an eleventh argument to a message with ten arguments is qualitatively quite different to adding a second argument to a unary message”
"If you have a procedure with 10 parameters, you probably missed some"
— Alan Perlis
By lifting and grouping the parameters into respective objects, we make the purpose of a function more explicit. These groupings (abstraction into specific types) make the reasons for change more explicit and meaningful. For instance, when writing a Paint software, it makes sense to talk in terms of coordinates (both X and Y value) together. So it makes API more readable when we extract them into a Point type:
This makes it easier for the client developers to reason with the APIs and eases their cognitive load during implementation.
I found this pattern very similar to DDD’s “make implicit concepts explicit” and using value objects over primitive obsession.
Selector Object
Quite often an API exposes several functions which perform similar underlying operations. For instance, continuing on our Paint software theme, lets say we have a graphical “View” type which exposes various methods to draw different shapes:
“Protocols (APIs) where many messages perform similar functions are often difficult to learn and to use, especially as the similarity is often not obvious from the protocol's documentation. Because the messages are conceptually closely related, they will often need to be maintained as a group, which will require changing a number of method implementations in servers and many different message sends in clients.”
In a typical Object Oriented world, you would use polymorphism to have different `Shape` types and expose a base class `draw()`function which can take different types of shapes and then draw them. These different instances of shapes then behave like a Selector Object to the base draw function.
Noble mentions that in simpler scenarios even just an enum type would also work fine as a Selector Object. In more complex scenarios you could use multimethods or even double dispatch. I also found Composite pattern to be quite similar to Selector Object concept.
One could also use Selector Object as Flyweights .
Curried Object
This is one of my favourite go to techniques, especially when I’m dealing with external libraries and their APIs. Let’s take a typical example of using node’s fetch API to get some data from an HTTP API. Let’s say I am interested in getting all the posts in my blog and comments from a particular post. A typical code would look like below:
There are a plethora of options available while configuring the request. For the sake of this article, I’ve limited these options to passing of a bearer token for authentication purpose and using `Get` method of fetch. Evidently, each client code interested in calling the `getPosts()` and `getComments()` functions would need to pass the token. It needs to be aware of authentication mechanism. Most of the projects, after first authentication with server, would keep this token in some sort of a middleware. With the above code, each client function would also need to be aware of middleware and invoke some sort of get to obtain the token before passing to the getPosts() and getComments() methods. This adds unnecessary coupling and complicates the client code.
The paper mentions how such arguments are “content” and the client needs to cache them (for e.g. knowing about bearer token before it can call getPosts() method) in order to use the APIs:
“These kinds of arguments increase the complexity of a protocol. The protocol will be difficult to learn, as programmers must work out which arguments must be changed, and which must remain constant. This information is not explicitly represented in the protocol, and often not provided by standard documentation. The protocol will be difficult to use, as clients must cache constant arguments between sends and compute the values of slowly-varying arguments.”
This is where currying and partial functions shine. We could very well use currying and expose partial functions to the client code, making the authentication mechanism “invisible” to them:
Conclusion
Quite often, the design can be simplified by “finding” additional objects. Strong emphasis on trying to “find” objects that part of the ubiquitous language. This makes sure we’re not introducing phantom concepts in the system.
These patterns introduce another layer of indirection. While this can simplify understanding of objects/modules in isolation, at times it can interfere with ability to reason with the global design of the system.
Since with all these patterns, additional objects and their construction are getting introduced, at times it can even introduce space and time complexity. Each pattern is to be employed with caution.
Use Factory or other creational patterns when construction of these objects (Argument, Selector etc.) gets complicated. Using a creational pattern can greatly simplify the API for client.