Passing around complex objects is the opposite of encapsulation
I see this a lot:
class Foo: spam = None eggs = None def frob(foo): return sprocket(str(foo.eggs)) f = Foo() s = frob(f)
It tends to be more sinister, and difficult to see, in verbose examples. But generally it is easily identified by the called method using a single attribute or
method from the object passed in (or multiple in longer functions that should be split up ;) ). Sometimes I bring this up and say, “pass in the value directly,” and the ‘why’ clicks right away. Sometimes people (including my older self) say “but taking in a ‘foo’ encapsulates my method!”
I guess. It certainly hides the detail that `frob` needs only `.eggs` and doesn’t also need `.spam`. But you’ve also coupled the implementation of `frob` to the interface of `Foo`. So you’ve achieved encapsulation by greatly increasing coupling.
Of the two, I’d vastly prefer a method that must take additional parameters if its implementation changes (ie, if it needs access to `.spam`), than increase coupling. High coupling leads to brittle, untestable, and non-reusable code. Changing the interface of a method leads to… what exactly?
Not only that but the contract of a method is much clearer (to both callers and maintainers) if it takes in meaningful parameters, rather than a single object which it accesses a bunch of properties of. It conveys more information for callers, and establishes what it is supposed to do to maintainers (who will not be able to just get or set the attribute of an object that happened to be passed into that method because it was a convenient place to do so).
So it is usually vastly preferable to take in the values the function uses, rather than pass around complex objects, and in fact this is a common design paradigm in functional programming. But obviously I’m not just using strings and ints everywhere. So what guidelines do I follow?
- Immutable objects are fine to pass around (though prefer the advice about just listing what the function takes as per above).
- Mutable objects should never be passed around, as I consider creating an object and passing it to a method that mutates it one of the greatest sins in OOP.
Not many reasons for this kind of crime that I can think of. In a statically typed language I suppose it’s easy to allow exceptions to the rule because you desire a function of type Foo -> Bar, although equally you could happily implement this as a pair of functions to extract and then manipulate whatever subset of Foo’s data you’re interested in, currying and composing towards some elegant solution.
I’m more interested in where you draw the line, though. At some point, wrapping up the function arguments into a single structure affords better properties than simply passing in an awful lot of arguments piecemeal. Lots of arguments are horrible: potentially messing up the order, involving an awful lot of boilerplate for named args, or just unpacking the Foo into named arguments (why aren’t you passing in the whole thing, at this point?). Yes, you should probably split the function up if it requires that much *stuff*, but something still needs to unpack the stuff and dispatch it to your small functions. Does the function orchestrating these take a Foo?
To my mind, this becomes a bit of a wobbly issue. It’s not so much a case of how many members frob needs to access, but whether the function notionally operates on Foos or a collection of subFooian values. Maybe the structure of Foo itself carries information. And this kind of argument extends to methods… to abuse your example, Foo.frob doesn’t need to be on Foo, and if it has any useful behaviour in a universe absent Foos it probably shouldn’t, but in many cases such offending functions are actually methods.
Part of the problem is that OO tends to leave us with the idea that we should only be passing objects around and using methods, when frequently operating on simple structures with independent functions is the Right Thing in my opinion.
Bundling up arguments into an object can be a great thing. The question for me is always- is the object mutable? If the object isn’t mutable, I don’t really care if you pass it around instead of a few arguments. If it is mutable, I’d rarely take it as an argument to a function. Who knows what the function will do when it is passed a mutable instance? As I think I said in the article, I only pass mutable instances into functions (in general) with internal/inline methods/functions.
In that I quite agree. In general, I’m of the opinion that _everything_ should be immutable by default. 80% purity will buy you quite a lot of sanity, which can then be spent on the 20% remaining horrible stuff.