DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Flexible code considered harmful
Coding Unicorn πŸ¦„
Coding Unicorn πŸ¦„

Posted on • Updated on

Flexible code considered harmful

Alt Text

🧠 The biggest mistake programmers make is writing flexible and abstract code. Some of us believe that writing flexible and abstract code helps the system evolve fast. We write interfaces, abstract classes, frameworks, and platforms, assuming that they help us fulfill future requirements faster.
β €
Open-Closed Principle suggests that we should be able to extend the behavior of a system without having to modify that system. It's the most dangerous and widely misunderstood programming principle I am aware of.

πŸ”₯ In theory, it's a good idea. But there is a caveat. All those extension points introduce extra complexity. Complexity makes the system harder to understand and harder to charge. What's worse, our abstractions are usually wrong, because often we design them up-front, before the actual flexibility is needed. According to Sandi Metz:

Duplication is better than the wrong abstraction.

There is a paradox in software design titled "Use-Reuse Paradox":

What's easy to use is hard to reuse. What's easy to reuse is hard to use.

Flexible and abstract and flexible code is hard to use and also hard to understand. It slows us down. Keep in mind that speed is achieved by writing simple and direct code with as few abstractions as possible.
β €
πŸ’‘ Resist the temptation to write flexible code. Write dumb and straightforward code by default. Add flexibility only when necessary.

Agree/disagree?


dev.events

Top comments (169)

Collapse
 
docbill profile image
Bill C Riemers • Edited on

You have a false premise. Sometimes making code extensible increases complexity, sometimes it reduces complexity.

It comes down to design. For example if you ask me to write a calendar to display the holidays in 2019, I could hard could images for each month with all the holidays in the code. My code would be none extensible, and suddenly become incredibly complex as soon as you ask for the most minor modification,, such as to display different holidays per country.

Or I could write really good code for displaying a single month and a list of holidays for that month. I then just pass in parameters for each month of 2019. It turns out my implementation of the original requirements is simpler and extending it for new requirements does not overly complicate things.

So you premise making extensible code is always more complicated is simply false. The truth is some things will make the code overly complicated, some will actually make it simpler.

Collapse
 
rosseyn profile image
Rosseyn

I took it as a caution against early optimization. Unless you're building something that explicitly calls for reuse, spending time on accounting for unknown future requirements can actually cause more work when you have to undo it later.

There was a point made about dealing with extensibility once requirements are known, if you have to use the same calendar in two places, you already have those requirements.

Collapse
 
davealexis profile image
David Alexis

With experience comes the intuition to know when abstraction is needed and at what level, without first having that 3rd or 4th requirement that will inevitably come.
I worked with so many people who insisted on not "future-proofing" their code, only to end up eating everyone's time to completely rewrite their horrible, single-use code or hack it horribly to make it work for more than 1 scenario.
Anyone blindly spouting YAGNI is a mere programmer in my book, not an engineer.

Thread Thread
 
mschleckser profile image
MSchleckser

That comes down to the intent of the code. With experience come habits that lead to easily extensible design that can be readily modified to meet new requirements. But to that end, at what point are you over engineering your code? Should systems be written to be infinitely modifiable to meet any requirement, or should you write rapid code that meets the needs of the now and can be easily replaced with a more extensible design? Personally I fall into the second camp. When you write a POC or the first lines of a new system, code quality, while important, takes a back seat. That's not too say you shouldn't write quality code and use lessons learned, but when you are spending hours trying to design a highly extensible and robust system when you could have banged it out in thirty minutes, you are wasting time over optimizing for what ifs.

It boils down to a singular postulate, just make it work then refactor.

Thread Thread
 
rosseyn profile image
Rosseyn

Experience gives you insight on what not to do, and that can be invaluable in simply not writing yourself into a corner for future use, you don't even need to guess at what the future holds if you aren't locking yourself into early rigidity.

Again, if the requirements aren't to write a reusable module, you probably shouldn't be doing it quite yet. If you think you should be, you likely need to renegotiate the requirements to include it.

Thread Thread
 
davealexis profile image
David Alexis • Edited on

A POC is throw away code, and should never be used as the basis of a real product by modifying it to add features. It's like the paper and wood model that an architect builds. The real building includes numerous engineering and design consideration that are not in the POC model.

It's the same with good software. Quality and consideration for potential future use cases must be baked in. Why? Software tends to live much longer than expected and be used in unexpected ways, and you're probably not going to be around (or remember what you did and why you did it) when users come back asking for enhancements. Every single codebase I've ever come across where the devs only coded for current requirements without considering the effect of time has been an absolute mess to deal with, and impossible the alter for new/changed requirements.

Thread Thread
 
lifelongthinker profile image
Sebastian

I would give this a thousand thumbs up if I could.

Unfortunately, I have seen too many POCs ending up in production, too many devs saying "we will never have to touch this again" -- and boom! its back on MY desk and I have to deal with the mess. Nothing lasts longer than a provisional solution.

So yes, I rather err on the side of SOLID and all these principles. If once in a while I over-engineer code slightly because it is actually never touched (extended, fixed) again, then so be it. The vast majority of cases go the other way.

Collapse
 
spaceninja151 profile image
Christian Knoll

A hard-coded calendar isn’t very extensible but is the easiest to understand. All the months are written right there instead of importing objects and classes with month name data saved elsewhere. There is always complexity in abstraction. I took her point to mean that if you start extending before you completely understand what you will need your program to do, you have added needless complexity, which is self-defeating at best.

Collapse
 
oleglukia profile image
Oleg Lukianchikov

A too straightforward approach would almost always be too verbose hence hard to read.

When it comes to over-abstracting, I guess the devil is in the details. Some level of future-proofing is almost always needed. The clients do not expect that adding a feature after initial development can take so much time (when your first version is very rigid).

Collapse
 
3boysdad profile image
3boysdad

100% agree, this article sounds more like the butt-hurt of Junior programmer who thought they were all that and found out they really weren't. Clearly there's a lack of exposure to enterprise development than real experience shown here.

Collapse
 
jmcooper profile image
jmcooper

I've been a software engineer for more than 25 years and have consistently found the worst code to work in to be code that has been over-engineered, over-abstracted or unnecessarily complicated. If @codingunicorn is a Junior (which I doubt from her insights) then she has already learned an important lesson that it can take years to learn. The point isn't to never abstract it is to not abstract too early.

Collapse
 
docbill profile image
Bill C Riemers

It is the absolute type statement of youth. It takes decades of experience for most developers to gain enough experience when they should simply go for the simplest solution they can think of, and when they should give deep thought as to how the code can be extensible, and if it should be...

The bits of extensible code that always seems to work out I'd when you notice you are coding the same thing multiple times, and decide to instead just create a util or helper method. The types that are really a bad idea is when you are doing a quick pov, and spend lots of time solving problems for a 5% use cases and a framework for the next part of the project. As the point of a pic is to try things and learn and then later design based on what you learned. Much the same way a mechanical engineer designs a prototype to learn how it will fail.

Most software projects are somewhere between. There are bits that should flexible and bits you just want ant anything that works.

Collapse
 
millimoose profile image
David Vallner

That's because it's written by a marketer's fake persona; I would bet money the pic is a stock photo / model. Just look at the whole-ass blog.

Thread Thread
 
lifelongthinker profile image
Sebastian

Would be an awesome stunt, though πŸ˜‚

Collapse
 
acadianrow profile image
acadianrow

Firstly embedding data is a quick and dirty fix which immediately limits useful lifetime of code. Also, if you are tasked with building a 2019 calendar, you will be asked to produce a 2020 update. It would be so much easier and more elegant to just replace a control/data file than rewrite the sucker. If the "black box" is well designed and documented, there is no reason to ever open it! For instance i wrote a UPS, FEDEX, etc billing parser/accounting info extractor 25 years ago that is still running strong. It has been recompiled for new platforms but never reengineered. The control file is tweaked every so often to keep up with input and ourput changes.

Collapse
 
programaths profile image
Christian Baune

I have a simple tactics that keeps things really stupid!

It's a cousin of dependancy injection.

In your calendar example, when I need to get specific holidays, I would call a non existing method like: getMonthHolidays(1)

Then I would ponder from where this method has to come from. Is it a private method OR a first order method ?

That means that I am always writing some stupid code at different level of a abstraction.

This also leads to easy to understand, maintain and extend code.

I had a CS curriculum and that wasn't even taught as a quick win. That's sad!

Collapse
 
dsdorazio profile image
Dan

This, exactly.

Collapse
 
fredsteffen profile image
fredsteffen

I think you'd still use standard oo principals to code the calendar. You just might forgo adding interfaces to every utility, creating a factory to determine which type of calendar (Gregorian, Hessidic, Klingon, etc), create an adapter to wrap your ui components, a service bus to notify for click messages, etc.

Collapse
 
lukemcar profile image
Luke Carpenter • Edited on

Hello. I'm a solutions architect with 20 years in the industry. If designed correctly abstraction is very useful and powerful. With that being said, I have found that most developers don't understand abstraction. That 80% of developers are unable to think abstractly and if they try to design/code abstractly they'll achieve what you outlined in your post. Final point: abstraction is not bad it just shouldn't be done by developers that don't know how to do it.

Collapse
 
robertvasile profile image
Robert VASILE

good point. one should not take "code writers" for developers or developers for software engineers. while abstraction works for everything, it's not actually for everyone. even more, abstract design becomes a pain when done by code writers or developers lacking real live implementation experience

Collapse
 
fredsteffen profile image
fredsteffen

Abstraction and indirection are different. Abstractions simplify. Indirection adds complexity. Most of the indirection I'm seeing is useless. Always 1 implementation in the factory. Why have the factory? Well, it's good design. We "could" switch out the ORM with anything now! Most of the time it's not needed and even when it is, actually making the switch still requires changes to many other parts of the code. Delaying optimization, in my experience, has usually been a much cleaner approach. When you implement the needed indirection, you know the whole problem you need to solve.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
lukemcar profile image
Luke Carpenter

I think you are reading things into my comment that are not there. You may want to reread it.

Thread Thread
 
chadsteele profile image
Chad Steele

Really? I think you were very clear. "I have found that most developers don't understand abstraction. That 80% of developers are unable to think abstractly and if they try to design/code abstractly they'll achieve what you outlined in your post."

In other words, you're smarter than 80% of developers.

Consider that our job is to make the user and subsequent deverlopers feel smart. They should be able to understand our UI and our code without having to be experts at anything.

Thread Thread
 
lukemcar profile image
Luke Carpenter

First off you are reading things into my comment. Never did I propose that I am smarter then 80%.

Your assumption that all abstractions are hard to follow is wrong. If designed and implemented correctly they are easy to follow and use which was my main point. If this was not the case we would not see so many frameworks in our industry.

Thread Thread
 
chadsteele profile image
Chad Steele

I'm sorry you're taking this personally. Your primary point seemed silly and arrogant to me. Don't defend it. Edit it.

And yes, I agree that abstractions can be useful, but mostly they're abstract. Hence the word. I've been programming professionally for about 40 years and have mentored lots and lots of developers. I don't think abstractions, in general, make for better software. Or even less software. Programmers who love them tend to rubber stamp them all over the place causing them self and others to write lots of extra code around the abstractions to make them useful.

Human language is already OO - tactile, tangible nouns and verbs. If you listen to the business, not the tech, you'll hear a highly refined already refactored business model with users, stories, nouns and verbs already well defined. Any code we write that doesn't reflect that model is going to be unintuitive and is ultimately added complexity. Our job is to solve business problems, not create them...aka add as little complexity as possible.

Thread Thread
 
chadsteele profile image
Chad Steele

About... "If this was not the case we would not see so many frameworks in our industry" is a function of creativity, not evidence of a cultural commitment to simplicity. Yes, most engineers preach about simplicity, but don't actually produce it. We have so many frameworks because software people are creative and love to produce "solutions" for imaginary problems, not because they're committed to less code.

For example, what makes a good musician? A good musician is someone with a large capacity for music. When we have a capacity for something we tend to have more of it. We enjoy it. And we often create it. Technical people have a capacity for complexity. We enjoy it. And we often can't help but create it. And so, we have lots of frameworks that go in and out of vogue every year and will for the foreseeable future. This is evidence of our creativity and capacity for complexity, not of a commitment to simplicity.

Collapse
 
alwynschoeman profile image
Alwyn Schoeman

Totally agree.

There are developers who think things are complex when they are just incapable of thinking beyond the lowest level of coding.

Collapse
 
jwp profile image
John Peters

To me, the right answer is simply use the right style of programming where warranted. Saying the Open/Closed principle is problematic discounts 25 years of proving it a wonderful principal where needed.

Collapse
 
bfloydd profile image
bfloydd

Exactly. Any principle not fully understood or without greater context will be wrong some amount of time. And so the next generation comes, misunderstands, makes some mistakes, adds some bits, then arrives back at the same conclusion. The age-old cycle.

Collapse
 
jwp profile image
John Peters

So true, take Angular or React. We use it everyday but few of us know the Internals. None of us knew it at all until we spent substantial time learning it. Any good reusable code takes time to learn.

Thread Thread
 
tracker1 profile image
Michael J. Ryan

Ironically, that's exactly a major reason I prefer React, it's easier to understand as it isn't as prescriptive. There are very simple look alikes that are easy. React itself is a bit harder, but not really harder than just the rxjs library angular is using by itself.

Thread Thread
 
jwp profile image
John Peters

Agreed, Rxjs takes a long time to grasp, but is worth the pain because asynchronous push notifications are almost alwys better than pull constructs, even asynchronous pull requests.

Thread Thread
 
tracker1 profile image
Michael J. Ryan

I prefer redux/flux or graphql myself.

Collapse
 
lambage profile image
lambage

Open/closed is often difficult to grasp. I like to sum it up as its backwards compatible and not limited for new features. However sometimes no amount of planning will ever allow you to know what future features you'll need, and this is why you need to follow Robert C Martin's rule of "classes should be small, the second rule is they should be smaller than that"

Collapse
 
jwp profile image
John Peters • Edited on

C# made open/closed constructs simple. Just use a static method using the this keyword for first param. Javascript and typescript both support the protoype extension method.

The power of doing this is all good. Some naysayers claim that finding the source of these methods is a problem. To that argument, following covention over configuration is all that's needed. The convention of putting all extension methods in a unique namespace is all we need. Intellisense and go to definition takes us to the source.

Collapse
 
itlackey profile image
IT Lackey

Totally agree! This is an easy one for us devs to trip over. I still find myself doing "speculative programming" after years of being a professional dev.
Two major things I've noticed when this happens.

  1. The requirements/concepts of the feature are usually not complete or fully understood.
  2. The work that is done is usually redone because my assumptions were incorrect as you had mentioned in your article.

This is certainly a tricky aspect of development and takes real thought and discipline to avoid "gold plating" code that will look beautiful but never be used. This code is equivalent to writing features that were never requested.

To avoid doing this I personally try to balance the desire to be the overlying flexible system with the YAGNI principle

Collapse
 
codingunicorn profile image
Coding Unicorn πŸ¦„

Exactly! YAGNI!

Collapse
 
nickytonline profile image
Nick Taylor
Thread Thread
 
codingunicorn profile image
Coding Unicorn πŸ¦„

Awesome :D

Collapse
 
rgsingh profile image
Rai Singh

I agree. The balance is key. Systems are complex and designing for future-proofing often requires a qualified definition of what that means for the projects in scope and the constraints, technical and business, in play. I often ask myself how I can manage the technical debt of something I design rather than if there will be any debt at all.

Collapse
 
safijari profile image
Jariullah Safi

I like to always start simple when it's a new problem and coalesce similar functionality as time passes. This idea really only works when you're willing to spend time refactoring the same code over and over (which I don't see as a waste of time when done correctly).
That said when an abstraction is obvious I absolutely begin with it.

Collapse
 
waveitgoodbye profile image
WaveItGoodBye

I have a co worker that is a big fan of the rule of three. It's a good general rule of thumb to follow and I find it helps summize the same point I think you made here.

Collapse
 
seanmclem profile image
Seanmclem

It's about finding a sweet spot, and not pursuing extremes

Collapse
 
warichter1 profile image
warichter1

I’ve spent way too much time trying to manage others poorly written inflexible code. A minor change to requirements, say a change to a service account breaks everything. Taking hours or days to fix. Usually end up rewriting, and simplifying poor design, by improving flexibility.

Bad code design is ok for inexperienced developers. Hopefully these developers move beyond this. If not, they call developers like myself to rewrite code properly. I’ve spent decades mentoring inexperienced developers on avoiding bad habits and developing good design.

Can do the project quick and cheap or do the job right. Never a lack of work building and documenting flexible code.

Collapse
 
rafi993 profile image
Rafi

Problem is people prematurely optimize and premature optimization is the root of all evil. When you optimize prematurely you create leaky abstraction which will require you dive into the internals. At that point you have lost the advantage of creating that abstraction.

Collapse
 
rafi993 profile image
Rafi • Edited on

Really good abstractions holds strong for most use cases. Remember last time you had to deal with quirks of document.createElement in the browser.

Collapse
 
fjones profile image
FJones

This is something I have been noticing a lot particularly referencing interfaces: A tendency to write an interface "because we want to implement against an interface".

My answer as a software architect is always the same: Until you need it, the best interface is the implementation you already wrote. Particularly with fast-changing applications, you'll quickly find that you cannot - and must not - anticipate use cases for next month. You'll almost always have to redesign either way, and it is much easier to do when you know - rather than guess - the new requirements.

Collapse
 
lambage profile image
lambage

Depending on your language you may need to always develop against interfaces so you can use DI to unit test your code.

Collapse
 
nickytonline profile image
Nick Taylor • Edited on

I definitely agree that it's hard to get the right abstraction.

Here's a great talk from Sebastian Markbage from the React team

The cover for the video says it all "No abstraction > wrong abstraction"

And here's the great blog post about the wrong abstraction by Sandi Metz which you referred to.

Collapse
 
sinapis profile image
sinapis

I find this assertion misleading.
It's also true that no code is better than wrong code. It's always better to do nothing than to do evil, the same way that zero is always larger than any negative number. But where does it gets us? Zero is not enough. We need to do good, positive work. We need good abstractions that allow our code to be flexible. Having inflexible code that has enormous cost to every tiny change is useless. We live in fast moving and dynamic world. The only constant is change.

Collapse
 
nickytonline profile image
Nick Taylor

For sure we need to write code and ship things and iterate over them. The main premise is, finding the right abstraction is difficult or can be difficult, so don't just create them because it makes things DRY.

Thread Thread
 
sinapis profile image
sinapis

I agree. DRY is a very good principle, but it should not be followed blindly. Sometimes, it's not really DRY, it looks the same, but it has two complete different purposes, and if we leave it alone, the two so called "duplicates" will evolve on two completely different paths, and change for completely different reasons. Sometimes, the cost of creating a dependency to share the code outweighs the benefit of reusing the code. usually I follow the WET principle instead: Write Everything Two times, but not three.

Collapse
 
codingunicorn profile image
Coding Unicorn πŸ¦„

Thanks for the video!

Collapse
 
gtanyware profile image
Graham Trott

Complexity makes the system harder to understand and harder to charge. What's worse, our abstractions are usually wrong, because often we design them up-front, before the actual flexibility is needed.

This goes way beyond programming; it's a problem in engineering, architecture, law and politics too. Systems are created by dedicated, committed people with a clear vision of what is needed, but over time maintenance and further development are delegated to others without the same commitment or skills. The result is always the same; a steady degradation of the system. We eventually reach the point where not even the original builder is able to rectify the mistakes made; the complexity has become too great. Every participant in the project leaves their own flavor of change, with no explanation of where, how or why it differs from the original master plan.

We can argue forever about method and about which currently-fashionable magic bullet will save the day but the root cause remains, as do its effects. It's quite simple; a system will always break down once its level of complexity exceeds that at which it can be understood by those tasked with its maintenance. This is not a problem of software; it's a problem of being human.

If there is a solution - and I'm not saying there is - it's to design for the lowest common denominator. Or to accept that systems will always need to be rebuilt from the ground up, way before they reach their intended lifespan.

Collapse
 
ty1824 profile image
Tyler Hodgkins

I can't tell you how many times I've listened to arguments like this from people too inexperienced to see how their code will evolve. It's gotten to the point where I view YAGNI as a bad word. Even though I completely agree with the concept in principle, it is often overapplied by conservative developers.

Sure, for new and inexperienced developers, overgeneralization is risky. They don't have a full grasp on the patterns and concepts that they need to properly design simple and flexible code

However, once a developer had some modicum of skill, domain expertise and business understanding, they should always be expected to develop flexible, scalable code.

Occasionally, experienced developers will find themselves on newer, research-oriented projects. This path may necessitate writing less-flexible code - but the purpose here is not to go to production. It is to learn and gain insight into new technologies and patterns.

If we all held ourselves to the standards described in this article, we'd have no shortage of jobs, but we'd also have no shortage of technical issues with the software we write (and use).

Always strive to write the most clear and flexible code you can, unless you are attempting to learn some new technologies or prototype within time constraints. Your teammates and successors will thank you.

Collapse
 
sinapis profile image
sinapis

Your assumption that flexible code must be complex is wrong. Good abstractions are easier to grasp than zero abstractions sphagetti code.
It is true that finding the right abstraction is hard. The solution is not giving up on abstractions, the solution is learning how to abstract correctly.
At some point, changing inflexible code is so hard that the project grounds to a halt and all the devs cry to throw the code base to the trash and start everything anew. The only way to keep large codebases alive is making them flexible.
That's why there is 'soft' in software.

Collapse
 
forkbomb profile image
Joe Buckle

Haha it's so true. When you write code you're always thinking "how could I make this more generic so I could potentially re-use it" but in reality you're just evolving an ever-more complex code-base (guilty!).

Collapse
 
mattkeyes profile image
Matt Keyes

This just more or less sounds like a problem for novice programmers. The proliferation of languages and technologies has not changed the fact that knowing how to write syntax and knowing how to program are two separate skills.

Advocating the limitation or elimination of reusable code sounds like the battle cry of the under-experienced.

(this is not meant to sound harsh or judgmental - just an observation)

Collapse
 
gdubz profile image
gdubz

Refactor early and often but not before necessary. An abstraction can wrap a concrete implementation after a similar but unique use case develops. That's the time to add the complexity. Leveraging internal tools, documentation, and style standards as well as familiarizing common abstraction patterns will help fill in the complexity gaps.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.

Want to rep DEV and be comfy at the same time?

Check out our classic DEV shirt β€” available in multiple colors.