Situation:
Is it better to have one or few parameters that supply more than enough information for the intended process or should you have more specific parameters that provide just enough information?
When making parameters more specific, if the process of making them more specific means including business rules/logic or heavy coupling with a very specific class is required should it still be done?
Example:
In the case of an Interface:
public interface IQuestion
{
// Specificity in Interface parameters increases Coupling right?
void MyFunction(IPrincipal user, string destination);
// Is this any better? It allows me to be more flexible in what I'm doing
// Inside these functions but it also makes them even more coupled right?
void MyFunction(ControllerContext context);
}
Question:
Are there general rules or cases where it’s specifically better to do it one way over the other?
Is it better to have one or few parameters that supply more than enough information for the intended process
“More than enough” is the red flag here.
When paying in a store, you don’t give your entire wallet so that the cashier takes cash out of it. The cashier doesn’t really need to know what your wallet is like, or look at pictures of your kids, and a customer might have no wallet whatsoever. Programs should work in a similar manner.
By passing bundled values that contain (or could contain) more information that necessary you are breaking encapsulation (called functions have access to data that’s not of their business), plus it obfuscates the design because there’s no clear-cut contract. It’s not obvious what the called code actually needs and what it doesn’t need just by looking at it, you have to look up its implementation to find out. You don’t know what exactly is expected from your ControllerContext
to be of any use for MyFunction
. It may blow up in runtime.
Of course if User
and destination
come together, then they simply belong in one class and there’s no point in splitting them everywhere. But there’s that “more than enough” notion that indicates this is not the case…
1
It’s a balance.
In general, having specific interfaces is better than general interfaces. Functions should not know more than they need to know to work.
But also, in general, it isn’t good to have piles of interfaces that are only ever used in one place.
So you need to balance making specific interfaces for your needs and using existing interfaces even though they don’t quite fit. Thankfully, if you’re doing things right, this comes up infrequently. If your objects are simple and have a single responsibility, then it’s uncommon for functions to only need part of that object.
Is it better to have one or few parameters that supply more than
enough information for the intended process or should you have more
specific parameters that provide just enough information?
I wanted to kind of echo Telastyn
here and suggest there’s a balancing act to be had, but focus more on the interface design perspective. In general I’d suggest leaning heavily towards the former whenever you can (with “whenever you can” being based on the confidence in the stability of your designs with respect to how much information they need to receive).
As a caveat, I’ve worked in frantic environments where, within a couple of months after you release a design to the team, already thousands of lines of code will be using it throughout the codebase, and written by multiple authors. The design is effectively cemented in stone at that stage, and to change it in response to a change in user-end requirements would cause unacceptable cascading changes which would make the rest of the team want to run you over by a coordinated bus. Yet, simultaneously, we were working with clients who always changed their minds and would brainstorm new design ideas in the middle of the process. Some of what I focus on might be overkill outside of environments where interface design so quickly becomes the most unforgiving process.
I’ll try to share my mess of thoughts here.
Flexible Parameters
Instead of focusing on coupling so much here, I want to put a spotlight on just the conceptual amount of information we pass through an interface, and also return from an interface.
Do we pass just enough information for a function to do its thing? That would be absolutely ideal, provided you know exactly what the right amount of information is. Do we pass way too much? That could end up making the interface quite difficult to test, possibly couple it to some unstable monolithic type, and potentially obscure side effects.
To me just enough would be absolutely ideal, but there are occasionally times where the message passing involved between interfaces is actually the most difficult part of the interface to stabilize. What the interface should do at the broad level might be obvious and unchanging, exactly how much information it needs to do it might actually change over time.
Flexible Parameters: Regular Expressions
An example I want to use to focus more on “information” than data types is regular expressions. As a note, I hate regular expressions. The syntax gives me a headache, and I always have to look things up there. Nevertheless, it’s worth noting the stability of regex interface designs, like so:
// Search for just about anything you can imagine in a string.
string regex_search(string str, string pattern);
It’s actually quite incredible to me that even though we’ve had all kinds of regex standards competing with each other from simple, basic, extended, that this above design can remain stable even if it continues to expand to support new regex standards.
In this case, string pattern
is actually modeling a super bundle of information, capable of communicating a practically-infinite amount of information through a variable-sized string. It is largely because of this super bundle of information that this regex_search
function, and the callers of the function, can remain stable (unchanging) in spite of evolving and expanding regex standards.
Where this is a bit different is that pattern
would be used in full by regex_search
, no matter how much information it contains. The entire contents of the string would be relevant for the search, and that’s owed due to the ease and flexibility of a string
interface to merely add as much information as you need at the site in which the call is made. A string can contain infinite information, but the nature of its interface and representation makes it so it doesn’t have to contain more information than necessary.
Nevertheless, this quality is something I’ve rarely seen mentioned, of flexible parameter types, and it’s a noteworthy quality to interface designers seeking greater stability in their designs.
Imagine, instead, if string pattern
above was actually IRegEx pattern
, with IRegEx
modeling SRE (now deprecated), with functions rather than strings to modeling each individual element of a pattern. Such an interface would likely find difficulties remaining stable in the face of new standards, having to decide between outright trying to evolve and grow (possibly becoming a monolithic design with a lot of warts), or new interfaces and possibly deprecation (and therefore a new regex_search_ex
kind of function in addition).
This is not a suggestion to reduce the richness of our designs to ones that accept things like strings all over the place. Nevertheless, the stability that results here is a very interesting quality to note. Interface design is always a tightrope balancing act, weighing the pros and cons of one decision over another. It becomes easier if we start to become more aware of the possibilities, and the precise pros and cons of each.
Stability
Typically we don’t think so much about interface-level coupling to plain old data types or standard types like string
or int
. The above regex_search
function might be described as “decoupled”, completely independent. Yet an obscure way to look at it is that it is still coupled to the interface design of a string
. The reason we can generally omit such discussions, and in our dependency diagrams, is simply because string
is so incredibly stable. It’s unlikely to ever change in design for as long as the language is around, as such a change could potentially break almost all codebases ever written against the language.
As a result, we can often reach for a string
parameter here, an int
there as parameter types, without worrying about string
or int
changing and breaking the implementations of both caller and callee.
However, when you reach for a user-defined type like ControllerContext
, then stability starts to become a concern. Will ControllerContext's
design likely change? If the design changes, would it simply grow and therefore not affect existing dependencies to it, or might there be temptations to change some existing parts of its public interface change in ways that might lead to cascading breakages? It’s all a balancing act.
On the flip side, if you use this design:
void MyFunction(IPrincipal user, string destination);
… will you have sufficient information always? Is the design for the existing parts used of IPrincipal
going to remain stable?
It’s difficult to answer these questions perfectly, and thus design is difficult. It’s also not worth asking so much if such designs are not widely-used, in which case we might waste more time trying to too hard to come up with the right design choice upfront when it would have been cheaper to just make a mistake and change the design later without much of a cascade in changes.
Yet a big goal here is stability, always. We don’t want to design interfaces that have to change, we don’t want to accept parameter types that eventually offer too little information in a way that needs to expand and forces us to change these designs.
Side Effects
One thing to consider is side effects. Are there side effects (changes made) to context
here?
void MyFunction(ControllerContext context);
… or will MyFunction
simply read from context
? If there are side effects involved, these kinds of bundled types tend to be a lot worse, since they don’t simply provide more information than the function needs, but they also supply more state than the function will actually modify.
As a result, there can be incredible confusion when passing such bundles as to exactly what the function has modified inside the bundle, and bundled information of this sort can quickly carry far more cons if it is not read-only.
Yet the same kind of concerns carry to user
here:
void MyFunction(IPrincipal user, string destination);
Obscure side effects are a very common spawning point for bugs, so we generally want to make side effects clear, and strive to design functions that only cause, at most, one logical side effect. That might affect how you name your functions, document them, and possibly even the parameter types being passed. It’s also worth striving for immutability when possible.
If you ever have bundled states that could be split into a read-only part and a mutable part, for example, the split may help a whole lot instead of just mixing “input and output” parameters in one bundle.
Testing
Bundles can make testing more difficult, depending on how easy they are to construct on the fly. Strings are pretty easy to construct on the fly to contain just the right amount of information.
ControllerContext
may not be so easy, and its interface might want to expose non-homogeneous data in a way that contains far more functionality than needed just at the interface level.
The ease at which you can construct such bundles on the fly with the necessary information will affect the ease at which you can test your interfaces against a wide range of relevant parameters. An example of where a bundled design might really complicate testing is if it can only be constructed from a config file.
Degeneralizing Bundles
One of the ways to achieve some sense of stability in your design but not veer too far into the danger of passing way more information than needed, and all the cons associated, is to degeneralize your bundles and make them site-specific.
For example, instead of ControllerContext
used for IQuestion
, maybe you should use QuestionContext
which is only used by IQuestion
. That mitigates the temptation to bundle way more information than needed.
This is often useful if you want to transfer potential instability elsewhere, if QuestionContext
ends up being easier to change than IQuestion
. An example is if you need to extend or change the underlying data representation of parameters passed to IQuestion
, but not in a way that affects all code written thus far that uses it, only the implementation of QuestionContext
itself. In general, a lot of interface design boils down to that. We want to transfer potential instability to places that are easier to change.
ABI and TMI
This might be slightly outside the context of this question, but when ABI (application binary interface) is a concern, too much information (TMI) can be an essential mechanism to reduce the odds of backwards compatibility breakages.
In these cases, interface stability becomes more important than ever before, because an interface change may not simply break compatibility with existing source code, but with existing binaries for products (ex: third party commercial plugins) outside of your team’s control.
In such cases, a function like this:
void (*some_function)(int x, int y);
… may very well benefit from having some extra information available to input and output that it doesn’t currently benefit from having:
int (*some_function)(int x, int y, void* unused);
It’s really ugly but both the return type and unused
may save our butts some day in preventing a backwards compatibility ABI change that would have otherwise been necessary, by having shaped all existing client code using some_function
to pass in a null for unused
which may become a valid, but still optional parameter.
A similar idea might carry for struct
used in C, if it’s not opaque to the client. Having some unused fields might save the day, some day, and prevent a need for deprecation or breakages. Likewise, some fields formerly used may evolve to become unused.
This kind of practice is most useful when future expansions are towards optional routes, to allow breathing room for new options (options, as in optional) from breaking existing interfaces, widely-used.
Conclusion
So anyway, these are some things to take into consideration when trying to figure out exactly how much information you need when designing the parameters and return types of an interface. The ideal design is always going to be one that just receives and returns exactly the right amount of information required. Yet a less ideal solution which accepts or returns more than required might leave some additional breathing room for changes. Stability is one of the greatest goals, and achieving stability in the face of a world of changing requirements can be very difficult unless we transfer instability elsewhere to places that are easier to change.