On the Dilemma of Software Complexity

Core Challenge of Software Architecture Is the Growing Complexity

The larger a system is, the more important it is for software designers to ensure its simplicity.

Image source: https://divante.com/blog/10-companies-that-implemented-the-microservice-architecture-and-paved-the-way-for-others/
Source: http://themetapicture.com/the-life-of-a-software-engineer/

Why Does Software Complexity Grow Rapidly?

To understand the root cause for the rapid increase in software complexity, it is necessary to understand where software comes from. First of all, we have to answer the question whether a large software grows or is built.

1. Software Is Not Built, It Grows

Software is not built or even designed. Software grows up.

2. The Core Challenge of Large Software Is Understanding and Maintenance Costs in Its Growth Process

The core feature of a complex software system is that it is developed and maintained by many engineers. The essence of software is that engineers use programming languages to communicate abstract and complex concepts, but not human and machine communication, as Brooks clearly stated in [2].

  • When we find that our system has many problems, do not blame the original designer. The system does not become that complex in one day. Complexity is incremental [1].

Two Dimensions of Software Complexity: Cognitive Load and Collaboration Costs

Based on our analysis and understanding of the reasons for the rapid increase in software complexity, we naturally hope to solve this seemingly eternal challenge. Before doing so, we still need to analyze clearly the following question: What is complexity and how to measure it?

  • Collaboration costs: the extra costs of collaboration required when a team maintains software
  • Existing team members will probably be abandoned and new members will be recruited to start a new project. The original investment will be wasted. Even worse, the code will be abandoned but cannot be phased out. It will eventually cause severe consequences sometime.

Factors Affecting Cognitive Load

Cognitive load includes:

  • Degree of logics matching thinking habits: differences between positive and negative logics, logic nesting and independent atomization combination, differences between inheritance and composition.

1. Cognitive Costs Caused by Improper Logic

Look at the following cases from Google testing blog [7]:

response = server.Call(request)if response.GetStatus() == RPC.OK:
if response.GetAuthorizedUser():
if response.GetEnc() == 'utf-8':
if response.GetRows():
vals = [ParseRow(r) for r in
response.GetRows()]
avg = sum(vals) / len(vals)
return avg, vals
else:
raise EmptyError()
else:
raise AuthError('unauthorized')
else:
raise ValueError('wrong encoding')
else:
raise RpcError(response.GetStatus())
response = server.Call(request)if response.GetStatus() != RPC.OK:
raise RpcError(response.GetStatus())
if not response.GetAuthorizedUser():
raise ValueError('wrong encoding')
if response.GetEnc() != 'utf-8':
raise AuthError('unauthorized')
if not response.GetRows():
raise EmptyError()
vals = [ParseRow(r) for r in
response.GetRows()]
avg = sum(vals) / len(vals)
return avg, vals

2. Model Mismatch Brings High Cognitive Load

The software model design must match the cognition of the real world. Otherwise it will bring very high cognitive costs. I have encountered the design of such a resource management system. The designer has a very elegant model from the mathematical perspective: resource accounts expressed by contracts (the left section of the following figure). The account balance is obtained by accumulating past contracts to ensure data consistency. However, such a design is completely inconsistent with user cognition. Users only feel accounts and transactions, instead of contracts with complex parameters. Such a design brings very high maintenance costs.

3. Improper API Design

The following is the typical understanding costs of an improper API design from Google testing blog.

class BufferBadDesign {  explicit Buffer(int size);// Create a buffer with given sized slots
void AddSlots(int num);// Expand the slots by `num`
// Add a value to the end of stack, and the caller need to
// ensure that there is at least one empty slot in the stack before
// calling insert
void Insert(int value);
int getNumberOfEmptySlots(); // return the number of empty slots
}
class Buffer {  
explicit Buffer(int size); // Create a buffer with given sized slots
// Add a value to the end of buffer. New slots are added
// if necessary.
void Insert(int value);
}
  • To exit a module, the caller must perform a clear operation or call the finalizer after usage.
  • The caller has many ways to achieve exactly the same function in a module: during the software maintenance process, this situation may occur because of the redundancy caused by the improper initial design and subsequent modifications, original design defects. In any case this module is not good.

4. A Simple Modification to Be Updated in Multiple Places

A simple modification in multiple places is also a common factor increasing software maintenance complexity. It mainly affects our cognitive load: maintaining and modifying code requires a lot of effort to ensure that modifications are made in all places.

5. Naming

The naming of APIs, methods and variables in the software is very important for understanding the logic and scope of the code. It is also essential for the designer to clearly convey the intention. However, in many projects, we did not pay enough attention to naming.

6. Not Knowing What Modifications Need to be Made to a Simple Feature or Impacts of a Simple Change

This is the worst case among all the manifestations of cognitive complexity. Unfortunately, everyone has encountered such a situation.

  • The code has behaviors or boundary conditions that are hidden or not easy to be discovered, which is inconsistent with the descriptions in documents and APIs.

7. Low Cognitive Costs Mean No Mistakes Rather Than Blind Simplification

From the perspective of cognitive costs, we also need to consider the cognitive costs to measure different schemes or writing. Superficial simplification may lead to substantial increase in complexity.

// Time period in seconds.
void someFunction(int timePeriod);
// time period using Duration.
void someFunction(Duration timePeriod);

Factors Affecting Collaboration Costs

Collaboration costs refers to the collaboration costs required to add a module. Collaboration costs include:

  • Testing and release need collaboration and synchronization.

1. System Module Splitting and Team Boundaries

In the microservice age, module/service splitting and team alignment are conducive to iteration efficiency. The splitting of modules and non-alignment of boundaries increase the complexity of code maintenance. Then new features require joint development, testing, and iteration of multiple teams.

2. Dependencies between Services — Composition vs Inheritance/Plugin

Common service dependency modes include composition and inheritance. These modes exist for dependencies between local modules or classes or remote calls.

3. Collaboration Costs Caused by Insufficient Testability

The code delivered to other teams (including the test team) must have sufficient unit tests and good encapsulation and API description, and be easy to be integrated and tested. However, due to insufficient unit tests or module tests, the complexity, failure rate, and rework rate at the integration phase increase. This boosts collaboration costs. Therefore, the key to reducing collaboration costs and improving iteration efficiency is to perform sufficient unit tests and provide excellent supports for integration tests.

4. Documentation

To reduce collaboration costs, engineers also must provide clear, constantly updated, and consistent documentation for APIs, and clearly describe the scenarios and usage of APIs. These tasks require effort input and sometimes development teams are unwilling to do. However, if users must rely on DingTalk/Slack requests or PR articles, the collaboration costs are too high and the probability of bugs/improper use for the system greatly increases.

  • Documentation (README.md or *.md) is written, delivered, and updated together with code.

Software Complexity Lifecycle

Good Enough vs. Perfect

In the software realm, we say “good enough” to balance efficiency and quality. This theory is right because excessive pursuit of perfection compromises efficiency. In most cases, our systems are just Good enough but far from perfect.

Countermeasures for Growing Complexity

The introduction of new code increases the complexity of the system: When a class or method is created, it is referenced or called by other code snippets. This leads to dependencies or coupling and increases the system complexity (unless the previous code is excessively complex, complexity can be reduced through refactoring). If you all notice this problem and can recognize those key factors that increase the complexity, this article serves its purpose. However, how to keep a system simple is a very big topic and it will not be explored in this article.

  • When we do not take zero tolerance in code and design review, each hack or design product does not too much extra cost and complexity. However, each failed system is deteriorated gradually.
  • Broken window effect: When a broken window of a building is not repaired in time, the building will be considered uninhabited and then invaded. More windows will be intentionally broken and the building will soon be dilapidated. The broken window effect is very appropriate for software quality control. So, don’t live with broken windows (bad designs, wrong decisions, or poor code): Fix broken windows as soon as possible.
  • We can all make difference: Our code is the most fair place to take action and we can make difference if we want.

Reference

[1] John Ousterhout, A Philosophy of software design
[2] Frederick Brooks, No Silver Bullet — essence and accident in software engineering
[3] Robert Martin, Clean Architecture
[4] https://medium.com/monsterculture/getting-your-software-architecture-right-89287a980f1b
[5] API design best practices https://developer.aliyun.com/article/701810
[6] Andrew Hunt and David Thomas, The pragmatic programmer: from Journeyman to master
[7] https://testing.googleblog.com/2017/06/code-health-reduce-nesting-reduce.html
[8] https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
[9] http://www.multunus.com/blog/2017/01/naming-the-hardest-software/
[10] https://martinfowler.com/bliki/TwoHardThings.html

Original Source:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store