loading...

Clean, DRY, SOLID Spaghetti

Jason C. McDonald on July 24, 2018

Look at you, with your ready-to-ship project! You're quite proud of your style and standards conformance, and your strict adherence to DRY and SOLI... [Read Full]
markdown guide
 

Great post, Jason.

I can't think of a programming principle, technique, or "rule" that is free. Everything has a cost.

Sometimes the benefits exceed the costs, sometimes they don't. In my experience, great programmers are good at doing that math, but bad programmers don't even realize that there is math to be done.

 

I was reading a lot about these but I was able to grasp the idea on what to do and not to do, only after working on a single codebase as a sole developer over a year and then someone joins the team.

First, after a year you eventually start maintaining the essentials of the codebase of a less experienced and also a dumber version of yourself, and noone can help you out of this self-torture.

Then you start avoiding future self-tortures.

Second, you write more for your new teammate as you keep facing what was obvious was actually not that obvious, while facing the truth that they actually met your less experienced and also dumber version, not who you are today.

Then you start writing as if they sit next to you while coding (and include your chat as comments).

 

IMHO

Sometimes is better get the best parts of all worlds based on your experience, your team, your judgment.... not be a FANATIC.

The real world isn't in the books, blogs or podcasts, but their have a good tips(guides, etc) from another experienced devs where in their time they had the same problems or doubts than you, good information for you that will help how to clear up doubts and solve the current problem.

When you get more experienced you would note the problems of follow a rule or maybe you would say that the rule is really good ! But the wise words from @bosepchuk are very real:

I can't think of a programming principle, technique, or "rule" that is free. Everything has a cost.

I don't hate any rule, technique or principle, is good information and why hate that ? Differents points of view and opinions are so good !

I'm trying to get more experience and train my judgment to do the correct math.

Thanks for sharing this!

Sorry if my opinion have errors, my english sometimes is so bad.

 

Absolutely! There are so many amazing principles, techniques, and methodologies out there that we can learn from, but we must use them to, as you said, "train [your] judgment".

I'm currently studying Effective Project Management by Robert K. Wysocki, and (unsurprisingly), he makes much the same point in there about project management.

Projects are unique, and each one is different from all others that have preceded it. That uniqueness requires a unique approach that continually adapts as new characteristics of the project emerge.

 

Of course !

I think this cover many subjects and areas, not all obviously, for example:

  • Repairman -> Need to see the correct problem to apply the correct solution, if in the past he had the same trouble he could try to do the same, but sometimes the problem are not exactly equal, so.....

  • Lawyer -> He needs make the correct defense to him client, based explictly in client's case and another relevant information, so...

  • Doctor -> After read a lots of books (in university), he still need the correct evaluation from their pacient, so...

But all of this examples have another important thing in common: Everything has a cost.

I don't know if I strayed from the subject but this is my analogy.

Hey, it's a valid point. I've met doctors and lawyers who applied principles without doing any of their own observation or evaluation, and the results were always disastrous.

 

Interesting read. I haven't had to maintain a codebase for years and years, so much of what I'll say here is just the theory as I understand it, without having experienced it in practice, especially regarding the long-term impact of following a specific rule.

Also, a lot of the discussion here could probably be clarified with examples, but discussing the effects of applying rules to a large code base makes this difficult because it's impractical to share entire files of code + the folder structure.

I've seen code bases where, to find out what one single-goal function did, I had to jump to no less than two dozen other functions and macros, across multiple files. To make matters worse, I wasn't even sure which of the hundreds of linked files contained the next piece of the call stack. Everything had been abstracted out to the nth degree, making the code base entirely unreadable.

A function's name should tell yo what it does without reading the code. The whole idea of an abstraction is that you don't have to care about the details of how it does what it does.

But I sympathise with you on not knowing which file contains what. I wish more programming languages had, like python, a namespace for each file, that hooks into the way you import/include files, so yo can just know which file a dependency is in from the import path.

I also agree that a little repetition can aid readability, and I'm hoping to get a better understanding of when that's the case.


  • Open-closed Principle: Every additional feature added to the code base came in the form of extension, and always via inheritance. Yes, always inheritance.

There are a lot of posts out there on why composition is often better than inheritance. I think you can apply the open/closed principle without overusing inheritance and creating deep inheritance trees.

 

I have examples from a larger code base.

Here is an example where I had to chase down some code in ASP.NET Core.

You can see all the different places I had to look, and in the end I cannot be 100% certain because of various indirections through interfaces, inheritance, and registration patterns.

Similarly, I once went on safari across ASP.NET Core Kestrel and MVC code bases looking for precisely what AuthorizeAttribute actually checked from the HTTP request. And I could never find it. I found the code that I thought was ultimately called, but I could never find the chain of evidence linking them. Give it a try and see what you come up with. :)

I have a lot of respect for the accomplishments of the .NET Core teams, but their style of coding -- which appears to adhere to good practices in general -- makes it extremely difficult to trace through unless you already worked on it and know where things are. So anyway, it's an example for this discussion.

 

I think you make a lot of great points...largely because you applied common sense and a larger understanding to each of the standards, and that's exactly my point. DRY, SOLID, TDD, and all the rest can be excellent guides. We just have to apply our own judgment. :) (But then, I'm just reiterating - you know all that!)

And yes, composition is almost always better than inheritance. SOLID doesn't state that explicitly, of course, but that's not SOLID's fault. It's one of several guidelines to apply, not a magic bullet!

By the way, that terrible code base was written in Python, although I wouldn't say it was "Pythonic" by any means.

 

"Pythonic" not always mean good readable code, most of the time is apply the syntactic sugar and other advantages that Python offers you to do something, for example List Comprehensions are so good but not always:

This:

def generatorFunction() :
    for number in range(100):
        yield number

Is better than:

generatorFunction = lambda: [(yied(number)) for number in range(100)]

Is only an example, I'm not good to create good examples.

No, "Pythonic" means it applies the "Zen of Python", which puts a high priority on good, readable code.

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

But once again, even this must be applied with common sense!

I'm always have had different concepts of "Zen of Python" and "Pythonic way", but you are right, I'm agree with you. Thanks for the aclaration!

 

Excellent article! I'm definitely saving this.

IMHO applying SOLID (or "even only" DRY) to a code base too strictly almost always leads to better maintainability, but worse readability. It's much easier to change things, but it's hard to understand code when everything is abstracted. The decision to abstract or not to abstract is a complex tradeoff between readability and maintainability.

TL;DR the whole article: Don't blindly follow principles and use your brain. Find a balance between maintainability and readability.

 

Very true.

Ironically, poor readability damages practical maintainability; you can't maintain what you can't read. Meanwhile, less-maintainable code doesn't tend to be readable. So, while they're at odds with one another in one sense, they're paradoxically related in another sense.

 

There is a way to deal with poor readability. It is known as Commenting Showing Intent (CSI): standards.mousepawmedia.com/csi.html

Touché. :)

Of course, it doens't preclude good coding practice.

 

This was a breath of fresh air. I see too many developers that try to be SOLID to the extreme. I feel like the quote "Your job is not to write code, its to solve problems" is particularly important here. Yes, you need to solve problems, with your code. The code you write serves two fold: it solves the problem, and it makes sure that the next developer will be able to maintain the code you wrote that keeps the problem as solved. I cringe a bit every time I read that phrase.

 

That's an interesting angle, @luispcosta ! Problems don't necessary stay solved.

 

The purpose of tests is not to detect bugs. The purpose is to verify correct behaviour. You write tests first to define expected behaviour, then write the production code and after that you're free to do as much refactoring (as in improving design without changing functionality) as you will. Refactoring is an essential part of the process and cannot be done (most of the time) safely without tests.

 

Before one test, one must study requirements, plan a solution then break that plan into autonomous parts. Once the parts have been identified along with the reqs they satisfy, then the logic can be programmed. Then tests can be written to test that logic and also the comms interface between subsystems.

 

Except one major type of bug is literally "incorrect behavior". ;)

In other words,

Testing shows the presence, not the absence of bugs.

Just got that from an article which literally just came through my feed: Stop lying to yourself when testing.

That said, overall I'd agree with you. Test, build, test, refactor, rinse, repeat.

 

Yes, i expressed myself poorly here. I could say that purpose of writing tests in TDD is to define the expected behaviour. And of course the purpose of the is to verify that the system is working correctly or at least as defined.

[The] purpose of writing tests in TDD is to define the expected behaviour.

...and...

Refactoring is an essential part of the process and cannot be done (most of the time) safely without tests.

I probably didn't say, but that was beautifully concise. You really should write an article about the goal of TDD. Many developers seem to lose sight of it a lot. ;)

 

You've essentially written down my mantra: make sure it's maintainable!

I'm glad you wrote this post, so that I don't need to do it anymore - and you've done it better than I could, so win-win all around :-). Kudos.

 

Great write.

I don't quite get your point at TDD. Isn't writing your tests before the code already 'blind'?

 

In one sense, but tests are vastly simpler than production code in most cases, so its easier for us to get into the habit of subconsciously coding around our own "traps". As it is, in TDD, we're explicitly writing to satisfy the tests.

That's not necessarily a bad thing, of course - it's one of TDD's strengths - but rewriting the tests "blind" after writing the code makes it harder for us to avoid setting off bugs.

To put it another way, our initial tests define how the code is "supposed" to work. However, afterwards, when I don't remember exactly how my code is "supposed" to work, I instead write tests based on how I expect it to work...and then it doesn't work that way. I've more closely replicated real-world usage. I've actually done this many times, and it's uncovered quite a few bugs, memory leaks, and poor design decisions.

 

Thanks for the explanation.

In my experience, sometimes I do have code design prethoughts while writing unit tests. There's a danger that I purposefully write the tests the way it was because I want to shape it to the design I had in mind. Thus, writing the "trap" for myself.

I agree, though I think this problem will only likely surface on low level tests such as unit tests. That's why we couple Behevior Driven Development (BDD) automated tests with unit tests. To test the implementation of the unit tests and replicate the transactions as if how it will be used in production.

Normally, we'd write the acceptance tests (BDD) first, unit tests, then code.

 

So much misplaced blame here I don't even know where to begin.

Every problem you list stemming from SOLID principles is a misinterpretation. SRP is about cohesion. OCP doesn't dictate inheritance and the second you find yourself struggling with inheritance you need to wake up and realize you're going about the problem wrong (I can't believe I had to type that). Data-driven models aren't advocated by SOLID, and it was definitely a poor design decision.

In both SOLID and testing cases, you're using poor execution on the developers' part to somehow claim that the concept is bad.

This is like me saying all fast-food chains should go away because McDonald's doesn't taste very good.

 

...and I quote...

Was SOLID at fault for that? Absolutely not! It just goes to show, SOLID isn't some sort of magic bullet for maintainability. Its principles have to be applied with common sense and attentive discernment.

...and...

There are a lot of infinitely helpful principles and standards that help us write good code [...] You will always need to be actively invested in the process of crafting good code.

(P.S. To be fair, I just edited the post to add two words to the final paragraphs to help clarify my point even further.)

 

In my honest opinion he's not blaming anything. He says it's not SOLID fault for it. I feel all he is saying is that you can follow all these "rules" and still end up with a horrible code base. He's not saying or blaming them as the root cause for a horrible code base.

I believe he's saying you should never code blind. Don't just code to follow something. Know why you're writing something. Don't take everything as rules, take them as guidelines, but still think about what you're coding.

 

ALL OF THIS. Yes!

You did not mention it, but Dogma, and Cargo Cultism comes to mind.

 

Thank you. As a junior developer and hype follower I love your article.

code of conduct - report abuse