Ideal software design
Our world is not ideal and actually we meet very few things that completely meet our expectations. If you develop for many years you might have noticed that your code gets worse with the time, either you look at it with more criticism or the continuous maintenance spoils initial good intentions. But we should leave subjective attitude to philosophical discussions and find objective signs of good and qualitative code.
The agile development practice gives us some clues: code should be flexible enough to react to possible changes of specifications in the future, but still this sounds very abstract. You are expected to predict what changes may occur and design solutions that survive changes with “less harm”. But in reality you always have time limitations, so you think less about well implemented and perfectly tested solutions. You should also consider the YAGNI principle to be pragmatic enough and implement less possible functionality to cover only the defined use cases. So we can say an ideal code is not something perfectly flexible but rather pragmatic, resource sparing and good extendable.
An ideal code is for sure not something over engineered, sophisticated and in a way fancy, as the practise proves that this results in high learning curves for new team members, longer implementations of simple tasks and of course temptations to hack shorter ways. So an ideal code should be simple enough to let developers with diverse skills work with it without efforts in understanding.
An ideal code is far away from “spaghetti style”, so it’s about good practises of software architecture according to SOLID principles, design patterns and highly reusable components.
Ideal software is the best compromise, a golden middle between business needs and flexibility, simplicity, reusability, principles and best practises in software design.
This practically means “maximum code quality with less resource costs”!
A compromise is always a solution of a problem that satisfies at maximum the opposite parties and interests.
On the one hand sparing on quality leads to huge maintenance problems in future but on the other hand investing too much of ‘human hours’ into an ideally structured software product could be just a wasting of money as some features would never be used. Ideal software lies somewhere in the middle. Pragmatically saying, an ideal code is a result of work that spares resources (mostly developing time) in all predictable cases in the most optimal way. As you will see below, this formula can be applied to any existing understanding of ideal software design.
Let’s look at some attributes which make our code ideal.
Changes in the specifications require minimal changes in the code. It obviously spares implementation time. In some cases adding few lines of code can bring very powerful additions to the existing functionality. In other cases you don’t need to do a lot of work to meet the new specifications. But as we already said, this requires some kind of changes’ prediction. So the developer must be very careful with this ideal feature so he doesn’t spend huge amount of time on developing never used “plugin expectations”.
There is no universal recipe for achieving code flexibility as this mainly depends on the concrete requirements and company business model. We may achieve good flexibility:
- if our code structure and logical units (modules, functions, classes) are strictly corresponding to the business entities (domain model), their relations and properties. Therefore the code will evolve along side the changes of the business model.
- if our specifications include information about further plans of development of the mentioned features and requirements
- if we respect the Dependency Inversion Principle – in short we clearly separate high level units from concrete implementations allowing their flexible variation, and practically if your business logic requires sending emails, it should not be coupled with a concrete sending implementation (e.g. via smtp) but allows variation of sending methods
- if we always ask ourselves the question “What if this changes in the future, how much effort it will cost to adjust the implementation on the running system?”
- if we respect the Don’t Repeat Yourself Principle. Violating it means having repeating logical units, which is very hard to adjust when the requirements change.
- if we respect the Single Responsibility Principle avoiding “God” modules. In this case one responsibility corresponds a “single quantum of possible change” or simple business module, so if you want to change invoice generation, this should not affect email sending or order processing.
- we strictly separate code as business logic (high level), infrastructure, data (models, property objects, DTOs), ui and io (user interactions and processing/validating/preparing inputs and outputs)
This means doing as less code as possible and avoiding “inventing a wheel”. Reusable parts obviously spare a lot of implementation time. Achieving a high reusable code may seem to be easy but in reality developers tend to introduce a lot of scope specific solutions with many repetitions of abstract functionality, because it is easier and faster for the current task rather than in a general context.
Let’s list some of the recipes which help to achieve a better reusability
- It’s again following the SRP and DRY principles: repetition is the opposite of the reusability and having big “God” modules restrict possibility to reuse them in a different scope
- All infrastructure communications are potentially reusable – sending emails, saving files, storing data in cache etc
- Well organised team member communication – regular daily Stand-ups, discussing future tasks, presentation of features. All these increase the potential that the solution created by one developer may be reused by other team members in their tasks scope.
- Clear, understandable, logical code structure according to the business model. This means that the attempt to put a duplicated logic into some place in the code structure should lead to the discovery of the original module, e.g. if I want to write my own email sender I will quickly discover another email sender in the code structure and will try to reuse it instead of writing a new one.
- Culture of looking for already implemented logic – if a developer wants to write some new code he should always try to find anything that does the same. If he doesn’t find it, he should try to find a library and then write own code.
- Culture of writing/finding reusable libraries rather than putting “own wheels” into the common code base.
Code Readability & Simplicity
Let’s list some recipes and anti-patterns for it:
- Culture of writing code for others rather than for yourself. I should always ask myself if the next guy reading this code will understand it or even me trying to refactor it after some months.
- Clear naming of variables, functions, modules, classes and even packages. In a general case every name should correspond to the content or sense of the underlying code entity.
- Culture of writing simple and obvious code
- Usage of only common world conventional acronyms rather than inventing own “shortcuts” which no one from the outside world will understand
- Avoiding using of exotic and less common features of a programming language
- Common code base is not a show-case of own programming insights, over-engineered solutions or advanced math knowledge: if there is a simple (and maybe a bit boring) way of implementing a feature – go for it, rather than showing “how cool I can be”.
- Code meeting expectations – if I develop a feature which saves a file in a cloud, everyone would expect that inputs would be a source file location, destination info and maybe some additional options (e.g. login and password) and the output: some result info (success/failed flag, error message, file id) etc. Such kind of expectations are coming from the common sense of understanding how things in the computer world work. But if I meet a function which saves a file to a cloud without any inputs and outputs, I will be totally confused. Of course such kind of code design decreases the motivation of understanding it and of course reusing it in future.
- Precise tasks evaluations – in an environments with a very complex or messy code structure it’s sometimes very hard to give precise estimations for tasks complexity, as simple features might require much more time than expected because of way how things are done in the code.
There is a very good book about clean code and principles to program in simple, understandable and reusable way.
Ideal code is always testable. If you at some point decide to spare some time and do it later, you’re automatically starting going away from an ideal code. Tests increase the code culture in the most significant way and also make the mindset of a developer more pragmatic, logical and strict.
Test driven code approach has many advantages:
- The chance to deliver a completely not working code to production is approaching zero
- There is no need for manual testers in the company
- Developers plan their solution much in advance which gives a clear understanding of the implementation plan and better evaluation of the feature complexity
- Tests give simple use cases of code usage which increases understanding of some very complex code parts
- Doing code for a testing environment automatically gives an idea of different code levels and separation of concerns. If I am writing a function which besides the business logic sends an email, I would clearly separate the latter in an abstract code and replace the real email sending with some simulation because in a testing environment it will not be possible to send real emails.
- The changes of breaking an existing code as a side effect of some changes will be very low
- I feel much more secure of changing some core functionality of a working application, if I know that the tests would discover if my code changes break something else.
- Tests give idea of how well my code is designed. If I in a single test I try to assert too many different things about single code unit (class or function), it’s something wrong with the responsibility principle. If I cannot test a business logic because it requires some external infrastructure call (e.g. sending email), it’s something wrong with the dependency inversion.
Achieving an ideal code design is a very hard work, which requires not only the highly skilled developers in the team but also a lot of organisation and certain company culture. The most important is however that all team members are highly motivated to create a code product which is pleasant to work on. It’s like living in a house where each inhabitant takes care about cleanness, discipline and qualitative contribution to the common wealth. And the advantages of the possessing an ideal code base are huge: team members are highly motivated to stay in the company as probably no one wants to work in a messy overcomplicated code base in some other company, features and code fixes are implemented very quickly, learning curves are low for new team members and it’s almost impossible to break the production with a new code.