Ask HN: How do you write code so it's easy to change?

69 points by mtm7 ↗ HN
I work as a web developer in the startup industry. I've heard many times that code should be extensible – that it should be easy for engineers to make changes to the system in the future, especially as a product finds market fit. What tips would you give other engineers on writing easy-to-change code?

77 comments

[ 2.4 ms ] story [ 111 ms ] thread
Keep it short and succinct.
Don't repeat yourself.

There is an objective way to tell if design A is better than design B, and that is to measure how hard it is to make changes. You have to build that into your culture and not optimize for something else, or do things because somebody thinks that is the way to do it.

A big problem in current architectures is that what seems to be a small change (say, add a "cell phone" field next do a "home phone" and "office phone") field often requires a number of little changes in widely separated code:

* a database schema change * changes to database queries * changes to a back-end API * changes to a front-end Javascript app * changes to an iOS mobile app * changes to an Android mobile app

Conventional system design means that what looks like one change to management is actually at least six changes, which might affect that many source code repositories, etc.

An ideal design would let you create a feature by writing all of your code in one place. Of course this would require a big change in how applications are structured and built, but it could lead to a durable advantage. See

https://en.wikipedia.org/wiki/Software_product_line https://en.wikipedia.org/wiki/Feature-oriented_programming https://en.wikipedia.org/wiki/Low-code_development_platform

The biggest barrier to the above I think is that when you talk to a team they always have shiny objects that they can't live without. Maybe they think functional programming is great (yeah, you can handle errors with monads, but you can drop errors on the floor just as easily as you can with exceptions, return codes, etc.) or that immutability solves everything, JDK 13 is the best, whatever. So people are thinking about everything except the thing that management will care about which is long-term sustained productive development.

> Don't repeat yourself.

And also do repeat yourself. Consistency (patterns) are a factor in reducing errors.

Whole books have been written about this. I use the following rules from [1] myself. I like them because they definitely put some constraints on my coding which I like. It forces me to think beforehand and gives me (a junior developer) some guidance which I desperately need:

* Write functions/units that are <= 15 LoC.

* Create units with <4 branching points

* Write functions/units with <5 parameters

These guidelines force you to write maintainable code. Always open for discussion. Note that these lessons are lifted straight from [1].

[1] https://www.softwareimprovementgroup.com/resources/ebook-bui...

Write code for use not for reuse. Write code that is easy to remove, not code that is easy to change.
Be as explicit as possible. Use guard clauses. Don't couple things for the sake of less code. Very, very often, repeating yourself is OK.
If this were an easy answer, software devs would not be paid as much. Read some books about the topic, then write a lot of code, then change a lot of that code, and learn from your experiences regarding what was easier or harder to change.
You're going to get a bunch of extreme platitudes in response to this question.

Like "DRY", "YAGNI", SOLID, short vs. long functions, how to test, minimizing vs. maximizing reuse, static vs. dynamic typing, cyclomatic complexity, interfaces, functional programming, immutability, etc. You should definitely be familiar with these principles and their tradeoffs.

My advice though would be to take them all as suggestions, not absolutes, and to look at your specific requirements of your project, what you know about it and your customers (how it's likely to evolve over time, how it has already evolved, how your company will evolve, who the engineers are, etc.), and then find & adjust the principles that apply. There are no silver bullets here, and which techniques are successful depends very much on the context in which they're used. Work backwards from your team, your situation, and your customers.

I agree with this, but I'd like to add one more thing. No matter what you do, if you find that your code is hard to change, then change it so that it is less hard. Next time you visit it, change it again. Keep doing this until it is very easy to change the code. Write enough test support in your code that you can do this kind of work safely.
- Don’t write code until you need it. This results in less code, which is directly correlated with ease of change. - If you write lots of separated classes but then use them directly inside other classes, you’ve defeated the point. When you depend on something, pass it in instead of directly referencing, if possible. - keep methods less than 5 lines, classes less than 100 lines.
high test coverage and functional purity as much as possible.

don't get trapped by being "too DRY"

Start with the absolute most simplistic structure, or no structure. Starting from "Hello world" is OK! (ie. starting in a new programming language)

Keep in mind you can always start over, especially if you componentize your work!

As you prototype, or just design by drawing on whiteboard, learn your domain and gain ideas what would improve your initial lack of design.

Iterate and refine when cost effective to do so, not sooner. If still on whiteboard, you could benefit from implementing rapid no-code solutions before diving into any code at all!

Resist committing to structure, unless your experience and insights into the domain require it and you see clear benefits from doing so. Deconstruct structure you find bring unnecessary complexity, as well as build on structure when doing so clearly brings leverage.

Care for your code.

I would suggest reading Head first design pattern, apart from design patterns the book also explains how 'change' is inevitable in software development and how to design the software extensible and modular
Extensible doesn’t mean the code itself gets changed often. It would help to read up on SOLID [1] design principles and books on software development. Actually implementing SOLID would come by reading good code, getting mentored and from experience.

[1]: https://en.m.wikipedia.org/wiki/SOLID

I'd say write the least amount of code you can.

While you're adding a feature it's always tempting to go down the "what if" route and make some kind of crazy interface that is extensible and able to accept plugins and all that. The problem is you're likely making it extensible in ways that turn out to be useless down the road.

Just code the least amount you have to in order to achieve the current goal. That way down the road it's easy to delete what you did, or code up some changes to it. I personally think the worst thing you can do is write code that is all things to all possible future features.

This is probably the most valuable advice. Less code has less bugs, is easier to read, test and document.
Write shallow code, code that is the simplest solution for the problem

If the word "architect" springs to mind then you are doing it wrong

I used to focus on making my code easy go extend. I still enjoy that style in personal projects, but on large teams, I find it is better to make your code easy to delete.
Make it easy to delete conceptually

It should be removable in a way that does not break other systems and it should be easy to identify when "it" ends and when something else begins

Keep files small and use the file system to your advantage: it's much easier to organize information in a tree than in a string. Keep code for components together (e.g. no "controllers" folder). Write twice as much documentation as you think you need.
Only write code that's absolutely needed for what you're building.

It's very tempting to make it "extensible" by overgeneralizing. What you get then is overly complex code, most of which is never used.

Cut out everything that isn't currently used, and you have a small system where every line pays for its keep.

This. The problem with unused extensibility is that it tends to get into the way of the extension that you will actually need... It's easy to extends something simple in any way. It's hard to refactor something that's complex.
This this. Very aptly put
Agreed - and note that this principle is in tension with the junior/intermediate (heck, even senior) urge to make things "easy to change" by just making everything abstract and configurable. ("See, now if we want to do it this other way in the future, we only have to flip this bit!")

I still haven't found a pithy way to describe the limitations of this principle. Obviously, frameworks are helpful and also fail this principle, so even this principle has exceptions. But are you going to be so arrogant to believe your abstract extensible change will be adopted by all your fellow teammates in the future?

I think the core problem is that it assumes you know what part of the software will need to be changed. But you often don't so you end up with code that is too rigid in places that you need to be flexible and too flexible (i.e. complex) in places that don't need it.
How I think about this is that adding this flexibility is no harder to do when we actually need it.

So there is no reason to add it now, even if we're 100% sure we'll need it. And in reality, those expectations are rarely more than ~40% true.

This goes hand in hand with refactoring as a core skill. If you're good at refactoring, you can always tweak code to do what you want.

100% this. Usually reviewing code of juniors one of the things that appear the most is how they try to apply patterns they read online to over abstract things that do not need to be abstracted instead of solving the business issue with the minimal code necessary.
This mostly matches what I've come to believe over the course of my career with one slight tweak: also make sure that your code can handle the immediate next thing you know needs to be done. Don't try to plan any farther ahead than that because requirements change so often that you'll just waste time, but also don't write something so hacky that you'll have to do a bunch of rework to support the feature you know have to implement next week or month.
In a few words?....'Don't write the code in the first place'. Yes, follow best practices of modules etc, but in general write a lot less code. This is harder than it sounds, and often requires multiple rewrites.
Keep a document of your design decisions. It's very tempting to hack away to get things done, but human memory is short and feeble: you will forget why stuff was coded the way it was faster then you think.

I usually keep a Google Docs page open where, as I write the code, I update the documentation. It keeps things consistent and flexible, and much easier to go back and refactor.

I think this is key when you are working with code you don't touch every day.

On code you touch every day one can easily make changes since everything is fresh in one's mind.

When you are working with a code base that you touch once a week or even for a few hours/minutes every day having notes about what you did, why and perceived next steps at the time is crucial and VERY useful.

Another strategy I found useful is to try and be consistent with your choices even if they are not fantastic.

For example: all your tables are named like tbl_entityNam. Even if prefixing every table with 'tbl' is a bad idea, the consistency in keeping it will be useful later when you need to work with that code.

Keep code as simple as possible, shun abstractions until the refactor stage, adopt a language / toolchain that enforces a modular / functional discipline. EDIT: also, reading the comments here - absolutely shun DRY, be very explicit with code and don't try to couple anything until its absolutely clear that is the way to go. This in my opinion is the true mark of somebody experienced with codebases that grow unmanageable vs the less initiated -- the latter are always trying to reduce code by coupling things into abstractions, the former wait until it absolutely makes sense.
Writing code always involves starting with assumptions about how the code is going to be used. Extensible code is really just code that includes in its design assumptions that a part of it might need to be switched out or accommodate something more in the future.

Sometimes its easy to predict how your code may need to be extended if you give it a bit of thought. In those cases, it might make sense to allow for easy extensibility. This is pretty rare though. Most times, you can't really predict what will be required of the code in the future. In those cases I prefer to bake as few assumptions into the code as possible, so when someone else is extending it in the future, they spend as little time understanding your code.

Assume the code is going to be changed by first semester CS students. Dumb it down as much as possible. Don't do any clever tricks. Make the patches as small as possible so they can be (i) easily reviewed and (ii) allow easier bisecting.

Example: if a computation requires some intermediate value, create a new variable with a proper name and assign the intermediate value to it, instead of just inlining it in a bigger statement or assigning it to "res". The compiler knows how to deal with temporary variables like these in an optimal way.

This and include conversational comments that explain in plain terms what problems you're solving and why.
Make code easy to change by changing it often, thus forcing you to either structure it so that it's easy to change, or die