These are ways that I have found to work in improving maintainability.
Using design patterns correctly allows others to modify and extend the program within the scope and intention of the designer.
Let’s assume 2 similar complex functions in a program. The first function was created with an extension point via strategy pattern. The extension point allows others to use it and extend the program within the scope and boundary that exists inherently. When the business requested to extend the functionality, the maintainer only required to use the extension point. Several unit tests may already be available for the existing functionalities. The maintainer only requires to add a few new unit tests for the new extension.
The second function did not have any extension point. When the time comes to extend the functionality, the developer requires to open the function and modify the functionality directly. The original intention of the first writer dwindled every time this happened. There is no way to know confidently that the function would work as is. The maintainer would require to assess the new function individually and no longer within the scope of the previous developer. In this case, let’s assume that some unit tests are available for the existing functionality. As the maintainer requires to open and modify existing functionality, the existing unit tests are no longer valid and they all require to be replaced. The maintainer would have to then rewrite the unit test that tests previous functionality, as well as the new addition.
Knowing when to use a pattern is not an easy feat and it takes many practices of failure and error. The second scenario above is the most likely scenario that would occur. The first scenario could only happen by intention and may not always be successful. When it is successful however, the pay off is usually worth the effort. I have blogged about some design patterns such as the cascade pattern and command handler pattern previously.
The aim of having good readability is to help the maintainer understand the context and intention of the code written. It is to reduce the time required to debug, modify and extend the part of the application. The writer is required to convey information and ensure that their intention is clear. A high level code reader could usually discern hidden intention and information from any code. They are able to read code and understand to a deeper level the thought process of the writer. Not every maintainer however, is a high level code reader.
To ensure that code in an application is readable, it is important to have a consistent way to name classes, functions and variables. There are many guidelines on how to do this already such as the naming convention in Wikipedia. What is also important is that the team as a whole follows a standard that they agreed upon. Having a code review process also helps to reduce naming issues and increase readability.
Cyclomatic Complexity Rating
Looking into the cyclomatic complexity rating can be a good indicator on how readable the part of the function is. A good cyclomatic rating indicates that the part of the code contains only some amount of decision points which increases readability. Jason Roberts from dontcodetired blogged about this with code samples in more detail.
The aim for unit testing is to instill confidence when the code change during maintenance activity. By having “some” degree of unit testing, the maintainer would have some degree of confidence to modify existing code without breaking the existing functionalities. High level of confidence in this case is the key to continuous integration which leads to continuous deployement.
When being asked how much coverage is enough, Testivus on test coverage by Alberto Savoia resonates with me, and answers this nicely.
In summary, when there are no unit tests at all, aim to just start writing some tests. There will be a point that we have an overload of unit tests. When an application reaches this point, it is the time that we need to start assessing their values and how are we creating them. At the end of the day, as a rule of thumb, it is generally good to have 80% of code coverage.
Code coverage is the insurance for the future at the cost of maintainability.
Quality Unit Test
Whilst having a good degree of coverage should be the aim of every application, it is also necessary to aim for quality unit tests. Creating quality unit tests takes practice. There are many guides on creating quality tests already. In general, these are some attributes that good unit tests have:
- Fast: As unit tests are created to be repeatable, they require to be run very quickly as well. Developers could run unit tests several times in minutes to ensure that they don’t break existing functionalities.
- Reliable: Each test created must be deterministic. When a unit test run, it must produce exactly the same output every time. This is the key to instil confidence in unit tests.
- Independent: A unit test should only test one thing at a time and it should not rely on each other.
- Readable: Unit tests should be able to be understood and the intent should be clear. If the test fails it needs to convey information of which behaviour of the application that fails.
Knowing methods to measure and improve maintainability are a good start for any people working to improve their application. However, there are usually more challenges when someone or a group of people attempts to improve their software maintainability. The impact of these attempts can be negative and hinder the next attempt to improve maintainability.
In the next article we will be exploring these challenges in more detail.
- How to create maintainable application part 1 – codeconstruct
- How to create maintainable application part 2 – Measurement – codeconstruct
- Strategy pattern in .net – c-sharpcorner
- Testivus on Test Coverage – Artima Forum
- Using Cyclomatic Complexity as an Indicator of Clean Code – dontcodetired.com
- 10 tips to writing good unit tests – dzone
- How to write testable code and why it matters – Toptal
- What is good unit tests? 5 must haves – NDepend
- Unit Testing, Best Practices with .NET Core and .NET Standard – Microsoft
- Writing great unit tests: Best and Worst Practices – Steve Sanderson’s Blog
- JUnit Best Practices Guide – HowToDoInJava
- Querying Entities Interface – codeconstruct
- DRY-ing code with Command Handler – codeconstruct
- Naving Convention – Wikipedia