Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Accidental Complexity

0.00/5 (No votes)
15 Jun 2015CPOL7 min read 4.7K  
Accidental complexity

Accidental complexity is the entropy that exists in your system that is possible to eliminate. The opposite of this is essential complexity; the parts of a system that are required and cannot be simplified. These two concepts were discussed by Fred Brooks in his essay No Silver Bullet -- Essence and Accidents of Software Engineering. Many systems today are extremely complex, and any effort that can be done to eliminate complexity, should be.

There are a Lot of Should Be's...

I get very irritated with excuses. Because excuses just keep allowing the code to decay with each new statement that is added. I started writing a list of these excuses for entertainment, but I started to get irritated, then I stopped. Here is one of them, "In a perfect world..." There's really on one way that sentence ever ends.

Add Value, Not Garbage

Just because you have an excuse, even if it's a valid excuse, doesn't mean that you are off the hook for cleaning up messes. I remember a situation when I ran across a piece of code, and I thought that it looked peculiar.

C++

C++
(void)Sleep(k_one_second);

Casting a function call to void. I've never seen that before. There were no comments and I couldn't think of anything valuable to the code that is added by that cast. The Sleep function did return an integer, but the value is not being assigned. I scratched my head, and I deleted the cast.

Soon after my changes were being reviewed. There was a question entered that asked why I deleted the void cast. The comment then goes on to explain that cast was added to fix a defect reported by our static analysis tool.

I thought, "This is a fix?"

One of the rules in the coding standards we used, JSF++, is that all return values must be checked. This hardly qualifies as checking the return value, but it appeases the analysis tool.

I replied with "The reason why the tool is reporting an error is because all return values are supposed to be tested. This looks odd, there are no comments, and it's not even a fix." I immediately had two better ideas that would have required the same or less work, and be valuable changes:

  1. Change the functions signature to return void
  2. Encapsulate the original function in an inline function that returns void

Changes like this only add garbage to the code which reduces its readability. Even worse, it covers up a valid problem reported by the code analysis tool. Using subversive code tricks to prevent the tool from reporting a valid issue negates the value of using the tool. You should strive to make every statement add value to the code.

Several hundred instances of void casts were added in that situation. This only added more clutter that covered up a problem rather than focusing on finding a valid solution to fix the problem.

Simplify

Imagine that there is a specific feature that exists, and it will require a finite amount of logic to implement and use:

Logic required to implement and use a feature

Now imagine that functionality needs to be used in multiple locations. Generally we can simplify by factoring the common code into a function, or if it is more complicated logic it may even become a class.

Encapsulating part of the implementation

One particular problem that I encounter frequently is an abstraction that handles a minimum amount required for the implementation. This tends to leave much more logic for the caller than is necessary. More logic outside of this re-usable abstraction, means more duplicated logic; logic that can be written incorrectly, or even logic that does not properly initialize the feature.

Code left to caller opens door for errors

Can You Make It Simpler?

After you have simplified your code inside if your features abstraction, put it to the test and use it. Better yet, put it in a test, a unit-test. See if there is anything that you could actually take care of for the user with the known input.

Abstract more for the caller when possible

It is not always apparent, but this is accidental complexity. This is an example of a situation that could eliminate code that is duplicated.

This is unfortunate, because duplicated code is often performed with cut-and-paste, which is notoriously error-prone. This also adds more code to be maintained, which is also more code to read in order to understand what purpose a section of logic serves.

Consider the trade-offs

It is not always the best choice to continue to simplify. Simplifying the interface to a function usually means giving up flexibility. In most cases, there is no reason the choice must be either/or. Sometimes it is feasible and advantageous to do both.

Create the basic abstraction that leaves all of the flexibility and error-prone or cumbersome logic to the user. Then create simplified versions of the feature that handle commonly used cases.

Example

The cumbersome abstraction that I use as an example, is the ::GradientFill function from the Win32 API. I explored what this function is capable of a few years ago, and I learned that it is quite powerful. The interface provides a lot of flexibility, and it does not look too bad from a cursory inspection.

C++

C++
BOOL GdiGradientFill(
  __in  HDC hdc,            // Handle to the DC
  __in  PTRIVERTEX pVertex, // Array of points of the polygon
  __in  ULONG dwNumVertex,  // Size of the vertices array
  __in  PVOID pMesh,        // Array of mesh triangles to fill
  __in  ULONG dwNumMesh,    // Size of the mesh array
  __in  ULONG dwMode        // Gradient fill mode
); 

However, this function requires a lot of repetitive setup code. This is also the reason that I hardly ever used ::GradientFill up to that point. Here code from the MSDN documentation page for this function that is required to paint a horizontal and vertical gradient. I believe it would be simpler to write a for-loop than the setup that is required for this function:

C++

C++
TRIVERTEX vertex[2] ;
vertex[0].x     = 0;
vertex[0].y     = 0;
vertex[0].Red   = 0x0000;
vertex[0].Green = 0x8000;
vertex[0].Blue  = 0x8000;
vertex[0].Alpha = 0x0000;
 
vertex[1].x     = 300;
vertex[1].y     = 80;
vertex[1].Red   = 0x0000;
vertex[1].Green = 0xd000;
vertex[1].Blue  = 0xd000;
vertex[1].Alpha = 0x0000;
 
// Create a GRADIENT_RECT structure that
// references the TRIVERTEX vertices.
GRADIENT_RECT gRect;
gRect.UpperLeft  = 0;
gRect.LowerRight = 1;
 
::GdiGradientFill(hdc, vertex, 2, &gRect, 1, GRADIENT_FILL_RECT_H);
::GdiGradientFill(hdc, vertex, 2, &gRect, 1, GRADIENT_FILL_RECT_V); 

Image 5 Image 6

The code is even worse for a triangle.

I wanted to make these functions simpler to use in my code. To me, it should be as simple as a single function call to fill a rectangle. So I encapsulated the required code in the function below:

C++

C++
bool RectGradient(
  HDC hDC,        // The device context to write to.
  const RECT &rc, // The rectangle coordinates to fill with the gradient.
  COLORREF c1,    // The color to use at the start of the gradient.
  COLORREF c2,    // The color to use at the end of the gradient.
  BOOL isVertical)// true creates a vertical gradient, false a horizontal 

As I mentioned earlier, you often give up flexibility for convenience. One of the features that is given up from the previous function is the ability to control alpha-blend levels. Therefore, I created a second version of this rectangle gradient that allows alpha blending levels to be specified.

C++

C++
bool RectGradient(
  HDC hDC,                  
  const RECT &rc,          
  COLORREF c1,              
  COLORREF c2,              
  BOOL isVertical,          
  BYTE alpha1,    // Starting alpha level to associate with the gradient.
  BYTE alpha2     // Ending alpha level to associate with the gradient.
  ) 

These two functions could be used much more simply. Here is an example of how much simpler the code becomes:

C++

C++
// Horizontal Gradient
RECT rc = {0,0,300,80};
COLORREF black = RGB(0,0,0);
COLORREF blue  = RGB(0,0,0xff);
RectGradient(hdc, rc, black, blue, false);
 
// Let's draw a vertical gradient right beside the horizontal gradient:
::OffsetRect(&rc, 310, 0);
RectGradient(hdc, rc, black, blue, true); 

The Value of Many Small Abstractions Adds Up

Sometimes, the flexibility of the original code can still be accessible even when the code is simplified. This can be done with a collection of smaller abstractions. Utility functions like std::make_pair from the C++ Standard Library is one example. These functions can be used in series to simplify a series of declarations and initialization statements.

A series of small abstractions is valuable

In some cases, this collection of utility abstractions can be combined into a compound abstraction.

Abstractions can be grouped together

There are many ways that code can be simplified. It doesn't always need to be code that would be duplicated otherwise. If I run across a super-function, I will try to break it down into a few sub-routines. Even though this new function will only be called in a single location, I have abstracted the complexity of that logic at the call site.

It is even more feasible to give this function that is called only once a more cumbersome but descriptive name. When reading code from the original super-function, it is now much easier to ignore large blocks of code that obfuscate the intended purpose of the function.

Abstraction simplifies code, as long as it provides value, continue to abstract

While the code may be necessary, that does not mean that it must make the code around it more difficult to read and maintain.

Summary

Accidental Complexity is the code that exists in our programs that we can simplify or eliminate. Duplicated code and code that exists only to get rid of warnings are two examples of accidental complexity. The best case scenario, the code becomes just a little bit more difficult to maintain. However, in the worst cases, legitimate issues could be covered up. Worse still, the manner that they were covered up makes them that much more difficult to find if they are the cause of a real problem.

I witness developers exerting a great deal of effort to work around problems, fix the symptoms or even make excuses and ignore the problems. This time could just as easily be focused on finding a fix that actually provides value. Much more quality software would exist if this were always the case.

...but I suppose this is not a perfect world.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)