Chapter 12. Avoiding Unintentional Debt

In this chapter, we summarize software engineering practices that any team should incorporate into its software development activities to minimize unintentional technical debt. These practices are essential for organizations and teams to institutionalize an integrated approach to managing technical debt.

Software Engineering in a Nutshell

Managing technical debt requires a broad understanding of software engineering practices—and that is exactly the goal of this chapter: providing starting points for practices that are essential for establishing a well-rounded approach to technical debt management so you can spend your time on strategic technical debt rather than fighting avoidable fires. Because these practices are described in many software development books, we only summarize them here and explain how they support technical debt management or how they relate to technical debt.

Not using sound and proven practices to run a software engineering project is likely to bring you a lot of technical debt. We discussed aspects of this phenomenon in detail when we teased apart the causes of technical debt in Chapter 10, “What Causes Technical Debt?” More importantly, using recommended software engineering practices will help you avoid violating the key principles of technical debt that we’ve introduced in this book.

If you do not institutionalize good coding standards and code quality checking practices, in time your code will inevitably degrade. You will start getting lost in accumulated defects. Your architecture will also eventually start degrading.

If you do not know your architectural decisions and trade-offs and review them continuously, you will not react to architectural changes in a timely way. You will not be able to determine what to fix, where to fix it, or what caused the issue in the first place. Keep in mind these two principles:

If you do not know your short-term and long-term organizational and project goals and do not institute practices to establish a roadmap toward them, you will get caught in the “blame game.” As we pointed out in Chapter 3, “Moons of Saturn—The Crucial Role of Context,” only the most trivial systems escape technical debt, and it is better to manage it deliberately than to have it manage you accidentally. Keep in mind this additional principle:

Good coding, architecture, and production practices are essential components of good software engineering and lead to greater responsiveness to business needs and quality code that is easier to evolve and maintain. Making your software “observable” in some way—through techniques such as static code analysis, monitoring, and logging—will allow you to collect data and use it to interpret system behavior and how it correlates with the evolution and maintenance challenges you experience. We take a deeper look at these practices next.

Code Quality and Unintentional Technical Debt

The following four fundamental practices are critical for creating high-quality and maintainable code:

  • Establishing and following sound coding standards

  • Establishing and following secure coding standards

  • Writing maintainable code

  • Refactoring

If you abandon fundamental principles of software craftsmanship, your code will drown in recurring interest payments throughout the life of the project.

Sound Coding Standards

Coding standards are guidelines for specific programming languages that recommend programming style, practices, and methods for each aspect of a program written in that language. The most common form of scattered and unintentional technical debt results from not following such coding standards.

Most software development organizations adopt some form of coding standard that specifies acceptable and objectionable code idioms. These standards are development language specific. Their main objectives are as follows:

  • Increasing programmers’ and maintainers’ understanding of the code

  • Avoiding common coding mistakes

  • Preventing the use of dangerous, error-prone, or costly forms of implementation constructs

These guidelines include naming conventions, formatting of code, and permissible language constructs. Other areas of concern include file organization and documentation in the form of comments to improve understandability of the overall codebase. Examples of commenting guidelines include the minimum amount of documentation for every public class and public method and what does not need comments within the code. An effective style guideline often describes phrases that avoid confusion and key phrases that increase ease of navigation. “Basics” go a long way, especially in projects where large teams need to be orchestrated, such as when establishing and following naming conventions for public, private, and protected attributes, classes, and method calls.

Integrated development environments help enforce standards and style guides. All the developers on your team should be intimately familiar with and follow the standards and style guides to be used for the project. These can be company specific, or you can adopt an industry practice, such as Google Java Standard Guide or Oracle Code Conventions for the Java Programming Language.

Secure Coding Standards

Secure coding is the practice of developing software in a way that guards against the accidental introduction of logic flaws and implementation mistakes that result in commonly exploited software vulnerabilities. A combination of security issues, especially when caught late, will accumulate and become technical debt.

The timeline of the Phoebe team mirrors the journey of many teams we interact with. As their product matured, team members started to realize that they would have to do more to demonstrate the security aspects of the product, especially given the needs of their government customers. In anticipation of coming requirements, they decided to be proactive and run a security analysis tool through the codebase. They added several technical stories and tasks to their backlog:

Task: Execute security scan on Phoebe code and document findings.

Technical story: As a Phoebe developer, I want to resolve all the security scan findings with Critical or High priorities.

Technical story: As a Phoebe contributor, I want to address all Medium and Low security scan issues so that the code quality is improved.

Team Phoebe elected to use a security scanning tool called Fortify, which offers features in static and dynamic application security testing through automating the checking of conformance to secure coding standards based on commonly found security issues. One reason for selecting Fortify was the fact that Phoebe was implemented with Java, with extensive use of J2EE libraries for which Fortify offered up-to-date conformance checks at the time.

As a result of the security scan, team members added 69 more issues to the backlog. In isolation, none of these issues were technical debt. Indeed, most of the issues were minor. However, when analyzed together, it became clear that the Phoebe project had technical debt related to security in the code. A significant number of the issues that were returned by this scan included poor error handling where null pointer exceptions were not caught properly or exceptions were not thrown or caught properly. These were symptomatic of an underlying design limitation in the treatment of exception handling. The list of violations included other common examples, such as the following:

J2EE bad practice:

Leftover debug code

Poor error handling:

Overly broad throws

Poor logging practice:

Use of a system output stream

Poor style:

Value never read

Non-final public static field

Confusing naming

Redundant null check

All these vulnerabilities in time will create security risks that can crash the system, be exploited, or both. After team members addressed these issues, they also educated the rest of the team to follow secure coding practices.

A number of resources can guide you in improving your secure coding practices. For example, the Open Web Application Security Project maintains a document that summarizes secure coding rules and practices. Some of these resources provide general guidance, such as “protect server side code being downloaded by a user,” without specifying the kind of protection mechanism to use. Others enforce very specific rules, such as those included in the SEI CERT Secure Coding Standards. There are also tools that implement these and other rules. The MITRE Corporation maintains a universal Common Weakness Enumeration (CWE) database as well as a Common Vulnerabilities and Exposures (CVE) database. These are only some of the ample resources available to educate your teams in secure coding and help them implement best practices. Teams should review secure coding practices at the beginning of a project, when they are establishing coding standards.

But secure coding gets a bit tricky from the perspective of technical debt. Security often gets top priority, and when such issues are found, they are the first to be fixed. Sometimes these are random patches that introduce technical debt. Treating each issue in isolation will often not address the technical debt. Often combinations of security issues that relate to architectural design consequences both create technical debt items and constrain the approaches to fix them. Not following known secure coding practices and standards increases the odds of introducing technical debt to the system, and it will make finding the roots of problems harder as the system grows.

Maintainable Code

Maintainable code and architecting for maintainability are closely related to each other. Following well-established best practices will enhance code maintainability, such as establishing common criteria for class sizes, guiding the use of external libraries, and selecting architectural patterns that promote maintainability.

The ISO/IEC 25000 standard (which evolved from ISO 9126) on software product quality describes system quality characteristics. Maintainability incorporates such concepts as changeability, modularity, understandability, testability, and reusability. Many source code properties affect maintainability. Characteristics that are relevant to maintainable code include unit size, unit complexity, unit interface, duplication, coverage, coupling, cycles, propagation, and types of dependencies. Units can be groupings defined by the artifacts in the development environment (such as lines of code and the number of files, directories, packages, or projects) or semantic constructs of the software asset (such as functions, blocks, classes, statements, and accessors). Organizations such as the Object Management Group and Consortium for IT Quality have recommended standards specifically related to maintainability.

Writing maintainable code is part of developing high-quality code. In the same way, understanding maintainability is part of architecting the system. Establishing clear baselines for these practices will help you avoid the kind of technical debt that is most commonly seen and most costly, yet least likely to be fixed.

Refactoring

Refactoring is a behavior-preserving transformation that improves the overall code quality. Refactoring is not simply cleaning up code; it is a technique involving applying known patterns of improvement. While each refactoring does a little, a series of transformations can introduce both needed restructuring and improvements in code quality and complexity. Basic refactoring guidance is widely available. There are resources that describe how to make small, local transformations and some tools that implement them. There are also catalogs of generic refactoring patterns as well as catalogs of patterns specific to programming languages.

Before refactoring code, you need a solid set of automated unit tests. The unit tests should pass both before and after the code has been refactored. Unit tests safeguard against introducing new issues unintentionally. Savvy developers ensure that unit tests are used and passed during refactoring activities.

The Atlas team relied on refactoring to manage its technical debt within short iteration cycles. Here are two of the team’s technical debt items:

Atlas #102: Placeholder: I changed the code and made the tests pass, but the tests are not testing the code. I will fix this tomorrow.

Atlas #623: We should create a toolbar superclass /ui/toolbar/bottom_toolbar.mm. And reading_list_toolbar.mm, clear_browsing_bar, and bookmark_context_bar should be based on the superclass. This way, we can reduce the redundant code and technical debt and make sure the style, font, and spacing of the toolbars are always consistent.

In the first example, the developer knew that she made the code work after refactoring but that she also introduced another problem. She created a technical debt item to alert everyone on the team, assigned it to herself, and went back and fixed it the next day. In the second example, the developer had a solution, opened a technical debt item, and described its benefits along with the recommended refactoring.

Refactoring is an approach commonly used by teams to bundle known technical debt issues with other changes and reduce them as code is improved. While refactoring does not resolve deeply rooted architectural issues, it can be an effective technique for improving maintainability and code quality and eliminating some common problems before they become costly.

Architecture, Production, and Unintentional Technical Debt

We have established that the most expensive technical debt is at the architecture level. Today, a good architecture practice can be summarized as a deliberate and continuous focus on architecture issues, not a massive up-front design. Architecture design is not a point in time but an activity that is integrated with a project and that may continue while the system is in operation. Your choices of technology, frameworks, integration, and deployment pipeline will all encapsulate architectural decisions and enable or hinder quality attribute requirements. Here we call out some practices that are essential to understanding the design trade-offs that are part of architecting:

  • Eliciting quality attribute requirements that drive the software design and quality

  • Incorporating iterative incremental design into release planning

  • Aligning the architecture and production infrastructure

  • Documenting to address stakeholder needs

  • Incorporating lightweight analysis and conformance checking throughout

Quality Attribute Requirements

Producing high-quality systems and managing their technical debt closely depend on understanding their architecturally significant requirements. Quality attribute requirements are the architecturally significant requirements for the system that affect its run-time behavior, system design, and long-term evolvability. There is no shortage of taxonomies and definitions to guide you in requirements specification (for example, IEEE 830-1998: Recommended Practice for Software Requirements Specifications).

Establishing a common understanding of quality attribute requirements allows teams to design for them and, more importantly, understand the short-term and long-term architectural weakest links. Designing for these requirements that drive the system structure and behavior is often an ad hoc practice. Organizations often do not allow time to explicitly focus on quality attribute requirements, and this can result in significant amounts of technical debt as the project progresses. Designing with security, scalability, and maintainability in mind is not a trivial task.

Several established techniques can augment existing team requirement management practices that focus on quality attribute requirements. Those that are elicited from key stakeholders and represented as scenarios provide a quantifiable definition and specific prioritization of the architecturally significant requirements. Agile software development processes can incorporate quality attribute requirements as user stories when a system’s run-time qualities are visible to the user or as technical stories when a team is focused on internal structural issues.

Iterative, Incremental Design in Release Planning

Reasoning about architecture alternatives and using the architecture to guide implementation choices during release planning provide opportunities for handling technical debt strategically. Explicitly defining tasks related to realizing quality attribute requirements in development iterations and release planning is key. Failing to allocate time blocks for architecting is a recipe for unintentional technical debt.

Modern software development approaches acknowledge the critical and strategic importance of architecting. For example, the Scaled Agile Framework (SAFe®) defines the architectural runway as the production infrastructure, architecture, and code that are essential for near-term features and functionality. It recommends allocating time in sprints to create and extend the runway as needed to support the development of the features that depend on it.

Systems with a smaller scope and smaller teams, such as Atlas, may need a shorter architectural runway. Especially in the face of uncertain requirements for technology or features, it may be more efficient for the team to try something out, get feedback, and refactor as needed than to invest more time in trying to discern requirements that are in flux.

Systems with a larger scope and larger teams, such as Tethys, need a longer runway. Building infrastructure and re-architecting the software take longer than a single iteration or release cycle. Delivering planned functionality is more predictable when the structure for the new features is already in place. This requires looking ahead in the planning process and investing in architecture work in the present iteration that will support future features that the customer needs.

An explicit focus on allocating architecture tasks driven by quality attribute requirements will support the development team in making design trade-offs wisely and taking on technical debt strategically. Understanding the state of a development effort focuses teams on architectural design. It is desirable for development teams to reach a software development tempo in which each release delivers value as new functionality or improvement to the stakeholders. Initially this state does not exist. Teams need to build platforms and frameworks, establish architectural patterns, and make decisions about structure and its implementation.

A key enabler to achieving iterative, incremental design requires the following:

  • Understanding the short-term and long-term goals of the business and, therefore, the key quality attribute requirements: Quantitative response measures and priorities for quality attribute requirements will help teams establish design strategies for these requirements.

  • Eliciting quality attributes as early as possible in the project: They should be prioritized based on technical difficulty and value to the business and revisited at least at each release point.

  • Understanding the dependencies between technical constraints, products used, and these requirements: This is an ongoing activity because dependencies are often not immediately apparent as the devil is in the details. Lightweight analysis approaches incorporated into sprint retrospectives will help uncover these dependencies.

Aligning the Architecture and Production Infrastructure

Another essential aspect of the architectural runway is the production infrastructure and the tooling needed to achieve continuous integration, continuous deployment, and monitoring. Recognizing how the software aligns with the release process and production infrastructure will make continuous delivery and its tooling easier to achieve. At a minimum, employing parameterization, self-monitoring, and selfinitiating version updates will enable teams to avoid technical debt in production environments:

  • Parameterization focuses on environmental variables relevant to the production infrastructure, such as databases and server names. It allows a team to defer binding time and change aspects of the build and production environment without having to change the build.

  • Self-monitoring allows for monitoring the system performance and faults as it runs and when it gets out of sync. Both the production infrastructure and the architecture of the system can take advantage of load balancing, logging, and redundancy tactics to realign the allocation and improve system behavior.

  • Self-initiated version updating allows a team to run scripts that update the ­relevant versions of the software in production. Versioning becomes an issue particularly at scale and when continuous integration and deployment is a goal. The clients and the main applications may get out of sync, as may the supporting tooling environment.

Documentation

For many systems, some documentation exists, but it has rapidly become disconnected from the running software. Under schedule pressure, it is all too common for a team to jettison updates to the documentation and use that time to fix one last defect. Consequently, documentation suffers from several problems:

  • It rarely helps authors immediately (“I know this, and I can remember it for several weeks or months”), so they have no immediate incentive to spend the time and effort writing documentation.

  • What is obvious to one developer may be counterintuitive to another.

  • Diagrams take time to create and are tedious to update, even though they provide high value for the reader.

  • Documentation is not trusted because it is assumed to be out of date. For some organizations, this is a cultural issue.

Make sure you document what is actually useful. Developers can read the code, so do not create massive amounts of documentation that just paraphrases what is in the code. However, new developers may have a hard time understanding a large body of code, and they can benefit from “roadmaps” to help them navigate the code and get the big picture of how it works. They also need explanations of key design decisions, so they can integrate this original reasoning into their own designs. This is the role of a software or system architecture document, along with some accompanying design guidelines. The architecture document should include documentation about key interfaces in the system: the APIs. Another key document should describe the development process, from end to end, including production.

Project management discipline is the key to writing and maintaining documentation. Here are a few heuristics for deciding what documents a development team should produce and how to maintain them:

  • No write-only documents: If no one will ever use it, do not waste time developing it and maintaining it.

  • Single point of maintenance: Do not force developers to change information in multiple places. Part of the documents can be generated by tools; for example, diagrams representing the structure can be “decompiled” from the code.

  • Version control: Documentation should be under configuration management, just like the rest of the system.

  • Mandatory updates: Release to production should be blocked if vital documentation steps are not completed.

Lightweight Analysis and Conformance

Analyzing the codebase for conformance to the architecture and design should be part of the routine iteration and sprint reviews. A focus on quality attribute requirements will provide the goals to be met and a strategic perspective on design trade-offs. If appropriate, listing the trade-offs and risks identified by analysis as technical debt items will provide additional elements to monitor and manage on the product backlog.

At a minimum, the team should establish the following:

  • Module interfaces and responsibilities

  • Conformance guidelines, from module to code

  • Key design decisions, architecture decisions, and technical constraints

Lightweight analysis allows a team to assess the trade-offs that may turn into technical debt. Every architecture approach used to improve one quality attribute can negatively impact others:

  • Putting everything that needs to change in one place may introduce unnecessary dependencies to other components. This is bad for security and other types of changes.

  • Data structures with generic interfaces may impose a performance penalty.

  • Versioned interfaces increase complexity, which is more difficult to test and increases the chance of system crashes.

The team needs to be aware of these issues, mitigate them, and document them. Lightweight review of the system with regard to quality attribute requirements uncovers such issues early and gives the team opportunities to address them before they become technical debt or to explicitly take them on as intentional technical debt items.

At a minimum, the team should understand the principles of lightweight architecture and design analysis:

  • Important quality attribute properties of the architecture need to be evaluated. The important qualities are derived from the business goals.

  • Quality attribute scenarios translate business goals into required quality attribute properties.

  • Quality attribute scenarios help identify relevant components of the architecture to analyze.

  • Architectural approaches with their quality attribute properties should be clear to the team, as should the side effects and trade-offs of those architectural approaches.

  • Mismatches between architecture properties and scenarios become risks to the business goals and potential technical debt items.

What Can You Do Today?

Not following established software engineering practices will result in reckless and unintentional technical debt. This chapter highlights essential practices that you can incorporate into any development effort. Doing so will help you avoid unintentional technical debt and take on intentional debt strategically. Embrace and educate your teams continuously on bread-and-butter software engineering practices such as code review, unit testing, and coding standards. And consider automating these practices, including static analysis of the codebase.

For Further Reading

The concept of the architectural runway is a key practice in SAFe (Leffingwell 2007). In addition, Stephany Bellomo et al. (2014) describe how to “agilely” design an architecture, an approach in which reducing the Big Up-Front Design should minimize the premature technical debt that is incurred.

Adopting the right software development practices for your project will definitely have a payoff in the long run. For example, Forsgren, Humble, and Kim emphasize the benefits of adopting a DevOps practice in their 2017 DORA report.

Refactoring existing code enables a development team to get ahead of unintentional technical debt. Scott Ambler (2017) lists this as one of his 11 strategies for dealing with technical debt as well. Three books provide in-depth review of related strategies: Martin Fowler's Refactoring (2018), Joshua Kerievski's Refactoring to Patterns (2004), and Michael Feathers' Working Effectively with Legacy Code (2004).

Meeting high standards and enforcing good software craftsmanship are increasingly important in our software-intensive world. The ideas presented in this chapter are embodied in work that discusses how developers should understand and enforce high standards for their implementation practices. Robert Martin's Clean Code: A Handbook of Agile Software Craftsmanship (2008) explains the fundamentals of writing clean, maintainable code and provides examples. See also Sandro Mancuso's The Software Craftsman: Professionalism, Pragmatism, Pride (2014).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.163.197