Hacker News new | past | comments | ask | show | jobs | submit login
How to Write Unmaintainable Code (2003) (github.com/droogans)
164 points by guavaNinja on Aug 30, 2020 | hide | past | favorite | 85 comments



All that is really amateurish way to write unmaintainable code. I specialize in dealing with legacy apps (diagnosing, refactoring, figuring out what to do that would salvage them). Let me share some tips from my experience that will really help you step up your game (real projects I have joined):

1. Don't write it yourself. Your expertise is too precious for the company and you work in a high cost location which automatically makes you too expensive to actually code. Outsource it to some other team in a low cost location. Hire entire team for the purpose of the project, ensure they know most of them will be let go before the project end and ensure they have no vested interest in the success of the project. Provide as little actual guidance on how the problem is to be solved, only very general requirements. Let the team self-organize. Don't supervise (your time too precious, remember?) and only ever involve in discussion about high-level architectural concepts. Expect the application to be delivered to PROD on time regardless of the state and the missing features to be substituted with workarounds.

2. Not happy with quality of the code? Start a rewrite without a good idea on why exactly you are doing the rewrite, what you want to achieve or how you are going to do it. Decide to slowly migrate functionality from the old service to the new one using strangler pattern. Don't get buy in from your stakeholders. Annoy them with requests for more staff while delaying development of critical features and creating more and more incidents. Eventually put the migration on hold having multiple services with unclear boundaries, copies of functionality, nonexistent documentation and new development team because the old got fed up with your bullshit.

3. Accept government contract to move 10 messages from webservice to a mainframe. Ensure you need 5 different features when processing moving the data. Each of the features requires, of course, their own programming language, database technology and their own message format. Use 20M (yes, that's twenty million) LoC. Yes, I have worked on a contract to add new functionality to something like that (10 messages with two dozen fields to move from ASP.NET service to IBM mainframe. 20M lines of code. Polish social insurance administration)


Parent is tactical, and you are strategic. Together you will be employed for life.


> Eventually put the migration on hold having multiple services with unclear boundaries, copies of functionality, nonexistent documentation and new development team because the old got fed up with your bullshit.

Even better: now that you're about halfway through that rewrite, start a NEW rewrite! Continue this process over and over until your codebase is a mishmash of ten different frameworks and development philosophies. That way, you won't be able to combine different components together unless they were written during the same six-month period -- and when that happens, you know it's time for another rewrite.


I once worked on a codebase that had been subject to this. It had been substantially, but incompletely, refactored at least three times in five to eight years. Everything was a mishmash of half-updated features nobody understood from the dawn of time linked up awkwardly to current methodology.

The product person was more concerned with the font in use than the product, which likely did little to help matters.


note to self: stay the fuck away from the Polish social insurance administration domain.


I opened a file with C code. I expected you know, the regular stuff, includes, functions, etc. Instead the first line was already a statement, followed by 5k lines of statements.

There was a huge function that had to be split in no less than three separate files and then pulled in with #include directly into function body. These were #included in multiple other places, for greater obfuscation effect.

I opened another file (this time .NET), still same application, though... I jumped with my editor into middle of roughly 25k LoC of code... to be faced with empty screen. A bit surprised, I started dragging the scroll bar and then suddenly the code started showing on the right side of the screen... There was a span of roughly 15k LoC that was indented so much it was off the screen.


Scrolling out into the expanse like that showed you have the mind of an explorer. And you were rewarded by discovering code never before seen by civilised man.


The IDE that took part in writing that code was too ashamed of it and indented it out of the right side of the screen so it never again has to render these appalling statements.


I would LOVE to work on this code. Of course not for a boss who wants features yesterday - just to be left alone to refactor it slowly and make it beautiful. Like those people who will unravel tangles of thread into neat balls for free and mail it back if you send it to them.


Any chance this was machine generated?


Well, whatever the case, it was generated by tools.


No, it was not machine generated, at least not in conventional sense.

This was written by team of drone developers from specification written by a team of drone analysts.


4. Split up the codebase into modules. You yourself cannot change these modules. Only the ‚module experts‘ are allowed to change their own modules. Your final executable will consist of ~200 such modules.

If you find bugs / problems you will need to convince the module owners that is in fact a bug in their module.

And modules are delivered only every 4/5 months..


Yep. Worked on application like that, too, albeit with only something like 140 modules maintained by team of 20.

It had a repository in subversion but modules were versioned by their owners by making copy of the code in a new folder with version in the name.

Builds were not automated and instead each module owner built it manually in Eclipse and "uploaded" them binaries to svn. Each module had a single owner, nobody else knew anything about the module except for the owner. Builds could not be executed when the owner was not available.

I was required to "prove myself" before I was given read access to module I was supposed to start my work on.

The application had a lot of problems in the form of "there was a bug a month ago and now it reappeared because somebody forgot to include the module in the updated version this deployment".

There was ostensibly a "database layer" but different versions of it were used throughout the application. The versions were really branches of it because they included workarounds for app-specific problems that they did not want to be present on other services for the reason of "stability". This of course made it a nightmare for me when I decided to create a proper Gradle build.

Each jar or class was updated separately. I spent a quarter trying to convince teams with their managers that just versioning everything together and deploying everything everywhere at the same time is going to be a better strategy. They said if updating single jars and classes causes so many problems then updating everything has no chance of working. 2 years after I made the change there was still not a single deployment that would fail.


How do you begin a complete rewrite without stakeholder approval?

Please advise.


Adopt long cycles, spend most of it rewriting, cram every feature into the last couple of weeks so you have something to show for in the PI Planning. Blame subpar performance on lack of staff. You will luckily be awarded with more workforce you can employ in rewriting code. Repeat until done or fired.


It depends on the organization. Most teams in most organizations have enough freedom (and rightly so) to do some kind of cleanup work, improvements, refactorings. When the development team is unsatisfied with the state of the application and they feel they will not get buy in for rewrite (and usually rightly so...) they might decide they will rebrand it as internal improvements/cleanup/refactoring. Have seen it many times.


Ew, that's a deadly dose of office politics waiting to blow up.

Guess I haven't read the guide on how to make a toxic & dysfunctional workplace.


To seriously think about creating unmaintainable masterpiece you must ensure correct environment that will meet the challenge.


Developers are autonomous, they can decide what to work on and start a rewrite without approval. Call it a cleanup/refactoring if management doesn't like rewrites.


The link seems a bit old, modern day development has added many new and exciting methods for making unmaintenable code:

- Use exceptions for control flow. With different exceptions leading to entirely different flows. On C you can use setjmp/longjmp + globals for a similar effect. This will help the maintenance programmer develop thinking in multiple dimensions.

- To save space, do not declare separate constants for every magic number/string. Instead use one large constant pooling all the constants, and then deduce the other constants from parts of the one large constant. You'll be teaching the maintenance programmer an advanced optimization technique.

- Abuse localization rules whenever a case-insensitive operation is operated (e.g. Turkish i, German eszett, etc.). You'll be broadening the mind of the maintenance programmer, teaching cultural differences.

For extra points, when dealing with fixed string constants in the code, abuse Unicode's right-to-left rules to make the shown data rather different than the actual data the compiler sees, and zero width characters the make the shown length different than the actual length. This is particular useful with the "one large constant technique" from above.

- Do use locals when the locals shadow the global variable. This will teach the maintenance programmer to pay attention.

- Supply more precise overrides in files/projects included in partial compilation as to make code that will behave differently depending on compilation order, included files/projects, etc. This will help teach the importance of a build system.

Bonus points if the included code is actually similar, but relies on subtle differences in the behaviour of a previous version of the framework or included packages.


> Use exceptions for control flow. With different exceptions leading to entirely different flows. On C you can use setjmp/longjmp + globals for a similar effect. This will help the maintenance programmer develop thinking in multiple dimensions.

Exceptions? Pfsh... that's so early 2000s.

If you really want to mess with a dev these... make everything async and don't bother to check if it completes.

And don't forget the flip side... the ui thread shouldn't be responding to messages when it could be blocking on a long running process!


Colleague I worked with "resolved" the second problem (UI thread blocking) brilliantly by just spin waiting Application.DoEvents() in WinForms.


> Use exceptions for control flow. With different exceptions leading to entirely different flows. On C you can use setjmp/longjmp + globals for a similar effect. This will help the maintenance programmer develop thinking in multiple dimensions.

I recommend writing a CustomException class instead. So everytime you see a third-party error, catch it and throw a new CustomException() (Without arguments, otherwise you may carry some bad third-party classes/exceptions in your codebase). And don't forget to catch CustomException at strategic places too, so you can actually throw a new CustomException().


I saw a version of this dating back to 1997, so that's why it probably seems old.


The second to last paragraph states that the author gave a version of the document as a talk at a conference in 1997. So it appears that your memory is spot-on.


I remember seeing it in the early 2000's, so yeah.


Just a short side note there is a capital esszet (ẞ).

https://en.m.wikipedia.org/wiki/Capital_ẞ



> Names From Other Languages

This is the worst thing that you could ever end up dealing with. Back at an old job many many years ago, the company bought a smaller one with the same business. Me and my at the time tech lead were given the task to migrate the database from the system they were using into ours. "How hard could it be" we thought. Well... All 51 tables scattered around two databases were in Romanian. Needless to say neither of us knew a single word in Romanian. I was surprised how much we managed to learn in a month though.


I write all my personal code, including comments, in English and not Swedish precisely because I want non-Swedish devs to be able to perfectly understand the code even without Swedish language or cultural knowledge. Also, thinking and writing in English is much simpler than jumping between two languages.


Personally I've always wrote my code and comments in English, even if I never fully understood why. That said, having studied abroad and returning after graduating I did hit a few rocks: My tech lead at my first job upon returning(the same job when we had to migrate the Romanian database) was telling me how to do something and he said something along the lines of "You take those and shove them into an array" in Bulgarian(my native tongue). As you could imagine, "array" in Bulgarian sounds nothing like that. I nodded with approval(having no idea what he had just said), and google-translated the word he used 5 minutes later. When "array" came up on the screen I felt like someone had smacked me in the head with a sledgehammer.


масив? I had a similar experience wondering what this thing is when I first heard this. I wonder what the etymology is.

Python dicts seem to be „асоциативен масив“ but „речник“ is also acceptable, so at least that's understandable.


Precisely. I truly have no idea how it's made it's way into CS, but I suspect it has to do with:

> Голямо пространство, заето от еднородни предмети. Горски масиви.


It's probably from the Russian CS.


Students from different countries are asked to write an essay on elephants.

An English student: "Elephants and their import for the industrial production"

A French student: "Elephants and their sexual life"

A German student: "Elephants as the precursors of tanks"

A Soviet student: "The USSR: the country where elephants originally came from"

A Bulgarian student: "Bulgarian elephant, the younger brother of the Soviet elephant".


Personally, I don't write comments. I write self explanatory method names. Comments become unmaintainable


Regarding the relative usefulness of a well-named method vs an explanatory comment, I think it depends on the culture of refactoring for the codebase. If a codebase sees its code very regularly refactored, then comments can be downright dangerous/misleading while methods are easily renamed throughout. On the other hand, a well placed comment can tell you a lot of “why” or “how-to” that a method name / signature cannot.


Comments can get stale really fast but I probably write at least twice the amount of comments as code because that's how I reason about the function and intention of the code. Sometimes the code seems simple but the reason why it exists can be really complex.


How do you deal with the domain language? Translating that can be a pain and a source of unclarity too.


I had fun adding https support to a decade old PHP app for managing a domain registry where the entire thing was in German :) I spoke neither German nor PHP.


I have to concede that I write doc:s and code comments in my native tongue so moving development like this gets more expensive. Also writing in English takes like double the time.


I know this will get downvoted into oblivion on HN - but I just can’t resist. Another way to write unmaintainable code - write it the Ruby/Rails way! No comments, no flowerboxes, no docstrings! Your method and variable names ARE your documentation!

In all seriousness though, I have found that even with carefully and thoughtfully named variables, classes, methods, etc., code with no comments and no human-readable explanation is very difficult to maintain. I’ve been doing it for years and it never gets easier, especially with large code bases. Yes, the ROI for adding useful comments may seem low now, but it does pay off for the unlucky newb, intern, or junior engineer who’ll be stuck fixing that tricky bug or integrating that odd feature request in the future.


Can you elaborate? What are the juniors stuck on? Usually if a junior doesn't understand something it means the naming/structure is not as clear as you think. One of my favourite things as a senior is having a junior peer review my code, the places they have questions is a great sign the change needs work.


I didn’t mean stuck “on”, I meant stuck “with”. I say this because juniors are often tasked with fixing the odd, seemingly easy bug, but if they don’t know the code base well, and especially if it’s not well documented, it could be a challenge, often requiring the seniors to work with them.


There are "programming antipatterns" that can be intentionally used to create unmaintainable code bases... like magic numbers/ strings, obfuscated spaghetti program flows, etc.

But there are more unintentional ways of making your code base less maintable:

- Lack of consistency: Avoid using multiple different words to describe the same thing.

- Mix everything together: This happens when instead of having a dedicated piece of code for each purpose, the code contains functions that tries to do everything at the same time.

- Cyclomatic complexity: This happens when a function can have many different unique outcomes, making it hard to understand and test. Long functions usually fall into this category.

- Implicitness: Being implicit can make it very hard to figure out what is going on. Abbreviations and acronyms fall into this category as well. Bonus points if you excessively use operator overloading, reflection or anything that can be used to have a layer of magic happening.

- Messy concurrency: If you are going to be using concurrency primitives, you better have zero tolerance with messy code... otherwise, you'll be living in a multithreaded hell made out of spaghetti involving mutexes, condition variables, semaphores where things break and you have no idea why.

- Memoirs about journeys to nowhere: This happens when instead of writing a comment explaining what something does leaving out irrelevant details, you write a longer comment that reads like a monologue with many irrelevant details where the central point is you rather than the code being documented, and reads in a complicated, non-linear way... like: "I thought this did A, but then because of B, C happens this does D. Right? TODO: find out more about E". Some people think this makes them look clever.

- Interleaved levels of detail: Instead of having layered levels of detail that do not mix, write code that deals with high level things and very low level things at the same time.


> Single Letter Variable Names

I saw @axegon_ talking about "Names From Other Languages" and calling it "worst thing that you could ever end up dealing with". But I tell you, a "Single Letter Name" master will turn your mind really quickly.

You open up the code, that's a file of over 2,000 lines, no comment. Everybody inside there were called `a`, `b`, `n`, `i`, `q`. Some functions were a little better, `handle`, `add`, `equal`, `DataClass`, `MainProcessHandler`, `Service`.

Yeah, I rewrite the whole thing because at least I can trust the stupidity of my own.


Weirdly enough, Go seems to actually promote short / single letter variables, depending on context: "The basic rule: the further from its declaration that a name is used, the more descriptive the name must be. For a method receiver, one or two letters is sufficient. Common variables such as loop indices and readers can be a single letter (i, r). More unusual things and global variables need more descriptive names."

https://github.com/golang/go/wiki/CodeReviewComments#variabl...


An example of bad naming from some code I inherited and worked on last week

  if check_date(owner):
actually meant

  if has_an_active_subscription(owner):


I'm just here to say that the plural of status is not "statii", not even in Latin. Status is a noun of the (relatively rare) fourth declension, and its Latin plural is "status" itself.


One other excellent way to throw off the maintenance developer is to define & use LOTS of dynamic methods in obfuscated locations.

Step up your game by defining those methods from a DB entry.

(I wish I was making this sinister plan up for satirical purposes)


Well of course you should define the methods in DB entry, that way they can be changed as needed as business needs change. Seeing as nobody is going to be touching the absolutely sacrosanct source code, putting functions in the DB and reading them into the code base during compile and/or hot-reload is really the only reasonable way.


Surprisingly light on version control best practices.

1. Big commits make it easier to evade code reviews, submit pull requests close to the deadline

2. Commit messages should be generic and misleading

3. Logical units of change should be split up between many non consecutive commits, big commits should contain multiple different features and bugfixes

4. Have bots commit to version control frequently

5. Keep multiple copies of the same files for backup

6. Rename and move around files frequently

7. Reuse filenames liberally


One thing not mentioned here is using regular expressions. Use regexes liberally to process all data [1] because eventually it will converge to it and if you haven’t looked around for a solution involving regexes, [2] you’re not working on the really hard problems. Oh, don’t bother writing comments about regexes. They’re pretty much self-explanatory once the expression itself is beyond 10 characters long.

If regular expressions seem too tame to you, write all your business logic in SQL that’s contained in a long stored procedure. Prefix the name of the stored procedures with “HBD_” (HereBeDragons) so that your future self doesn’t get hurt. For added pain, use triggers that will invoke stored procedures, but not everywhere. In this case, inconsistency is the name of the game.

[1]: https://xkcd.com/208/

[2]: https://stackoverflow.com/questions/1732348/regex-match-open...


SFINAE is the obfuscator's best friend. In c++, you only need a single function name. You'll need to add garbage parameters to disambiguate some calls, but that will only add to the reader's challenge.


ADL is nice too. For best results, spread each namespace over as many files as possible.


Using an underscore for a variable name is perfectly fine when defining callback functions, where the underscore merely fills the void for the 1st parameter name, and you only need the 2nd parameter.


So naturally, one should be sure to use underscore variables in all contexts except that one.


The convention in our codebase is quite simple: only use `_` as a name if you will ignore that parameter or value, and always use `_` when you will ignore that parameter or value.


It doesn't matter much how much work you put into maintainability, someone somewhere will find a rule to declare it unmaintainable.

The rules for maintainable code are not maintainable!


I would agree if this was about "bad" code. But maintainability is easy to prove -- just give some random developers a feature or two to implement and see the results.

In every single project I ever joined there were people complaining about the quality and maintainability of the code. Yet some of the projects were truly stuck not able to deliver anything and others were chugging features at a steady pace.

Maintainability is difficult to define but it is definitely real.


Java? Pfft.

Step 1: write your project in Node.js.


Came here for this.


statically compiled languages are a pain, let's use a dynamic one for this project!


> Never ascribe to malice, that which can be explained by incompetence. - Napoleon

I have never seen that quote attributed to Napoleon before.

Today it is known as Hanlon’s razor, but it can be traced quite far back in time, just not to Napoleon. As always, Wikipedia got you covered:

https://en.m.wikipedia.org/wiki/Hanlon's_razor


I this case it was probably neither malice nor incompetence. They've probably attributed the quote randomly just for fun.


In modern development you should actually use micro-services. It is so useful that you can use it for everything. For example, if you find out that you have too many different functions to validate an integer, write a micro-service that validates integers!

And because the new service crashes when you input a non-integer string, please catch and ignore the exception client side (we'll fix it later).


Definitely don't spend any time on things outside the scope of your microservice, your code is wonderful and pure and any pesky architect that wants to argue about integration with the rest of the architecture just doesn't appreciate your Art.

On that note, as architect make sure to insist on perfect implementations of specs, like the JSON:API media type (https://jsonapi.org/format/), or use older formats like XML and insist on perfect implementations. A simple "validate number" microservice will have so many nonfunctional requirements it'll take years to develop.


I've inherited a codebase that seems to have taken this article to heart. It uses random unnecessary abbreviations, reuses variables like "i" within the (function) scope (JS) of >1000 line functions, it's got some 15K line files, comments that mean nothing, copy / paste everywhere, it just goes on.


How to write unmaintainable code? Easy : write a project based on the latest "saveur du jour" JS framework.


I believe that the current trend to decouple everything is one way to make something a maintenance nightmare. Using reflection or micro services to tie everything back together. Data shouldn't flow between components without some black magic fuckery ever or you could have hard dependencies that can't be decoupled easily and split into more testable functions and seperate files.

For good measure do it in TDD. That just creates the cherry on top for maintainability in high pressure scenarios where features have to be generated quickly ... and possibly not by the same dev doing previous work and is used to this high powered mode of existence.

Whatever happend to finding balance between coupling and cohesion?


> Hungarian Notation is the tactical nuclear weapon of source code obfuscation techniques; use it!

As a young programmer I worked in a very serious enterprise codebase that used the systems variant of Hungarian notation: prefixing variables with str, int, bl, fl, etc. They also sometimes prefixed for scope, like g[lobal], m[odule], etc. To this day I don’t know if anyone on that team of people 20 years older than me understood why I had such a tough time not cracking up in meetings discussing ‘gstrNotFoundErrorMsg’.


Woah, do compilers really have function name length limits? Which languages?


The ANSI C standard requires a compiler to ignore any characters in a variable after the first 31.


This is not true. It requires compilers to support at least 31 significant initial characters in external names, 63 otherwise (in C99), but beyond that it is a quality of implementation issue and implementations are explicitly encouraged directly from that same standard not to impose artificial limits. The C standard allows and recommends compilers treat the full names of variables, functions and anything else as significant.


Ah, good to know, I thought it was proscriptive.

Let me rephrase, then: to write portable C, you must keep the first 31 characters of any name distinct, because it's legal for a compiler to ignore everything after that.


Yup, phrased like that it's about right and a legitimate gripe. The limits in C90 were even worse. There are still a few projects around that use the requirement to treat more characters as significant in internal names to work around that, like so:

  #define copy_from_memory copy1
  #define copy_from_file copy2
And then provide the declarations and definitions of copy_from_memory and copy_from_file as normal, knowing that they will be renamed to copy1 and copy2 by the preprocessor.



in short, grab a task. make it work, and never look back to make anything right.


(c++) template everything


I'm pretty sure there are lots of people in the industry who write unmaintainable code on purpose for their own job security. I don't endorse this, but I've heard horror stories at several well-known companies about instances of this.


Probably not working very well since the bosses can't care less about the actual technical quality when making decisions. I.e. they don't know or care it's unmaintainable. So you build your fort and get picked off just outside the gates.

It might be revenge though, not job security? Or maybe "organic" local job security, i.e. the colleges of the perpetrator can't take over his tasks easily without management investment?

I guess most bad code is just bad/novice programmers or bad project management.


"I'm the only one who can understand this important code" seems like good job security to me


I worked with a guy like this, long-term contractor at a business institution. Every shell script he did was wrapped in many levels of awk,sed,perl,regex, with no comments whatsoever. And he worked there for 10+ years. At the end it was easier to rewrite his "modules" than to maintain them.


Just make and use your own libraries in your own private repos that can't be switched out easily. Then just revoke access as needed.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: