61 comments

[ 1.5 ms ] story [ 136 ms ] thread
Things aren't unconditionally necessary except in modal logic. In the real world things are conditionally necessary. Clean code is necessary... for ongoing development. Clean code is necessary... to allow developers to onboard quickly. Clean code is necessary... for reducing the cognitive overhead and avoiding mistakes.

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.

Clean code is not an OO concept. It's a higher level concept. SOLID may help you achieve clean code in OO, but clean code does not mean "know SOLID".
I agree, but for the bulk of software engineers it's probably on point.

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.

> If you do backend, the user doesn't give a shit about your clean code

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.

Often I find that clean code is something I do for my future self too. Having some external factor break your algorithm and coming back to multiple nested loops and "i,j,k,l,x,y" variable names makes it take way too long to fix.
Oh cringe. Please don't use "l" as a loop index! Don't use it for anything! Make it go away!
Yes it is about the end user. They don't care about your backend code. They want it to work and for it to perform. Your cleaner code means nothing to the user.

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.

And why would the user care about your clean code on the frontend? It's usually minified anyway. Clean code is not for the user, it is for you as a developer.
Yes you are correct, but I'm not talking about clean code on frontend.

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.

I think the author, being a Java guy is using OO and Java as a reference point, not arguing that "this isn't applicable to non OO development". Basically he's saying that remembering the principles and practices is essential in every level of the development.
Encapsulation and modularization is necessary, nothing more.

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.

Really nothing more? I beg to differ after having to plough through lines of code just yesterday where, just to give some examples

- 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

Can all be fixed rather easily, if there ha been good modularization.

But yes, I prefer those things too, haha

But to replace code, first you have to understand it.
Modules have an api and a responsibility, often this +any code helps to understand whats going on good enough
(comment deleted)
Out of curiosity, what is the largest project you've worked on, k__ (as in, number of lines)?
The largest project I worked on was Firefox.

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.

Speaking of Firefox, encapsulation and modularization have a cost, and can be taken way too far.

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/

I agree with this. It reminds of a post I read a little while ago from Cognitect:

> 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.

As with everything, it depends on your requirements, including timeline, shelf-life of application, and future maintenance/feature building.

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.

Depends on the definition of necessary. If you consider living a pleasant life necessary, then the answer is yes.
I think the author's statement of the Liskov substitution principle is backwards; An object must be replaceable by a subtype, not a supertype.
Yes you are totally right. I mistyped a word. Where it says supertype I actually meant subtype. I corrected the article.
TLDR: Yes.
And here I was expecting something that will shake the core beliefs and uproot good ole Uncle Bob; alas, not the case, yet another "Yes, clean code is good, unclean baaad"; I mean, the concept is great and all, but damn, stop preaching to the choir, it will get us nowhere...
Liskov Substitution: An object can be replaced by supertype without breaking the program

Shouldn't that be subtype? Or am I misremembering this?

You're correct.

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

Surely it follows logically anyway?

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?

LSP doesn't specifically deal with inheritance; it's equally applicable to interface implementation.
Yes you are totally right. I mistyped a word. Where it says supertype I actually meant subtype. I corrected the article.
Many developers, myself included, aren't fans of the Liskov Substitution Principle because base classes aren't a great way to reuse code.

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.

That's a very narrow reading of LSP. If you're dealing purely with interfaces, LSP is still just as useful. In that case, if two types share the same interface, it should be possible to substitute objects of one type for those of the other where that interface is expected and not break the program.
In that case, it's better to phrase the guideline in terms of interfaces and implementations to avoid the digressions into ABCs.

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 came out of Liskov's work on CLU (amongst other things). CLU didn't have inheritance, though it did have have 'interface specifications'. LSP is most properly thought of as pertaining to interfaces rather than inheritance.

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.

> ...C++, where inheritance and polymorphism are welded to one another

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.

The problems with subclass inheritance aren't a challenge to LSP. They're an illustration of exactly what LSP is about.

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.

I never thought about interfaces this way. Great comment.
I wasn't challenging the LSP as much as saying it's practically moot, at least for me. To that end, the digression into ABCs and LSP only gives inexperienced developers a digression to explore on the way to cleaner design principles.

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.

If there's any physical representation, then it's not all the way there on the abstraction front. I meant to exclude any definition of storage format with the "that contain no implementation" qualifier, because specifying data members is necessarily specifying how you expect the storage of that information to be implemented.

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 huge issue is that subclassing is different from subtying. LSP is absolutely necessary for subtyping, but subtclassing is only tangentially related.

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.

> Many developers, myself included, aren't fans of the Liskov Substitution Principle

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).

Today a discussion like this make me so jealous I get vertigo... :-)

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.")

At least it's a good excuse to put on a Prince album while you work.
My music of the time was more singer songwriters, balkan gypsy, modern jazz and death/black. :-)
Is Clean Code Possible? Not if you're writing ES5 JavaScript for the browser.
This comment should not be downvoted.

It's a hard realpolitik of web programming.

(comment deleted)
Personally, I've found that heavy application of OO patterns leads to code that is very unclean, in terms of being "simple and direct".

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.

Not convinced it's the application of OO patterns that leads to bad code but bad programmers (at least in the major part).

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.

> Not convinced it's the application of OO patterns that leads to bad code but bad programmers

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.

I tend to agree.

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.

Agree in general.

> 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.

Yes - but my point is, you're 'having to re-write' that function is not an existential problem for the product or company. It's just a shitty piece of work.

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.

This seems like more of a case of over-organization, and abstracting things before it becomes necessary (premature optimization etc...).

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.

Very true.

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.

"Clean code" at this point in time sounds more like a inside joke for uncle bob fans to satisfy themselves with

"Clean code" sounds a lot like "True Scotsman"

SOLID is better but it still is subject to some subject interpretation

Clean code does not matter by itself; comprehensible code does. If you understand what you have you can reason about when it might go bad and avoid it; even if it does fail you can understand why and fix it; you can see what you need to do to modify it in the future. That's what matters.
It only matters if you value the code. Nobody cared if Space Invaders source code was a ball of mud that was hacked together once and never looked at again. It still made millions of dollars.
> Open-Closed: A class should be open for extension, but close to modification

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.