4 ms·
Most of those Clean code rules are BS. 1. Prefer polymorphism to “if/else” and “switch” - if anything, that makes code less readable, as it hides the dispatch
by pkolaczk 4y ago
Most of those Clean code rules are BS.
1. Prefer polymorphism to “if/else” and “switch” - if anything, that makes code less readable, as it hides the dispatch targets. Switch/if is much more direct and explicit. And traditional OOP polymorphism like in C++ or Java makes the code extensible in one particular dimension (types) at the expense of making it non-extensible in another dimension (operations), so there is no net win or loss in that area as well. It is just a different tool, but not better/worse.
2. Code should not know about the internals of objects it’s working with – again, that depends. Hiding the internals behind an interface is good if the complexity of the interface is way lower than the complexity of the internals. In that case the abstraction reduces the cognitive load, because you don't have to learn the internal implementation. However, the total complexity of the system modelled like that is larger, and if you introduce too many indirection levels in too many places, or if the complexity of the interfaces/abstractions is not much smaller than the complexity they hide, then the project soon becomes an overengineered mess like FizzBuzz Enterprise.
3. Functions should be small – that's quite subjective, and also depends on the complexity of the functions. A flat (not nested) function can be large without causing issues. Also going another extreme is not good either – thousands of one-liners can be also extremely hard to read.
4. Functions should do one thing – "one thing" is not well defined; and functions have fractal nature - they appear do more things the more closely you inspect them. This rule can be used to justify splitting any function.
5. “DRY” - Don’t Repeat Yourself – this one is pretty good, as long as one doesn't do DRY by just matching accidentally similar code (e.g. in tests).
- smcl 4y agoI think if someone takes any of these typical guidelines - clean, SOLID, REST etc - and mindlessly applies it they’re likely to end up with a few parts of their applications which look weird, or perform poorly or end up being worse somehow. This is because there are inevitably going to be situations where the guidelines don’t fit well - they’re not necessarily hard and fast rules after all. Any time you have these common rules of thumb in any part of your life you need to evaluate whether or not they are appropriate. But just because they’re not infallibly universal, doesn’t mean they’re wrong, it just means life throws complex situations at us sometimes, and that we need to be pragmatic and flexible
- sebstefan 4y agoIn his small example he already added unforeseen couplings that could get out of hands if it was a big codebase. If you follow the principle "switch statements over [X]", try to add a new shape down the line and see how quickly you run into problems. In the clean code version, your compiler will remind you to implement calculateArea, calculateNumberOfVertices, calculateWhatever, and so on and so forth. With his version you have to add a new `case` to every switch statement and hope you didn't miss one with a default case, because the compiler won't catch it. You don't need virtual functions and polymorphism to remove his switch statements. Just compose over inheritance.
- pkolaczk 4y ago> With his version you have to add a new `case` to every switch statement and hope you didn't miss one with a default case, because the compiler won't catch it. The compiler not catching it is a limitation of the language he uses, not the limitation of the general concept of switch / pattern matching. Scala, Haskell, Rust do catch those. > If you follow the principle "switch statements over [X]", try to add a new shape down the line and see how quickly you run into problems. And who said you'd ever need to add a new shape? Maybe you will need to add a new operation? Try to add a new operation `calculateWhatever` and see how many places of the code you need to chnage instead of just adding one new function with a switch. Often you really don't know in which direction the code will evolve. Most often you can't guess the coming change, so don't make the code more complex now in order to make it simpler in the future (which may never come).
- sebstefan 4y ago>The compiler not catching it is a limitation of the language he uses, not the limitation of the general concept of switch / pattern matching. Scala, Haskell, Rust do catch those. You're thinking of defaultless switch statements. Rust and Typescript catch those issues as long as you don't add a default. I'd already thought of it when I typed it >And who said you'd ever need to add a new shape? ô_o >Maybe you will need to add a new operation? Try to add a new operation `calculateWhatever` and see how many places of the code you need to chnage instead of just adding one new function with a switch. You do realize that your "one function with a switch" does have to cover every case exactly like the clean code version, it's not magic you're just fitting it all into one switch/case, and you'll probably end up extracting those case: blocks into separate functions anyway. And on top of that you're not safe from a colleague adding a useless "default" case at the end ouf ot paranoia and making your compiler not catch a future problem when a new shape gets added.
- kps 4y ago> Prefer polymorphism to “if/else” and “switch” - if anything, that makes code less readable, as it hides the dispatch targets. Worse, I think, is that it hides the condition, which can be arbitrarily far away in space and time. Certainly dynamic dispatch can be very useful, and the abstraction can be clearer than the alternatives. A rule of thumb is to consider whether you'd do it if you were writing in C, using explicit tables of function pointers. If that would be clearer than conditional statements, do it. (In case it isn't obvious, I'm talking about languages in the C++/Java vein here.)