Table of contents
As a developer, our primary objective is to develop software solutions that address a set of specific requirements (Fit for purpose). We achieve this by defining the architecture and finally implementing it by writing code. I believe there is more to it than just writing code. The way the code is written is equally important as what is written. Any source code should possess the following qualities for it to be considered a good code.
Well-commented and documented and
Following 14 principles would help to achieve the above qualities easily.
1. Draw Before You Code
It is always good to have the design ready before you start writing code. Irrespective of whether you are developing a small component of a huge project, it helps in:
Identifying the gaps/issues in the requirements
Helps to avoid issues during implementation.
Making the application's components scalable, which in turn allows accommodating requirement changes that may come up later in the project life cycle.
Dividing the requirements into smaller chunks and prioritising them.
UML diagrams like a
Sequence diagrams, etc. could be used for these as required.
There are a few approaches for this based on various factors.
Big Design Up Front (BDUF)
If you know the requirements upfront and if you are not expecting massive deviations from them, the BDUF approach would be a perfect fit for you. The software design is completed in advance, considering current requirements and the room for scaling in the future.
Example: You are asked to develop a login module for a web application. Right now, the requirement is to use the ASP.NET membership provider. A well-designed solution could accommodate Single Sign On easily at a later stage if the customer decides to switch.
There are certain caveats for BDUF.
If you are not sure of the requirements, and its scope, the design is more likely to be flawed. Don't go too deep with the design in that case.
Design revisions will be harder and more expensive once the code is written.
Agile and Emergent Design
If you follow methodologies like Agile, requirements are bound to change frequently. Sometimes, you may not be having clear visibility of the road map as they evolve. Having an emergent design is preferred over having a concrete design. It emphasises having the design emerge as we get more clear of the requirements.
Still, it is good to draw your classes, the relations between them, how they are interconnected, etc. It helps you to think better on paper.
Just Enough Design Up Front (JEDUF)
This approach tries to hit the balance of both BDUF and Emergent design by defining the foundation design upfront based on which, you could build later on. The upfront design will only take into consideration the fundamental aspects of the requirements. This helps to identify core risks at the start itself, leaving enough flexibility to account for unknowns later.
Another notable approach that could be mentioned here is Adaptable Design Up Front (ADUF). It becomes relevant when there is an ambiguity on how much is “Just Enough”.
Various architects and developers may have differences of opinion about the approaches listed above. It is not a one-size fits all solution and is highly debated. Whatever approach you take, it is always good to have your design upfront before you write the code. It helps to make your code - efficient, concise, reusable, scalable, and well-designed.
2. Measure Twice and Cut Once
This principle is an extension of the first principle – Draw Before You Code. Ensure all the project requirements are considered in framing the design to avoid surprises later. It helps in having predictable outcomes. It also helps in a more accurate estimation of timelines.
3. Result-Oriented approach
Defining what you are trying to achieve and all the edge cases in advance help to ensure that the code you wrote does what it says on the tin. This could be achieved by defining all the test cases in advance and, planning unit tests. This will prepare you to handle all the scenarios that need to be taken care of during development. If you don't have the outcomes prepared in advance, it is easy to miss some edge cases, resulting in a bug.
Example: If you are asked to develop a method that takes 2 date time objects and returns the time difference between them; While writing the code, it is easy to miss an edge case – the first date time is greater than the second one, or one of them is null.
4. Occam's Razor
Occam's razor or the principle of parsimony is a problem-solving principle put forward by William Occam in the 14th century. It states that – “Entities should not be multiplied beyond necessity”. When presented with multiple competing hypotheses, one should select the solution with the fewest assumptions. It favours simplicity over complexity. This aligns with what Albert Einstein said – “Everything should be made as simple as possible, but not simpler.”
When applied to software development, this advocates lean software development, encouraging developers to start with the most straightforward possible code. It reduces the chances of having bugs and allows developers to easily design, develop, test, and fix bugs at each step.
This principle is more relevant in selecting an appropriate model for machine learning problems.
Reference: Occam's razor - Wikipedia
5. Don't reinvent the wheel
For some reason, many of us developers tend to write code to solve problems that someone else might have already fixed. This might be because:
We are not aware of its existence.
We are not ready to get out of our comfort zone and try something new.
Furthermore, we assume it to be way more complex than it is.
Try to find if there are libraries or NuGet packages that could solve your problem. Even if you spend a couple of hours researching, it would still be better than writing it from the scratch, as the amount of time and effort required to perfect it could be utilised fruitfully.
Making use of design patterns (More about this in future posts) and existing application frameworks also helps to avoid trying to solve the problems already solved by others.
Example: If you want to work with JSON objects in a .Net project, use NewtonSoft JSON library or JSON.Net instead of trying to write your implementation of parsing JSON.
6. Keep it Simple, Stupid (KISS)
This principle is as simple as its name suggests. Make your code as easy and simple to understand as possible. It not only makes it easy for other developers to follow but also helps to break down complex problems into smaller chunks.
Ensure to use meaningful variable names describing what they are used for.
Ensure that your method names reflect the purpose of that method
Provide adequate comments
Ensure your methods do not exceed 50-100 lines of code.
- If it does, you can simplify it by splitting it.
Ensure each method solves only one problem or does only one job.
If your method is flooded with conditions, consider refactoring.
Do not keep redundant code. Delete it.
7. You Aren't Gonna Need It (YAGNI)
This principle states that a developer should not add functionality until deemed necessary. It is quite common for developers to think about future possibilities and add some code or logic to address them; which is not required at the moment. The software can become larger and more complex if developers add code for future use. This could end up as bloated code, making the software difficult to maintain. Avoid this mistake and keep your code clean of clutter.
8. Don't Repeat Yourself (DRY)
Andy Hunt and Dave Thomas in their book The Pragmatic Programmer defined DRY as :
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
This principle is self-explanatory from its name and aims at preventing duplication of code, logic, or data. A piece of code should only be implemented once, instead of written several times wherever it is required. It helps to save time, reduce complexity, and reduces the chance of bugs.
Example: Creating helper classes, common services, or libraries for sharing between multiple classes or projects.
9. Separation of Concerns (SoC)
This principle aims to separate software into distinct sections, where each section addresses a concern. Each section should be independent of the other, which makes it easy to maintain.
Ideally, a component, a layer, a package, a class, or a method should have only one concern and implement it.
Example: A typical example of SoC is how we organise microservices in a Microservices architecture. Each microservice addresses one concern.
Another example of SoC is the MVC pattern where the Model (the data), View (the UI), and Controller (the logic) are divided into 3 separate sections.
10. Avoid Premature Optimisation
As developers, we are often tempted to optimise our code to make it simple and effective, which speeds up development. However, if done too early, it could produce adverse results. Optimising before knowing the complexities and bottlenecks of the program would result in wasted effort and eventually rewriting that code. Try to avoid this mistake.
11. Principle of Least Astonishment (POLA)
This principle says that it is recommended to design a feature that doesn't have a high astonishment factor. End users prefer to have obvious, predictable, and consistent outcomes when using the software. Otherwise, they will drift away from using the features that surprise/confuse them every time.
This principle also aims to leverage users' existing knowledge to reduce their learning curve.
Ctrl + X is a commonly used shortcut for performing a Cut operation. A developer maps that shortcut to some other operation by default.
12. Law of Demeter (LoD)
This principle is also known as the principle of least knowledge. It aims at achieving loose coupling between components.
As per the guideline proposed by Ian Holland, it can be summarised as follows:
Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
Each unit should only talk to its friends; don't talk to strangers.
Only talk to your immediate friends.
It makes writing, refactoring and testing code much easier.
13. Never Go At It Alone
If you are working on a module or software project all by yourself, it is quite possible to make mistakes and miss things in the flow. Getting a fresh set of eyes to review your design and code always helps in maintaining a healthy code base. Moreover, it could be challenging for a single person to adhere to the best practices and write effective code.
Using pair programming, collaborating with other developers, and effective code reviews exponentially improve the quality of the code.
SOLID is an acronym that represents 5 core principles of software development.
This itself is worth an individual article and I am not venturing further to explain it here. Those who are interested can refer to the following:
A special thanks to all those who read this article thus far. I am quite sure I comfortably ignored some of the above principles while writing this article (Especially KISS; making it complex and a larger read). But at least I have the excuse of not writing code. ;)
I hope this article was beneficial to you in some way. This being a vast topic, have its limitations in including all the required information. If you felt you need more clarity somewhere, please point it out. I will try to include as much as possible. There may include areas where some programmers/architects disagree with certain points/aspects mentioned above. Feel free to express your views and feedback. Thanks again!