It is easier to write an incorrect program than to understand a correct one.
- Alan Perli
Cost of adding a feature or removing a defect depends on the code complexity. At the start of the project, you have additional cost of building the infrastructure. But soon enough, the code complexity and technical debt start to slow you down more.
Complexity is the root cause of the majority of software project failures. Projects don't fail because your managers are bad or your customers abandon you. Projects fail because code become locked to a state where making changes becomes harder and harder.
Developers need to understand the system to develop it. As the system becomes more complex, it becomes harder to understand. You can gain understanding of a system from outside or inside.
Testing is used to see how system behaves when looking from the outside. Testing is hard because you can never know if you have tested all possible cases. Testing basically shows presence of bugs, not their absence. Testing is essential but relying only on testing is bad. Really nice aspect of usability testing is that it is unrelated to code complexity, whereas unit testing is related to complexity only in small degree.
Reading code is used to examine a system from the inside. But reading complex implementation is a lot slower than reading a simple implementation. When your whole system is tens of thousands lines of code without any modularity, fully understanding the system becomes nearly impossible.
You can divide complexity to three different types:
- Problem Complexity
- Solution Complexity
- Implementation Complexity
If the problem is not well defined before construction, you might end up solving the wrong problem.
Software is always a process to solve a problem. Each problem has essential complexity that must be included to fully solve the problem.
The original problem is a thing that you cannot change. But make sure you solving the real problem, not a problem hypothesis. If you are developing for a customer, you should be focusing on customer needs, not what the customer wants.
Divide the problem. You should divide the problem into multiple smaller problems. Then you plan on implementing solutions for each of those subproblems and double-check that the original problem is solved.
A simple solution to a complex problems in rarely a bad thing as it can be improved. A complex solution to a complex problem will backfire if the solution is wrong.
Solution complexity is the complexity that comes from the solution. You can always affect the solution complexity. Solution complexity is minimized with proper planning and design.
It is mostly about the design and architecture. Solution should focus on telling what is required to solve the problem, not how. Solution complexity relates to data the software works on, how the data flows through the system and how user interacts with the system.
All data that is possibly needed is essential data. You can use derived data but be careful not to create imaginary relations between unrelated information.
You can use email as username.
Save it separately so both can be changed later.
Then you have avoided an accidental state where email is strightly
bound to the username.
Store everything as simply as possible. Keep all information in plain text if possible.
There are no prefect solutions. Solutions can always change. Learn to predict what will not, what might and what will be changed later.
All programmers write bad code. Good programmers write working code. Excellent programmers clean up their code afterwards.
Bad code is the first step towards good code. Writing bad code that works is called technical debt. Technical debt makes code changes harder and harder, leading to a stand-still. Experience tells if and when to repay the debt.
Here are few examples that cause implementation complexity:
Forced Flow: Systems in which you must to consider the order in which things happen. Programmers spend time specifying how the system should work rather than what is desired. In the following, order of these three lines do not matter, but we are forced to specify the control flow in normal programming languages.
a := b + 3
c := d + 2
e := f * 4
States: States make programs more complex. For every bit of state we double the total number of states. Different states do not show up in the code so it makes reading the code harder. States are also contaminating.
If you have 2 functions, function A relies on state and function B does not.
If you call B, it does not rely on state.
But if you call B which tell internally calls A, it relies on state too.
Volume: When you have more code to read, you tend to miss some aspect of the system. Complexity arises when you have to read a multiple modules to understand just a single feature of the system.
Inconsistency: When you have two similar systems that offer similar functionality but work totally different ways. You require developers to remember how each of them work. You could make them have similar behaviour to reduce mental effort required.
Duplicate Code: When you change functionality, you need to update multiple sections of code base. Someone in the future will forget to update both and thus seemingly same functionality starts to work differently. "Don't Repeat Yourself", DRY, is a common mantra, but this applies to behavior and knowledge, not code lines themselves.
Unused Code: Reading code becomes more troublesome when trying to figure how a section works if it is riddled with unused code. Related to the problem of volume.
Poor Modularity: Poor modularity and missing abstraction will cause duplicate code thus has the same weaknesses.
Poor Documentation: If a block of code is not commented and you need to use it, you must read through the implementation to use it. This makes using modules and libraries more complex.
Complexity Breeds Complexity: When you do not understand what code does, you just copy-paste it. Causes duplicate code, unnecessary code, poor modularity and poor documentation.
Power Corrupts: Power of manual memory management increases complexity, thus automatic garbage collection is good. The more powerful the language, the harder it is to understand systems constructed in it. Same applies for premature optimization.
Simplicity is Hard: First solution is rarely the most simple. Complexity is caused by existing complexity and time pressure. Simplicity needs to be focused, sought, recognised and prized.
To reduce code complexity:
- Make everybody understand the architecture of the system.
- Make everybody understand what has been implemented.
- Make sure descriptions of features are accurate and understood by the team.
- Minimize rework caused by misunderstanding requirements correctly.
- Make it easy to find all the places where developers have to make changes when they are working on a feature or defect.
- Make it easy to validate that there are no unwanted side effects or regressions.
- Make it easy to notice defects in production.
- Make it easy to hotfix defects in production.
1. Diversify what parts of the system each developer are working on.
1. Keep your documentation updated.
1. Your code is self-documenting and consistent.
2. Have issue tracking.
2. Have recurring check points like sprint retrospectives.
2. Allow developers to showcase what they have done.
2. Have code reviews, developers commenting on pull request is enough.
2. Unit tests show what has been implemented.
3. Keep your documentation and comments updated.
3. Keep your issue and pull request descriptions updated.
4. Make issues contain as much information so misunderstandings don't happen.
4. Make issues specify whys behind the request, not what should be done.
5. Keep your test updated.
5. Have coding standards.
5. Consistent design makes it easier to find all the places.
6. Implement unit tests, integration tests and continuous integration.
6. Use quality assurance team/person.
6. Having low coupling and high cohesion reduces side effects.
7. Have email or SMS notifications on production errors and downtime.
7. Make it easy to submit user feedback.
8. Make deployment easier.
8. Make reversing a deployment easier.
8. Have a clear procedure how to do hotfixes.