4 design mistakes you will make as software developer: Low level design, Part #0Malay : November 22,2020
As software system evolves it grows in size and it becomes increasingly messier really fast if we don’t pay close attention to.
How to NOT create a code base that is a maintenance nightmare? What is the scope of SOLID design principles as a thinking framework to approach low level design problems?
In this first installment of the Low level design series, you will learn about few common ways to create working but bad code and how to not do that
TL;DR included if you want a quick read.
Mistakes to avoid
- Shiping the code as soon as it works
- Assuming requirement of an abstraction to account for probable obscure use cases.
- Not slowing down in development process. ( A idea on how to slow down effectitely is mentioned below)
- Not knowing what cleaning the code means. ( Cleaning the code is neither adding comments nor renaming variables with longer names)
Few Ideas to understand to avoid the mistakes
1. Complicated vs Complex systems: Complicated puts high cognitive load, Complex is big but it makes sense.
2. Slowdown in development phase to think about maintenance to make maintenance easy
3. Design is an iterative process. You switch between development and then design with every significant change to the system.
4. How to slow down: Go fast, implement --> Slowdown, create proper abstraction to model problem space
5. How to create proper abstraction: Integrate thinking framework of SOLID principles when thinking about abstractions of a system
6. A brief introduction to SOLID principles.
Simple > Complex > Complicated
As you start working on a software project, adding features to it, making changes to it, it will become bigger. It will never remain simple after you pass, let’s say, about 5000 lines of code. It is doing quite a lot of things and has a lot of moving parts. So you will eventually make it complicated, right?
Not necessarily. You see, Simple is better than complex. Complex is better than complicated. So if your project has a lot of moving parts it’s possible to drive the design of the system to make it complex instead of complicated.
A complicated system creates high cognitive load. You have to understand a bigger portion of subsystems for you to work on just one of them or worse you have to remember how these systems interact with each other to understand if that small change you are making will bring a system wide havoc in some insidious ways.
A complex system, on the other hand, makes the system obvious. You need to account for smaller sub section of the entire system while making changes or adding features. The system itself will make it obvious how your change will propagate through your system. You will be able reliably make a change without breaking something without your explicit intention. Hence such a system is easy to maintain.
How do you create complicated systems?
You can make your systems arbitrarily complicated in innumerable ways. Following are two common ways to create f***d up code.
1. Shipping the prototype
Yes! It is working!
git push origin master –f and ship it.
No! Don’t do that. When you first implement something you hack through it to get it working. It is always a mess the first time. You ship such code for two reasons: a) You are leaving the company and you don’t care about the maintainability, it is not your problem anymore b) The management don’t care about maintainability of the code and imposes unrealistic deadlines.
If these are not the case with you think of it like this, the first time something works is just a proof of concept. Once you have done that you have just figured out whether it is possible to do the task at hand or not, which is not software engineering. Software engineering will come after that.
How do you layout the things you have just done in a way that is most sensible and are obvious to someone who is going to read it? How do you create abstractions that let you think about the problem you are trying to solve, not how you implement it?
Answer to these questions are design decisions that will determine how complex or complicated your system will be.
2. Creating abstraction too early
You have something to implement. And last week you have learned a few design patterns. You have decided you are going to use your newly earned knowledge. So you jam in few “Singletons”, “Abstract Factories” and “Builders” in your code to account for use cases that will probably never occur. Congratulations! You have created an over engineered solution to your simple problem, that people who are going to maintain will curse you for.
The grave mistake that you did here is creating abstractions without getting to the point where you know which set of abstraction defines your problem space. Remember, model your problem space, not your solution. That way you can be certain to some extent that if someone understands the problem that you are dealing with, they will understand the abstraction.
3. Not slowing down
All these problems occur because you are not slowing down. Slow is fast ( if you’re not this guy ).
What do you mean “slow down”? I am glad you asked.
You spend a disproportionally bigger chunk of your time reading code then writing, so why not make that part of the work easy and efficient?
“Slowing down” means slowing down in development/design process to make the project easier to maintain.
Design is an iterative process. It is not something you (or the architect) do at the beginning of the project and then you forget about it. You have to re-evaluate your design after you make some change to it. You have to constantly ask yourself, “Does the abstractions of the system STILL represents the problem space?” If not, then it’s time to slow down. Slow down and remove abstractions that does not makes sense anymore. Remove design patterns that is not serving its purpose but is something that you have to worry about for the sake of it.
How to slow down effectively?
These are the steps to slow down:
a. Go fast. Implement your idea as fast as you can. Get it to working.
b. Once it is working realize you only 50% of your job is done.
c. Read the code. Clean the code.
d. Make sure it works after you clean it.
4. How to NOT clean the code: Problem Space VS Solution Space
Cleaning the code does not mean renaming your single letter variable to longer words ( e.g. ‘i’ to ‘index’ ). Cleaning the code is not writing a comment explaining the contraption of a code you just wrote. Cleaning the code is not adding try-catch to block, neither is adding debug logs.
Cleaning the code means creating abstractions that makes sense. Cleaning the code means, making sure when you look at a higher level abstraction it clearly speaks about the problem space. Your functions makes sense and obvious logical steps to handle the problem at hand.
When you have “Builders” or “Abstract Factories” inside your classes with business logic, your code is speaking about how you solved the problem, i.e. your abstractions are in solution space. Instead, your code need to talk about what are the logical steps to do after the other to get the job of that particular class done. That is your code need to be in problem space at every level of abstraction.
Now the critical questions you should be asking are:
Wtf are you talking about? These sounds fancy, but tell me how do I approach these problems? How do I clean my code?
I am glad you asked.
How to engineer a software: SOLID framework of thinking
SOLIDs are a set of guide lines created by (Robert C Martin) to work as a framework to think about abstractions of a system. Think of design patterns as a set of ready-made solutions to some of design the problem. You need to understand when not to use a design pattern. And why do they exist. Leaning design patterns without SOLIDs will be misguided.
SOLID is an acronym of acronyms of 5 principles that says what not to do with a system to avoid some common design mistakes and keep your system sane.
S.O.L.I.D. stands for the following:
Single Responsibility principle: It says, one thing should do only one thing, and do it well.
Open Closed Principle: It says, design your modules to so that you can change its behavior without modifying it? Huh?
Liskov Substitution Principle: It says, objects derived classes should be able to replace objects base classes, without anyone noticing.
Interface Segregation Principle: It says, I should only care about the part of an object that I need. I should not have to listen to the life story of the banker named Bob, just because the Bob is a writer too, when all I want is to withdraw some money.
Dependency Inversion Principle: It says, depend on abstraction not on concretion. You should care about your package being delivered fast and safe, you should not care about which courier company does it for you.
In the next installment of this series. We will dive deeper into these principles and talk about what is the intention of these. Then we can sensibly talk about design patters and their purpose.
Until then. Thanks for reading. Seeyaa….