Is Clean Code Necessary?
jeanrenesite.wordpress.com
jeanrenesite.wordpress.com
First developers apply the single-responsibility principle, and break down previously large but comprehendible classes in thousands of tiny components. While each component is trivial on its own, the truth of how they combine to produce a particular application feature is now hidden within an implicit object graph that cannot be understood by new developers, except through a torturous process of cross-checking and debugging until they have built a fragile mental model of what's going on.
Next developers get embarrassed by all the new-ing up of components they're doing, so they introduce dependency injection. But rather than just injecting those things that have a genuine need for varying implementations, suddenly everything gets to be a service. Abstractions proliferate, factory factories appear, everything is behind an interface (with a single "default" implementation), and navigating the codebase becomes a nightmare.
I've seen clean code bases in OO/functional and pure procedural and bad code bases in those as well.
I think it's tempting to blame bad code on things like bad languages and bad architectural patterns because they are externalities and can be blamed without mentioning the gorilla in the room, bad programmers write bad code.
I'm not clever so I don't write 'clever' code, I actually find it hard to work on code bases where they tried to be clever or do too much in one function/class.
Architecture is like salt, a little enhances, a lot ruins.
I don't write simple clear code out of any sense of purity or attempt to be 'clean' but because I know that in six months when I open this file it'll be me fixing it and I won't understand what the hell $me-6mths was thinking.
There's one thing I would point at. When non OO code is bad you can usually point at the problem: bugs, performance, security issues, coupling, so on and so forth.
OO code can be bad yet contain no debuggable issues. Once it's laid out, noone is going to "fix it" because it's not technically broken. It'll just rot due to lack of maintenance, until there's noone left with a good mental map of it and enough of the non-OO contents are broken enough that it gets replaced all at once.
There are ways to implement OO where you only add complexity when it reduces the size of the code base significantly, and things like that. Like, leave your monolith class in place, but then maybe move some boilerplate code into it's own class/function once you've copy-pasted it 2 or 3 times. If you find yourself using a dictionary and doing the same, somewhat-peculiar manipulation on the data structure more than once, maybe create a dictionary subclass. Wait until you see the obvious "bad code patterns" emerging and then use OO to fix them, don't just go ham and start arbitrarily "applying principles" until you get to Factory<factory> world.
Too many classes can create obfuscation and confusion.
I generally don't mind some overlapping function so long as my instinct tells me it makes sense.
I used to make classes for everything. Often ahead of time.
Now I make classes generally only as needed.
Also - the title is a little misleading.
At the OO level, this is not really even 'code' - it's 'design'.
'Code', I often think of as what's written, i.e. inside the functions etc..
As far as that goes, 'clean' is really important when it's exposed.
Anything that is really tightly encapsulated within a function ... well, can be a little grimy so long as it's not so bad. Point being, it's uglyless cannot spread beyond the function.
But some things - especially API's - it's really important that they make sense, because they represent the language of the application.
I'll take clean, consistent API's with messy 'code' than crap API's with tight code any day. You can always fix the code. APIs ... create dependency problems, and bad ones spread problems.
> Anything that is really tightly encapsulated within a function ... well, can be a little grimy so long as it's not so bad. Point being, it's uglyless cannot spread beyond the function.
Of course, if you're in the position of having to debug that function, you might curse its griminess.
I tend to only write really gnarly functions if I'm trying to heavily optimize them and it is only by virtue of those optimizations that they become grimy. And even then, I tend to do it only for those functions that can easily and quickly be rewritten entirely. (And I also try to write them one optimization at a time so I can always fall back to an older, less optimized one if a bug is introduced.) And, of course, there are always comments to help light the way and warn of dragons.
Any individually poorly written but well encapsulated piece of shit is just a 'bathroom clean' away from being nice.
Contrast that with bad design, bad APIs, or other things: they mess up other things. They can even create massive, ugly dependencies, and massively increase complexity, bugs etc. etc..
I've seen build-chains that involved 6 different scripting languages. Because we all had to use it, and almost nobody knew all 6 languages, it f*ed the whole company and caused huge pain.
There's some law of software engineering...can't remember the name.
It basically says that complexity can only be pushed around, not destroyed.
If you make every class simple, the composition of classes will be complex.
Unfortunately without stating these assumptions it's impossible to prioritize and be rational about development. It provides no guidance. Should we drop everything we're doing and "clean up" the code? Is it even worth investing in? These are questions that have different answers depending on the assumptions and simply saying it's "necessary" is insufficient.
For many of us, it's nothing new, but I do think that it's remarkably easy to just rip code toward a deadline unconsciously without keeping these concepts in mind.
If you do backend, the user doesn't give a shit about your clean code, so sometimes it's a trade-off in deciding your approach knowing the lifetime requirements and maintability needs of a project which only comes from experience.
It's not about the user. Coding in a 'clean' way is something you do for your colleagues and maybe future colleagues, who will have to maintain and upgrade your software in the future.
> so sometimes it's a trade-off in deciding your approach knowing the lifetime requirements and maintability
I have seen quite some hacks that were intended as 'one-time-only' and 'we-refactor-this-later' solutions. I don't think any of these trade-offs payed out in the end. The proper work to refactor crappy code and reintegrate it into the system took a whole lot of time longer than implementing it correctly in the first place.
The only situation where hacks like this are acceptable in my opinion are 'one-time-only' prototypes (where the prototype code does not find its way back into the codebase), where certain features are shown to a customer, and the rest are mockups.
Sometimes you have to put more time into frontend and you're up against a tight deadline. I'd opt for the UX in most circumstances.
You're assuming that there are colleagues to work on it and that it will live indefinitely.
Many projects, esp. those related to marketing, have a definite lifetime.
You also mention prototypes as another use-case.
Cleaner code as directed by the article is not always necessary.
The user doesn't care about any code. If it works, it works.
I understand clean code is for the developer(s). That's precisely my point. You don't always need to go through a SOLID checklist with your project.
Sometimes you have deadlines, sometimes you're the only dev and you know the project will die post-launch. So if it works; you're good.
I'm not sure why this part of my original comment is confusing or controversial:
"sometimes it's a trade-off in deciding your approach knowing the lifetime requirements and maintability needs of a project which only comes from experience"
A purist push for design patterns and paradigms for every project is not based in the real-world.
Shouldn't that be subtype? Or am I misremembering this?
Prefer composition over inheritance for reuse. And prefer interfaces over base classes (abstract or concrete) for polymorphic behavior.
There aren't many situations where Liskov is even relevant with those principles in mind. I have to inherit from a concrete type from time to time when when a third-party API requires it. And, inevitably, my design becomes more convoluted than necessary because of that.
In order to make it easier to follow LSP, it's wise to minimize the amount of concrete implementation in your base types. The more you can minimize it, the less likely you are to encounter a situation where a base type implements behavior you don't want. That in turn reduces the spots where you might even want to violate LSP.
The logical end to this is purely abstract base classes which contain no implementation. These types are so easy to reuse that some languages have even enshrined them as their own special language construct (typically called "interfaces"), and given them special privileges that types which allow implementation don't get to enjoy, such as multiple inheritance. The reason they get these special privileges is that it's relatively easy for them to enjoy those features without violating LSP, whereas it's inherently tricky for types that include concrete implementation to do the same.
It's also worth noting that ABCs are especially problematic when physical representation of data is concerned. Inheritance in C++ affects memory layout, for instance. And adding new serializeable members to a base class in any language is a reverse-incompatible change.
As a contrasting example, Java and .NET interfaces aren't allowed to contain fields, only method and property signatures. Which really means only method signatures, since Java and .NET properties are just methods + syntactic sugar. Both languages also have ABCs, but they are distinct language constructs that don't get to enjoy the same privileges as interfaces.
That may mean that C++ abstract base classes don't quite hit it on the LSP front. It's also true that C++ is a language that predates LSP's formulation by over a decade, so we can't really expect it to be a language that's designed to take LSP into account, anyway.
Long story short, though, just because LSP is baked into your programming language doesn't mean it's moot. It just means you're working in a language that's well-designed on that front.
The interfaces that complicate my designs were written by well-meaning engineers that thought ABCs are best practice if the LSP is followed. That's not the case anymore and I'd rather we start teaching from a more sensible default.
LSP as most people seem to currently conceive it is coloured by their experience with languages like C++, where inheritance and polymorphism are welded to one another. Same goes for the likes of Java, albeit to a lesser extent. In the case of languages like that, LSP also needs to deal with the subclass/superclass relationship, and that has over time overshadowed its most important aspect: substitutability of implementations.
Technically there many ways to have polymorphism in C++ without inheritance and virtual methods (closures, templates, tagged unions, etc.). In practice, students learn about polymorphism from OOP textbooks and conflate polymorphism and inheritance.
The work on 'concepts' for (maybe) C++20 fits more nicely with subtyping and LSP, it sounds like.
In practice, this doesn't quite hold though since the principle method by which subtyping is introduced is subclassing. This unification is the source of many woes.
To see an example where this doesn't hold you can take a look at OCaml. It has an object system which separates subtyping and subclassing and helps you to take note of what subclassing is actually about: reusing creation code. It ends up being very similar to normal function re-use at the end of the day and is very nice.
It's just not subtyping.
The way this is worded, it sounds like you would encourage violation of the principle. From the rest of the thread, it seems like you just encourage work where it does not apply. I think a lot of respondents are reacting to the former - while the latter is much less controversial (although note tel's commentary on distinction between type and class).
From Wikipedia[0]:
> Liskov's notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g. correctness).
0: https://en.wikipedia.org/wiki/Liskov_substitution_principle
Assuming sub/super are referring to inheritance (sorry, I'm a bad), a sub contains all of a super and more. Passing through an object with more shouldn't break anything. But a super might lack things the sub has, thereby breaking assumptions about what will be present.
I feel like the principle would be broken by the objects/inheritance being badly used rather than the calling code though?
Yesterday evening I spent two hours just to trace and understand the dispatch for a really simple and small part of a big, old web application.
(To save time, the answer to the question you want to ask is: "Yes, glad you asked. It really is from 1999. And no, they don't know the word refactoring.")
If you get this right, you can get away with a bunch of crappy thing, because you can easily throw them out in the future and reimplement them better.
- each single line uses a different formatting style
- variable names are hard to read abbreviations
- variable/class names don't make sense
- 5 is a level of indentation easily reached
- there's almost as much dead code as live code
- comments are grammatical nonsense and don't explain why the code is written the way it is
But yes, I prefer those things too, haha
> When you’re working with high leverage, you don’t have to put as much investment in to get the same functionality or value out. Because your code is simpler and easier to create, it can be considered disposable. And with disposable code, you don’t feel the compulsion to keep it, like you would with the old paradigm boat anchor projects. It all adds up to this: data orientation drives antifragility, enabling you to change individual vectors, and your entire organization more swiftly.
http://blog.cognitect.com/blog/2016/6/28/the-new-normal-data...
The implication is that when your code becomes less important the data model becomes more important in response.
But I think my statement is idependent from project size. I've seen it in big and small projects alike. As long as you get this fundamental thing right, you can get away with more than you think.
Did you know that "decomification" [1] is considered by the Mozilla project to be a "good thing"? And it doesn't mean fighting communism.
But like anything (just like COM), decomification can also be taken too far, and can lead to decomificationphobia [2]:
"Also in this way every class could be deComed leaving no risk of overdeComification. But since I don’t quite know the justification or use case for xpcom in mozilla, I find this decomification quite scary."
This fashionable de-over-modularization technique is also fondly known as "deCOMtamination" [2]:
"In Gecko, XPCOM or a primordial form of it was overused inside Gecko, where there are (or should be) no DLL boundaries, for private APIs that are not usable by other, independently evolving code. The overhead here in code space and runtime is significant (we will quantify it as we go), and deCOMtamination consists of getting rid of this bogus COM-like glue."
Mozilla has even developed automated deCOMtamination tools [3]:
"This is an update on the ongoing deCOMtamination work from the automated rewriting perspective. I think it’s pretty exciting that Mozilla is the first large-scale C++ project to attempt automated large scale source code cleanups and optimizations. I think the tools are finally getting mature enough for the job."
"The downside is that there isn’t a published roadmap of what we are planning to achieve with deCOMtamination as we are still in the planning stages. The upside is that there is still time for any interested parties to think up the next great improvement on how things are done, checkout the refactoring toolchain and either extend an existing tool or implement a new one."
[1] https://www.google.com/search?q=decomification&oq=decomifica...
[2] https://blog.mozilla.org/tglek/2007/01/03/squashed-and-compi...
[3] https://blog.mozilla.org/tglek/category/decomtamination/
"Clean code" sounds a lot like "True Scotsman"
SOLID is better but it still is subject to some subject interpretation
Sometimes, code is a one-off and it is better to write something that works rather than worrying about the perfect implementation.
One should strive for clean code when one can, but all things have tradeoffs that have to be decided for/against.
I hate when people do this. Closing class for modification is very easy, but opening it for extension requires predicting the future, which is impossible. Which coupled with being closed to modification destroys re-usability.
It's a hard realpolitik of web programming.