Software maintainability is implicitly at the core of the technology industry. Unfortunately, the maintainability factor is pushed off to the sidelines by all but those immediately impacted by the lack of it, as something that “needlessly” pushes already stretched deadlines out and hence should not be focused on. It’s labeled as “nice to have” and we have sparing and mostly incoherent guidelines and even fewer benchmarks. Organizations consciously and subconsciously lean toward software tools and frameworks providing some levels of expediency and maintainability despite many shortcomings of generic tools used without strong guidelines. We’ll discuss the subject here in some cursory detail. . .
So, what is maintainability? According to ISO/IEC 14764
“Software maintenance in software engineering is the modification of a software product after delivery to correct faults, to improve performance or other attributes, or to adapt the product to a modified environment”
This definition identifies fault tolerance, performance, and adaptability as the three pillars of maintainability. We’ll slightly adjust this definition to prepare the grounds for some level of quantification. So, let’s take it to the next level and produce a quantitative analysis of a comprehensive array of entities responsible for impacting software maintainability, so, we may begin to associate some bottom-line figures. Let’s define it as the target of our objective:
“Software maintainability is a quantitative index that represents the degree to which an arbitrary codebase is given to stability, usability and enhancement”
The three core entities of this definition are Usability, Stability, and Enhancement. Let’s drill down further to produce a list of concrete actionable items.
- Usability
- Use enumerations instead of constants where possible, with attributes if necessary to associate one or more string or other values with the enumeration items
- Keep the API relevant
- Encapsulate appropriately, e.g., Customer class should aggregate InvoiceCollection while avoiding associating Inventory. At the same time Invoice should update itself, while Customer may expose API to update Invoice, although preferably only just expose invoices, but definitely should not assume the responsibility of actually executing an invoice update
- Simplify the API
- Use naming convention effectively
- Use a consistent naming convention
- Use contextually relevant names
- Name actions with verbs whenever applicable
- Keep the client of your API in mind while naming exposed members and keep support team (including yourself in future) in mind while naming unexposed members
- Reduce API noise
- Minimize call to action ratio for the public API. Maintain single public call per action, whenever possible
- Tightly control scope, make everything private unless otherwise required and then broaden scope incrementally only as necessary
- Reduce confusion
- Don’t support static builders and constructors at the same scope. Use static build methods, e.g., create, build, etc. instead of constructor only if the resultant object may be a subclass or if the object instantiation must be controlled remotely
- Avoid using Type as postfix, e.g., CustomerCategory instead of CustomerType
- Raise fault tolerance
- Implement general data validation mechanism
- Explicitly handle null references, in parameters and otherwise. For example, line 1 creates an instance of Customer and line 2 accesses its InvoiceCollection property. If line 1 produced a null customer line 2 will automatically throw an exception. If this potential exists, handle it and/or re-throw with a more meaningful exception and/or direction
- Handle out of bound value types, in parameters and otherwise. Throw an exception when a character type that can only be ‘E’ or ‘G’ has some other value. This will force a resolution at the source instead of propagating it along and result in a bug elsewhere
- Use generics wherever possible to avoid frequent type checking, among other things
- Have a unified logging and exception handling mechanism, because it helps you be proactive and gives your team a good operational framework for improving fault tolerance and customer service
- Manage memory to minimize leaks
- Implement IDisposable interface if an object composites unmanaged objects and dispose everything that implements IDisposable in .Net
- Explicitly and comprehensively reclaim memory in a non garbage collected environment
- Provide reasonable performance. Performance is not crucial for stability, unless it’s noticeably bad which associates it in a broader negative context, rightfully or not
- Pass by value with caution. This will generally be safer than passing by reference, because the original object is not exposed to accidental mutation but it slightly reduces performance due to replication
- Use StringBuilder for text manipulation, use for instead of foreach, etc., in .Net
- Cache and lazy-load, as applicable within a given context
- Raise adaptability
- Couple loosely to external platforms, e.g., via configuration, brokers, etc. You may use SQL server as the data store, but your library doesn’t need to be aware of that. You may completely decouple it with an object broker library, e.g., serviced objects, etc. or isolate the coupling with a gateway class
- Normalize at the source
- Minimize logic duplication when possible
- Data should be cleaned up at the source and not in the business logic because different types of data cleansing would be required at different logic points cluttering business logic and temporarily suppressing the need for data cleanup further deferring and cumulating the problem
- Keep the rules as close to the data as possible to avoid rules redundancy. Avoid implementing business logic on the UI
- Use design patterns to simplify your code and raise reusability
- Value type defaults, value type bounds, textual content, external data access configuration elements should be isolated and made configurable
- Use predefined constants instead of in-line value types, so, meaning in addition to context may be associated with them which may then pave the way to moving them to persisted medium, e.g., resource file, database table, etc.
- Do not suppress exceptions independently. Doing so raises the probably of unintended consequences
- Provide explicit API overloads to suppress specific exceptions, if necessary
- Generally have UI and not libraries handle exceptions
- Refactor. The simplest option often lacks design considerations, potentially propagating logic redundancy
- Abstract soberly. Degree of generality of objects is directly proportional to usage flexibility and directly proportional to cyclomatic complexity of required supporting framework. Abstraction initially raises complexity, which follows the bell curve, so, past a critical level of code volume abstraction will reduce complexity in a given context
- General rule of thumb for class length, 200 lines or less. Rule of thumb for method length, 20 lines or less. Any longer and you should reevaluate the purpose and code coherency, god class doesn’t support enhance-ability or general understanding
We’ve created a list of tangible items that we may explicitly and arbitrarily quantify in a standard weighted format. We’ll attempt at doing just that in our next iteration of the series.