7 ms·
Redundancy vs. dependencies: which is worse? (2008)
- d--b 11y agoThis article is very important. I encourage everyone to read it, as it raises a lot of good points. In my opinion, the problem it describes come from the vagueness of the concept of elegance, and how counterintuitive it is. It doesn't even have to imply modules or anything like that. In the simplest form it boils down to: Would you rather write: if (a) { doSomething(); if (b) { doSomethingElse(); } } or if (a && b) { doSomething(); doSomethingElse(); } else if (a && !b) { doSomething(); } Of course, there is no true answer to that question, and it always depends on the context. But many programmers will never ever consider option 2. And that is for two good reasons: 1, you make one more test, and 2 you duplicate the function call do doSomething(), so your program is larger. So mathematically speaking option 1 is more elegant, it's shorter, it's faster, it's lighter, what's not to like? Well, multiply the ifs and the elses, and you will soon find out that option 2 is much more readable and changeable, which is the more elegant solution to anyone who's an engineer rather than a mathematician. The tension it describes is that in every programmer is a mathematician and an engineer endlessly conflicting. This should be a good guidance for which style to use. Is this code a math code, or is it engineer's code. When you can answer that question, you can decide which style to write your code in.
- pantulis 11y ago"Changeable" is the key here. We coders are trained for abstracting things, but real world requirements sometimes change and your perfectly abstract module needs some non-generic functionality. Of course, there are software engineering techniques to solve these problems, but hey you didn't foresee these changes and now need to bill your client for these adjustments. So the trick is achieving a good balance between redundancy and dependencies. This is probably what seniority means in software development.
- d--b 11y agoCouldn't agree more
- zimpenfish 11y agoOption 2 is less changeable if you now also have to consider e.g. `a && c`. I'd argue it was less readable too (two `doSomething()` to consider vs one is bad) but this is largely personal prejudice - I'd always spell things out "noddy fashion" like Option 1 because I am a simple brained developer.
- d--b 11y agoOf course it depends on the case. But the point is that if you chose Option 1, and you have to change doSomething() in to doYetSomethingElse() in the case where a && !b, then you _will_ have to change the structure to Option 2. So in many cases, it's better to go to Option 2 directly.
- agumonkey 11y agoOne should write a boolean refactorer to be able to test exhaustiveness and different nesting patterns.
- MrPatan 11y agoI completely agree. It's _vital_ to know what a, b, doSomething and doSomethingElse actually stand for, and what is the context. This is not a technical problem, it's a people problem (like almost all problems). Take this, for example: if (userLoggedIn) { setCookie(); if (itsANiceDay) { tellUSerToGoOutAndPlay(); } } And this: if (itsSunny && itsCold) { putOnSunglasses(); putOnCoat(); } else if (itsSunny && !itsCold) { putOnSunglasses(); } They both kind of make sense! Now let's see the other way around: if (userLoggedIn && itsANiceDay) { setCookie(); tellUSerToGoOutAndPlay(); } else if (userLoggedIn && !itsANiceDay) { setCookie(); } This one is just silly. We are mixing logic about two completely different things. Nobody would do that (many people would do that, I know. Too many) if (itsSunny) { putOnSunglasses(); if (itsCold) { putOnCoat(); } } This one seems ok. Until you wonder what happens if it rains and/or it's windy. Then you'll refactor it to a "switch/case" for clarity. And it's all about the domain specific content! You cannot reason only about the syntax tree in isolation and come to a meaningful conclusion!
- jessaustin 11y agoThe problem with these examples is that putOnCoat() should not depend in any way on itsSunny, so there should just be two separate if blocks. We should be discussing a scenario in which one condition is a subset of the other.
- JustSomeNobody 11y agoWhy not? I've been on Rainier when it's cold and sunny.
- Jtsummers 11y agoBut it's an independent test. You'll put on your coat when it's cold, whether it's sunny or not.
- denis1 11y agoI you didn't understand what the GP said. You can certainly have a day when it is both sunny and cold, but the checks would look like this: if(isSunny){ putOnGlasses(); } if(isCold) { putOnCoat(); }
- Dylan16807 11y ago>Well, multiply the ifs and the elses, and you will soon find out that option 2 is much more readable and changeable, which is the more elegant solution to anyone who's an engineer rather than a mathematician. Disagree. In the first case I know only one block (and its parents) can execute. When all the ifs are isolated, it takes much more reading to know that only one will trigger, and that's after I make sure 'doSomething' won't change a or b. The real way to write that is probably a switch statement.
- userbinator 11y agoWell, multiply the ifs and the elses I would consider that doing so makes it a different problem, and so what becomes the better solution changes. If you need very many different combinations, you should probably use a table-based approach instead (produce an integer from each of the booleans, index into a table of function pointers or similar; you can often reduce its size if you put "don't cares" into the more significant bits.) I think the second option is more difficult to understand since you now have to consider together all 4 different combinations of a and b to determine what code gets executed for each one. For the first option, it is immediately obvious that none of those functions execute if a is false, whereas in the second you have to inspect each if-condition.
- Retric 11y agoThey both share a dependency due to nesting or the else statement. A third option may be more maintainable depending on context. if (a) { doSomething(); } if ((a && b)||c) { doSomethingElse(); } Granted, this is a case where refactoring is fairly trivial. PS: The real question is does doSomethingElse depend on doSomething.
- deleted 11y ago[deleted]
- phn 11y agoI don't like repeating function calls, if logic calls for complex conditions (which I don't think the example does) I find it better to express them in new booleans before the if, keeping the logic contained there, and call them on a: bool shouldDoSomethingElse = a && b; if(a) doSomething(); if(shouldDoSomethingElse) doSomethingElse(); kind of thing.
- seanwilson 11y agoI like this technique for avoiding comments in code as the variable name describes what the condition is checking but I don't see it used often for some reason.
- RussianCow 11y agoI like it too, but I assume it's probably not used very often just because it forces you to write more boilerplate code, which some may see as "verbose".
- seanwilson 11y agoOh, I was only commenting on the use of the variable to store and label a condition. For example, I prefer: var insideEventTime = time > start && time < end; if (insideEventTime) ... To this: // When inside event time if (time > start && time < end) ... I find comments frequently become inaccurate compared to function and variable names.
- RussianCow 11y agoI understand what you're saying, but I can see certain devs having a problem with introducing extra variables only to use them in a single conditional. Not that I agree with that, but that's the only reason I can come up with.
- seanwilson 11y agoWhat's wrong with introducing an extra variable compared to introducing a comment?
- overgard 11y agoI generally find that if statements with multiple tests are a frequent source of bugs; because it's easy for someone to add another term to one test without realizing they need to update the other. (Especially if the function is long. Yeah you should write short functions, but long functions happen.) Your second example is much harder to reason about if someone comes in and adds: if ( (a && b) || c) So then what happens with the second clause? (Or what even should happen with the second clause?) On a meta note, any time I see multiple tests in an if statement, I try to pull it into local booleans that describe the intent instead. At the very least then if you add more conditions to the if statement, you have to update the names of the boolean for it to make sense, and so the code stays implicitly commented.
- iamcurious 11y agoNote that your examples are equivalent only if neither doSomething nor a modify b.
- rsp1984 11y agoThe article makes some very important points and it's certainly worth a read for every programmer. What the article misses to address explicitly however is that the whole redundancy vs. dependency conflict is caused by modularization. Without modules there would be no conflict. So the real questions to answer here are: When do you need modules or do you need them at all? What should be modularized? And, most importantly, how to choose smart boundaries? Good answers to these questions will save a project from a world of pain down the road. The classic OOP / software engineering education these days lacks critical debate about software modularization. Modularization is almost always presented as a good thing. What nobody tells you however is that in real world engineering, on real world teams, modularization can cause a lot of trouble if not done the right way.
- agumonkey 11y agoI remember a language project without the concept of function. An massive inlining system to avoid the issue of modularization.
- mdpopescu 11y agoWithout modules you just repeat the same dilemma at the level of classes or even functions.
- loup-vaillant 11y agoCode is basically a dependency graph. Each piece of code depends on a number of other pieces of code. (Dead code is an isolated island in this dependency graph.) You want two things out of that graph: less nodes, because less code is simpler, better, cheaper. And less edges, because understanding, modifying, or troubleshooting a piece of code requires knowing about its dependencies (hopefully, only the direct ones). When the unit of organisation is the function, you kinda state that each function is a node, and the call graph are part of the edges (the call graph would cover everything in a purely functional settings, but side effect produces implicit dependencies). Trouble is, in any significant system, you're gonna have a lot of nodes and edges. How to make sense of that? That's why we have module. When you look at your dependency graph, you will most certainly note that parts of your graph are denser than others. Those clusters are the natural modules. If you formalise that, and draw module boundaries around those clusters, you can now have a two-level view: inside a module, you have a small dependency graph, with a few outbound edges. Outside, you can visualise a coarser graph of module dependencies. Again, fewer nodes and edges, because you have grouped them. Now the real benefits of module is, once you start drawing boundaries, you have an incentive to make small interfaces, to minimise inter-module dependencies. Additionally, visualising the module dependency graph directly helps you spot spooky dependencies that probably shouldn't be there. You can then cut some dependencies out, simplifying your graph in the process. Without modules, I don't see how you would manage this kind of scale. Oh and by the way, some monstrosities are so big that they effectively requires a third level. But I've never worked on such beasts.
- tempodox 11y agoA very good article that aptly shows (some of) the hard & sticky questions we are confronted with all the time. How you answer these questions will determine the quality & stability of your code to a large extent. I agree with the OP that commonly, “dependencies are worse”. Redundancy will increase the quantity of your code, but dependencies increase its complexity. And quantity is always conquered easier than complexity.
- michaelfeathers 11y agoIt's interesting to read this with micro-services in mind.
- tel 11y agoIt's kind of funny how, rightfully, the author paints a picture where the "horrible, enlightened external dependency" itself is antimodular to the T. Given that all modules supposedly have stable interfaces, documentation, tests, reasonable size, yada yada then one might expect that each of their dependencies takes advantage of these properties to maintain light and wonderful themselves. Of course, this is a situation that's highly incompatible with C. Let's ditch that. In ML modules are king. You probably make hundreds in any non-trivial program and the compiler will beat your ass if you muck up their interfaces. Anywhere. Packages are just sets of 3 public modules wrapped up in twine and a README file (coincidentally this is where "ownership and lifecycle" are managed, but, sorry, I'm going to ignore those for a moment). This could be every bit as bad as I described before, but ML also realized that modules which just form a big dependency tree are actually quite annoying. The whole reason we define public APIs is so that there can be multiple satisficing inplementors, but this cannot be in 99% of module technologies today. So ML has functors (not Haskell functors, certainly certainly certainly not C++ functors) which are "parameterized modules that actually work". One could distribute their command line parsing module with a pluggable serialization and a pluggable help display. See MirageOS for a giant example of this kind of system working out. Does it really work? Probably not. It's not in most maintainers DNA to functor-ize everything. It's even a significant challenge to do so since you need to define sufficient external and internal public APIs and it's a significant community effort to standardize these sufficiently so that there is significant chance of re-use. But at least it's a way forward. Fight the heavy module trees. Let's use some higher order reusability.
- bunderbunder 11y agoI've had some success with the object-oriented equivalent of the pattern. Perversely, I find it to be most effective as a political tool. It's useful when someone doesn't like my minimalist solution to some problem, and starts peppering me with feature requests that will complicate the module and which I perceive to be of marginal utility. So I make that chunk of functionality pluggable, keep my minimalist implementation as the default, and publish some instructions for how to drop in a more complicated behavior. Then all I have to do is sit back and watch the original requester realize that they only think the stuff they were asking for is worth the effort if they can get someone else to be the one putting out the effort.
- guard-of-terra 11y agoThis depends greatly on your platform. Java projects accept dependencies much easier than C++ ones because in Java it's much harder to cause trouble and also coding styles aren't radically different for different dependencies. Perl&Ruby are even more eager, which should be strange since they're actually less safe.
- Sirenos 11y agoLess safe? How so?
- rwallace 11y agoExcellent article. Just one quibble: he claims a module shouldn't be over 30k lines. Counterexamples: Linux, Postgres, Boost, LLVM, V8, all in the million line range. To be sure, each of these has an internal module structure, but that's irrelevant from the perspective of someone deciding whether to incur a dependency on one of them - the answer to which may very well be yes because they do enough to make it worthwhile. If anything, larger modules like the ones I listed are more likely to be worth depending on because they do more. It's no coincidence that the author chooses command line parsing as a negative example - something trivial enough that the overhead of tracking a dependency may well outweigh the effort of implementing it yourself.
- sbov 11y agoI generally agree with this. However, sometimes using a module is not adding a dependency, it is making an already existing implicit dependency explicit. E.g. we have client and server code. Serialization configuration between the two is implicitly dependent upon each other - if the client expects dates in a different format than the server, things don't work. To make that implicit dependency explicit, we use a module, which also has the affect of making sure the two don't get out of synch.