How to write maintainable code?

So I have been coding for many years in highly level languages and I never felt my code unmaintainable.

I’ve been playing around with zig for over a year… and everytime I start a side project, as my code grows larger, I find my code unmaintainable… I drop my zig projects after like implementing like 20-30% of the Idea, whereas in Go, I can push it 70-80% before I loose interest. (not accurate stats, more like how I feel. also I almost never use anything outside std lib).

I can point out several reasons but I think it’s mainly that I *almost*** never coded professionally so I never had to worry about the maintainability.

Any advice?

2 Likes

If you are dropping interest regardless of what the language is then maybe this is an area you should look into first.

Writing maintanable code and having interest in what you are doing go pretty hand-in-hand.

There is plenty of information about maintanable code on the internet. My biggest would simply be- in no particular order: good folder structure, good names, decent comments.

1 Like

It’s a complex subject. Experience definitely helps, if you try to tackle hard problems that are close to the limits of your abilities (which is not necessarily a bad idea, that’s how you push the line forward), it makes sense that you get to a point where you’re stuck.

Dialing the difficulty down a bit might help (i.e. try an easier project), but alternatively a good approach is to be willing to restart the project, meaning that you just re-do it from scratch a few times, each time incorporating anything new that you learned along the way.

One major cause for getting your project into an unmaintainable state is accumulating too much debt as you try to sprint towards getting the happy path of a feature working. Redoing the work from scratch helps both with debt (since you start from 0 again) and forces you to slow down.

If that is the issue that you’re facing, over time you will learn what is the right pace for you and, most importantly, when to speed up and when to slow down based on your perception of the state of the project.

14 Likes

Some of the concern may be here. High level languages hide a lot of complexity from you. You don’t have to worry about memory management, pointers vs values, etc. Some of what you are feeling is the extra cognitive load of worrying about aspects of the program that you didn’t before.

I’ve been running into this a bit with my side projects. I get a bit paralized worrying about struct size or how to do it efficiently before I even know the shape of the data or the way it will work.
The opposite also makes it hard. You throw everything into hash maps and try to ignore allocations which ends up with a program that is hard to reason about.

All this to say, I struggle with this too, and I’m trying to work on it by not being afraid to rewrite it and trying to identify when to care about specific aspects (memory management, struct size, etc.)

2 Likes

At least for me this is the best approach. I program nearly every at least twice. And really just delete the old code before starting the next one. For some projects I’m on my 5th time but it’s getting really good.

The problem is of course that it takes time.

8 Likes

I’ve been coding for about 7 years and about ~6 months of professionally as intern. In those 7 years i have done max 2-3 serious projects, rest just messing round and tinkering things. But recent times I feel some sort of burn out even for tinkering things and I’ve been soul searching.

that’s what I usually do, tinker with things beyond abilities after I get something working, I move on to other things.

I’ve heard many people talking about restarting project but I have never done. Sometimes I randomly open on working projects I started 5 years. But I’ll try that, I’ll try restarting my older projects.

I’m do understand the concepts but yes manually memory management has forced me rethink how I code and also resulted in messy code. also mostly I code alone so I never worried about whether other people can understand it so I get away with writing confusing code.

1 Like

There’s a couple of Go Proverbs that I’m always reminded of, even when writing Zig:

A little copying is better than a little dependency.

This one is the big one. Especially if you’re doing something new and learning as you go along, writing maintainable code in the sense that it’s well-factored and “clean” might just be impossible. Over-abstraction is the bane of my existence as well and sometimes I just need to get over myself, copy an implementation from somewhere else in my code base, change what needs to be changed and worry about cleaning it up later (if it even comes up).

The bigger the interface, the weaker the abstraction.

Similarly related to over-abstraction and over-thinking. If you eschew the need to do these things and just focus on writing code that does the thing you want it to, the interface and abstraction will follow through necessity as you continue to evolve the codebase.

The final thing that I can really add from my own personal experience, not related to Go, is write tests. If you’re on a big project that’s hard to really see benefits from until all the individual components are complete, writing tests - unit tests, e2e tests for the functionality, whatever - will give you tangible feedback that you can then build on. From my experience, once I know one component works as I expect it to, it’s all that easier for me to have confidence that the rest of the project will fall into place.

Hope that helps!

4 Likes

Not sure how much online programming discourse has affected you, but so much of what I see online is stuff like “memory management is easy! just understand the lifecycle of all your objects and use arenas!” and other such pronouncements that seem intent on making (often less experienced) readers feel like dummies. The reality is that, while you’re still trying to understand what a good solution looks like, there’s nothing wrong with (for example) throwing stuff into hashmaps and cloning freely. To Loris’ point, any sufficiently hard problem is going to warrant a rewrite or two before you understand the problem intuitively enough to do all the “right” things.

7 Likes

Oh also one other thing:

Don’t worry - professional coding and maintainability are less mutually inclusive than you might think. :wink:

(This is obviously tongue-in-cheek, but the point is once you find out how the sausage is made, you don’t really sweat it as much. :smile: )

10 Likes

I empathize with you here. Sometimes when the thing you do for fun becomes a job, it’s hard to do it for fun anymore. That is also ok.
I am lucky in that I get to work on what I want to work on, and I see the value it is bringing other people. That makes me happy to keep working on it, even if at times I’d rather work on something else.

Any side projects I’ve had recently(~4 years) are all about tinkering to learn something. I could care less about maintainability at that point. My goal is to learn a concept, test a framework, try a weird thing, etc.

Usually burn-out means you should take a break. Think of something that you yourself want to use. Something smaller in scope. Where you can finish it without caring much about maintainability and then make the V2 where you do- if you want. (Zig being pre-1.0 gives you plenty of opportunities to get back in there.)

What are the parts of programming you actually like to do? Are those things that you could turn into a project that you would of course, then use? Dev Tools? Domain specific libraries?

I’ve also found that having people to talk to about a project helps push things along and can pull focus to the correct areas.

1 Like

I honestly don’t know, I do things all over the place,

Done full stack I built a Discord style chat application and made some meaningful OSS contribution to JS web framework, I’ve read RFC and implemented DHCP protocol, attempted to build build a minecraft server. right now building distributed kv with resp support as final project for my bachelor’s (I actually started this in zig and then moved to go), bachelor’s degree specialization in AIML, and messed around with assembly for sometime…

perhaps If I picked a domain and stuck to it I would have been in a better place.

I think the difference between what you achieve with Go and with Zig is understandable.

Zig is low-level, and there’s no getting away from it. Solving a problem in Zig is going to need you to think about more things, some of which can be mundane, but also intricate. I can see that would accelerate burn-out if you get stuck in it. The positive is that it gives you control.

I personally find that I struggle with Zig’s idioms, the patterns people use that make some of the mundane stuff easier. The idioms are fairly different to other languages and trying to make them my natural style isn’t easy. However I also know that falling back into patterns from other languages will probably make things worse.

So I can sympathise.

4 Likes

are you talking about new Io interface?

like @kristoff said, the only tips i can attest is 100% working (at least for me). Is write and rewrite and rewrite and rewrite and rewrite again. Like everything i have on github is maybe 10% if not less of all the code I’ve written, I usually just throw everything away and start agains

The reason is that when you write the first implementation you are only uncovering the problem space as it truly is, not just as you thought it was. And each time you rewrite you are taking into account more and more information, constraints and taste into account, leading to better code.

It’s like gradient descent for when you train a model, you make baby steps go in one direction, get the backdrop of errors, course correct, and slowly but surely make progress toward reaching the next local maxima.

I’d say one of the thing that helped me a lot, to avoid rewriting everything from scratch, is also implementing program by splitting it into many small packages and libraries instead. Because it’s much easier to write small (Unix like) dedicated, does one thing right in an opinionated way first, and stitching those at the end, than doing over everything even the things that were ok.

7 Likes

I think this thread has delved much deeper into the topic of burnout instead of writing maintainable code. I wouldn’t worry too much yet about writing maintainable code, it still seems you haven’t settled on your style of writing but even when you settle and you know “how you want things to look” it’s still super common to look at your own code from 6 months ago and think you could’ve done it so much better now, that’s just part of learning and getting better.

About burnout it seems your burnout comes from wanting to make a project that sticks, or a project that gets accolades and being unsuccessful. This is so very normal. There’s no cure all for burnout but usually it’s just something that comes and goes, depending on your motivation, life situation, exercise etc.

I’ll boost what other people have said and recommend just tinkering with projects and rewriting them but I’ll also add that you should do a 5 minute thought exercise where you go through all your previous projects and list 3 things you learned from each. Usually helps with visualising your progress in these years

4 Likes

Looking back at the sokol headers (written in plain old C), which are my ‘longest actively maintained project’ (10 years next year), and in comparison to the millions(?) of lines of C++ code I wrote and abandondend before, the solution is really quite simple:

In a nutshell, don’t let yourself be seduced by high level abstractions and fancy language or stdlib features.

(Too) high level abstractions never work long time (I’ve seen too high-level code become unmaintainable over time much more often than too low-level code). Abstractions will eventually become a restriction when the changing environment no longer fits those abstractions (and that point will ineviatably come). At some point you’ll end up fighting your own abstractions which you were very proud of and which might have worked very well at the start of the project, and it will be hard to throw them away (both mentally and simply because it’s a lot of work to rewrite all the code into different abstractions - those two things are probably the biggest motivation killers).

Don’t “think in objects”, instead think in “data that needs to be processed”. To describe the shape of data, all you need is structs and arrays. To process the data all you need is functions. Inside functions all you need is sequences, conditions and loops.

Group your data types and functions into systems/modules, each with a ‘flat’ C-style interface. If you like the idea of classes, at least don’t expose them in the system interface, only use them for the internal implementation. Try to reduce dependencies between systems (especially shared data types, these will become a liability when one system needs to change).

When building a system, start to define its interface first, write hypothetical example code against that interface. Only when you’re happy with that, start to build the implementation. The system interface should be a clear separation line between “private inside and public outside”. Don’t entangle public and internal data types too much, keep them as separate as possible, even when that means that data that is passed over the ‘interface boundary’ needs to be copied. Never expose data types in the public interface that are heavily used in the internal implementation.

Take your time to read and debug-step through your code from time to time, this helps to stay familiar with the code and keep the design and implementation warts fresh in your head. Your subconscious will eventually come up with a clean solution.

That’s about the gist of it I think of how to build long-lived software with minimal maintenance overhead :wink:

TL;DR: structs and functions in “flat” system interfaces is all you need.

21 Likes

Yeah one tips I might add to the part of writing the interface especially in C, is to always write that interface and as you write it use it in a throwable main, just to ensure it’s not clunky, i can’t count how many times I’ve forgot to done that, and had to throw my idea away because it was bad.

2 Likes

Yeah one thing that I do too, is just read my old code, and do a review from time to time, like you said it helps visualizing the progress, I think other than that making code that we want to use is the first step toward getting someone interested in using it, even if it’s not good at first.

1 Like

You could also try to focus yourself a bit on contributing to some open source project which you like and use yourself. That way you can have smaller pieces of work which are still useful.

1 Like

I’m not about to be critical about it. I don’t know the details behind why the large interface was necessary, but honestly it did not lead to too much friction for me when porting z2d to 0.16.0 particularly. :man_shrugging:

I will say this though - as mentioned in the quoted post, interfaces follow through necessity based on existing code and use cases. This means that it was likely reasonably unavoidable at the present to move the language’s goals forward, similar to the reader/writer changes. I’m sure if it can get better, it will.

2 Likes