Skip to content

Instantly share code, notes, and snippets.

@gernotpokorny
Last active May 22, 2023 13:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gernotpokorny/a1d8bbfe43207f0f248c65e4c8258c3c to your computer and use it in GitHub Desktop.
Save gernotpokorny/a1d8bbfe43207f0f248c65e4c8258c3c to your computer and use it in GitHub Desktop.
Clean Code

Clean Code (opinionated)

Tool Selection

Code which does use unfavorable tooling and design patterns cannot be considered clean code.

Of course SOLID, KISS and so on are all important and I don't want to downplay that either, but there are much more important things which have much more devastating effects on your codebase and company if you get those things wrong, because these things are at the foundation of your codebase. One of those things is the tool selection.

Most of the time there are many different ways to solve a problem, but at the end of the day one way is most of the time clearly better then the other. Why may one way be so much better then the others? Various tools have different advantages and disadvantages, because having various advantages and disadvantages is a natural consequence of doing things differently and most of the time one advantage may not be important at all to achieve the goal, but is actually one of the main selling points of a tool. In the end usually a favorite emerges.

Example: One of redux-sagas main selling points is being able to test various sagas, but in reality those sagas are just implementation details and you shouldn't test these implementation details anyway. You can test them if it gives you confidence, but in general this code will also be tested anyway via other integration tests which don't test implementation details and you'll have enough work with writing unit tests, non implementation specific integration tests and e2e tests. Implementation specific integration tests require so much maintenance time and don't give you that much confidence, because they have to be adapted very often and have far less value then non implementation specific tests. In addition redux-saga also has some disadvantages like it makes navigating through your codebase cumbersome (no F12) and the code looks not so nice in TS, because types are not inferred automatically. Many advantages of redux-saga are very much for quite edgecase scenarios.

Example: Electron vs Tauri. The toolchain to build cross platform application of Electron is much more fiddly, not so well documented, the community is not very active, the binaries for the end user are large, the BE is hard to obfuscate and if you want to use TypeScript in a completely clean way you also end up with a monorepo setup or you use other ugly workarounds which is also not very desirable. The development experience with Electron Apps is simply not that good and if you have problems, then you may have to solve them on your own. At Tauri this is different. There is a vibrant community behind it and the repo setup is also much cleaner, binaries are much smaller, because the backend is in Rust which means your repo setup will also be much cleaner and the BE code will be compiled to a binary and you do not end up with a monorepo when you want to use TypeScript. You shouldn't just pick one tool and code something through all the hurdles that you face along the way and not think about switching tools/tech, the future and maintainability just, because you already wrote some code. It is much better to try various options and make a comparison and at the end of the day most of the time there will be a clear winner.

Example: Use Typescript over Javascript. There is a learning curve, but using TypeScript over Javascript will make your code better. Of course this is a subjective statement, but if you ask ten industry professionals, then probably all of them will tell you this. Often when you refactor some Javascript code to Typescript, then you will see that there are many things which are not so pretty and could be done better and those mistakes wouldn't have happened if Typescript was used right from the beginning. Furthermore Typescript will also improve your development speed, because of the feedback it provides. And not the least it is very important to note that, if you have for example a large React & Redux codebase written in Javascript and many people work on that codebase, then your Redux logic is most likely a total mess at least at some parts of the app. If the codebase is very big, then it will be very challenging task to refactor the whole codebase to TypeScript and clean up that mess.

You simply need experience in order to make this selection and you will make mistakes at the beginning. It is a learning process and no master has fallen from the sky yet.

Making the right decisions in regards to the tool selection will safe you so much time and money and your mind and the minds of the people who work in that codebase will thank you for it.

Design patterns

If code is written in a unfavorable manner like with the examples below, then this will really hurt the maintainabilty, because at scale these hours compound and will hurt big time.

Example: If I need to read ten lines of setup code in order to finally know within the 11th line what all this setup is actually for (for example the particular function call) and then find out that I'm not even interested at that action (for example the invocation of the particular function), then this is not clean code. I want to know what the action is and decide if I want to skip it and all details of it and not read all setup code first in order to just find out that I want to skip, but I already then invested the time in reading the setup code, because it was not clean code.

Example: A callback hell.

Syntactic sugar and Abstractions

Most of the time it is pretty obvious which code is easier to read and grasp and obviously we want to read abstract code in order to understand the code as quickly as possible without having to read all details which may not even interest us.

Example: A callback hell vs async and await. async and await is much more readable and neat in regards to semantics.

Example: In order to make a table sortable we want to just pass sortable: true into the options at the instantiation of the table and not write a whole block of code every time we want to have a sortable table.

Avoid obvious mistakes

Code with obvious mistakes cannot be considered clean code.

Example: Not testing your code. Code which is not tested cannot be considered clean code. Untested code is very hard to refactor, enhance and fix — and testing is also a form of documentation, because tests also provide a specification. Untested code often will also simply be not correct (too many bugs) especially if the code solves a quite complex problem.

Example: Having a full copy of various states in redux instead of using references (via ids). Full copies may have inconsistencies as a consequence.

Example: Once I saw a codebase which had a couple thousand implementation specific integration tests and so much time went into maintaining those tests and at the end of the day the value of those tests was pretty small and those tests didn't gave you much confidence that the application is working as intended (because of the nature of those tests). The codebase had zero non implementation specific integration tests and no e2e tests at all, but thousands of implementation specific tests. This is simply an obvious mistake. Just refactoring a very small piece of code made two dozen tests fail (false negative). That is simply not the point of testing and those implementation specific tests do not give you much confidence, and having confidence is the whole point of testing.

You simply need experience in order to not make obvious mistakes. It is a learning process and no master has fallen from the sky yet.

Code rot

A codebase with very outdated dependencies cannot be considered clean code. A codebase which does use nowadays obsolete libraries and frameworks or frameworks which do not have any significant market share cannot be considered clean code.

You do not have to change working functions every two weeks, but especially avoid getting stuck on old dependencies and ancient tools. Don't listen to people who tell you to not upgrade for three or more years, because with the newer version something may break. That is simply self destruction and you should improve your testing and devops setup if you are so worried. Frameworks and libraries improve tremendously over time at an exponential rate. Many companies simply die, because of code rot. They do not find employees who want to stay there, because working with legacy tech doesn't provide any value to them and their future and it is mentally really difficult for them such that they will not want to stay at your company if you do not care about code rot. The maintenance cost of a rotten codebase is so much higher. Furthermore you will have a hard time finding support within communities for your legacy stack and they will often simply tell you to upgrade and current documentations will not be useful at all to you if you for example need to integrate a new tool with your legacy stack.

Do the hard work and upgrade even if there are breaking changes and new major versions of your dependencies. You'll then be able to leverage all the new features and bugfixes coded by very smart people and not have to deal with the mentioned consequences of code rot.

Frameworks and libraries improve tremendously over time (and not the opposite) and upgrading will save much time and money in the future.

Sometimes new tools emerge which are simply way better than anything before and they then have a huge market share. This is then a case where a refactoring is necessary, because of all aforementioned reasons and you need to have a solid testing suite in order to cope with the situation, that a refactoring is necessary. An example would be next.js and Rust. Of course with Rust we cannot say at the moment, that Rust already has a huge market share in regards to system programming languages, but I have no doubt that this will come and everybody who does not take action now will get left behind. Note that Microsoft is already making moves and taking action.

Formatting and Linting

Obviously linting is an absolute key factor for clean code, because it solves so many problems in one go, provides so much consistency and it is so easy to implement. Therefore a codebase which does not lint or check the code against some rules in some way, cannot be considered clean code.

If you are using a modern setup (for example create-react-app which already provides a very solid linting setup) and modern IDE with some extensions like for example the eslint extension, then this shouldn't be a problem at all. Obviously the setup can be tweaked and optimized here and there, but the base is pretty solid with a modern setup and a modern IDE.

Teams should format their code in a uniform way and not constantly overwrite each other's formatting for obvious reasons (version-control diffs etc). If you constantly have to deal with unnecessary diffs, because your team cannot agree on some formatting rules, then this is obviously not clean (code).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment