[comp.software-eng] examples complexity measure usage

marc@apollo.uucp (Marc Gibian) (01/09/88)

As promised, here are some examples of decreasing the quality of code
while reducing the complexity measure value.  The complexity measure
used was Raytheon/ED/SSL-s version of the McCabe metric.  They made
one modification to the algorithm because they judged that they did
not want to count each case in a switch as an increment in complexity.
The contractual requirement was that every procedure and function
have a McCabe value less than or equal to 10.  I must admit that since
I know longer work for them, the examples I am providing are not being
extracted from product libraries.  Instead, they are constructed examples
describing the things that were actually done to the product I worked on
while at Raytheon.

Example 1:

original fragment:

   if (a == value1) then
      y = target1;
   else if (b == value2) then
      y = target2;
   else if (c == value3) then
      y = target3;
   else if (d == value4) then
      y = target4;
   else if (e == value5) then
      y = target5;
   else if (f == value6) then
      y = target6;
   end if;

transformed fragment:

   new_func((a == value1), target1);
   new_func((b == value2), target2);
   new_func((c == value3), target3);
   new_func((d == value4), target4);
   new_func((e == value5), target5);
   new_func((f == value6), target6);

with new_func defined as:

new_func(relation_value, target)
{
   if (relation_value) then
      y = target;
   end if;
}

discussion:

This example shows an original code fragment which tests a number of similar
variables against a number of values.  This can not be transformed into a 
CASE construct, so some way of removing the if else if chain must be found.
The resulting code moves the tests into new_func.  Note that this particular
example uses y as global to new_func, to allow new_func to represent a single
iteration of the if else if chain.

Some may argue that the transformed code is better.  I argue, though, that is
hides the logic, while the original code was quite clear, therefore easily
maintained.

Example 2:

original code fragment:

if (failing) then
   if (color_display) then
      color = red;
   else
      inverse = TRUE;
   end if;
else if (mode = offline) then
   color = color_1;
else if (sub_mode = training) then
   color = color_2;
end if;

transformed fragment:

if (! fail_func) then
   if (! mode_func) then
      sub_mode_fun;
   end if;
end if;

where fail_func is :

BOOLEAN fail_func()
{
   if (failing) then
      if (color_display) then
         color = red;
      else
         inverse = TRUE;
      end if;
      return (TRUE);
   else
      return (FALSE);
   end if;
}

discussion:

The original code fragment handles the problem of selecting a color for a field in a
display.  The algorithm for this is a hierarchical checking of a number of variables.
The fragment mearly moves testings into less obvious locations, while the main code
fragment is reduced to calling functions that set the color.

I believe the transformed code is more complex and harder to maintain because it is bigger,
includes more interfacing, and separates the decisions relating to the color to use.

Thats all I have time for now...

I would like to repeat my feelings regarding complexity measures, based upon my
experience in using them:

I agree with the goal of using a metric to measure the characteristics of
code, I simply believe that no metric now exists that truly measures the
qualities that people (management, customers) want to measure.

Marc S. Gibian
Software Engineer
Apollo Computers

email:  marc@apollo.UUCP

g-rh@cca.CCA.COM (Richard Harter) (01/09/88)

In article <398e2f3e.fed5@apollo.uucp> marc@apollo.uucp (Marc Gibian) writes:
>As promised, here are some examples of decreasing the quality of code
>while reducing the complexity measure value...
>
>original fragment:
>
>   if (a == value1) then
>      y = target1;
>   else if (b == value2) then
>      y = target2;
>   else if (c == value3) then
>      y = target3;
>   else if (d == value4) then
>      y = target4;
>   else if (e == value5) then
>      y = target5;
>   else if (f == value6) then
>      y = target6;
>   end if;
>
>transformed fragment:
>
>   new_func((a == value1), target1);
>   new_func((b == value2), target2);
>   new_func((c == value3), target3);
>   new_func((d == value4), target4);
>   new_func((e == value5), target5);
>   new_func((f == value6), target6);
>
>with new_func defined as:
>
>new_func(relation_value, target)
>{
>   if (relation_value) then
>      y = target;
>   end if;
>}

	I would agree that the altered code is less desirable than the
original code -- indeed the altered code is not correct in that it is
not equivalent to the original code!  The altered code goes through all
tests whereas the original code stops testing when a match is found.

	The fundamental problem with the complexity measure in this case
is, I believe, twofold: (a) that the language being used lacks the construct
being used, and (b) the complexity measure is incorrectly calculated.

	The language doesn't really capture the parallelism of the
construct.  The desired construct, in pseudocode, runs something like
this:

	define value list A = {a,b,c,d,e,f}
	define desired value list B corresponding to A = 
		{value1,value2,value3,value4,value5,value6}
	define target list C corresponding to A =
		{target1,target2,target3,target4,target5,target6}

	loop through list A
	  if item from A equals coresponding desired item from B then
	    set y to target in C corresponding to item from A
	    and exit from loop

If the lists are short one prefers to embed them implicitly in the code;
if they are long one prefers to be put them in tables.  However this solution
is not available if the types in the the value list are not all the same.
If we borrow the naked select statement from PL/I the code would look like
this

	select {
	  a==value1: y = target1;
	             break;
	  b==value2: y = target2;
	             break;
	  ......
	  f==value6: y = target6;
		     break;
          }

Whether each case should count as an increment in complexity depends on the
cases.  In this instance the cases are parallel -- I would feel that the
appropriate measure of complexity is the logarithm of the number of cases.
If, however, the cases are not parallel, then I would feel that the compexity
goes up linearly with the number of case.  [I am not prepared to defend this
position.]

In any case what we see here is a complex structure that uses code repetition
rather than data repetition.  Neither the language being used, nor the
complexity measure being used provide means for dealing with the higher
level construct.  Each, in its own way, deals with the higher level construct
only in terms of lower level constructs.

I would agree that the complexity measure being used does not adequately
measure the complexity of the code -- I would argue that the fault lies
in the lack of adequate means for heirarchical composition of structure
in the language and a corresponding lack of measurement in the complexity
measurement tool.
-- 

In the fields of Hell where the grass grows high
Are the graves of dreams allowed to die.
	Richard Harter, SMDS  Inc.

franka@mmintl.UUCP (Frank Adams) (01/14/88)

In article <398e2f3e.fed5@apollo.uucp> marc@apollo.uucp (Marc Gibian) writes:
|As promised, here are some examples of decreasing the quality of code
|while reducing the complexity measure value. ...  The contractual
|requirement was that every procedure and function have a McCabe value less
|than or equal to 10.  

One general comment: these examples suggest that, at least at the current
level of development, the complexity measures are better as a tool for
evaluating code than as a goal.  They make a better servant than a master.

|original fragment:
|
|   if (a == value1) then
|      y = target1;
|[...]
|   else if (f == value6) then
|      y = target6;
|   end if;
|
|transformed fragment:
|
|   new_func((a == value1), target1);
|[...]
|   new_func((f == value6), target6);
|
|with new_func defined as:
|
|new_func(relation_value, target)
|{
|   if (relation_value) then
|      y = target;
|   end if;
|}

Two comments here:

It seems to me that a metric should regard an "if ... else if ... ... [else
...]" construct as approximately equivalent in complexity to an "if ... else
..." construct.

It seems to me also that use of global variables should be penalized.  As a
first approximation, one might include a cost of 1 for each global variable
referenced by the program.  (This still leaves the option of putting several
more or less irrelevant globals into a structure, to cut down on the number
of globals.  Perhaps each component explicitly referenced should cost 1?)

|I agree with the goal of using a metric to measure the characteristics of
|code, I simply believe that no metric now exists that truly measures the
|qualities that people (management, customers) want to measure.

This seems like a reasonable summary.
-- 

Frank Adams                           ihnp4!philabs!pwa-b!mmintl!franka
Ashton-Tate          52 Oakland Ave North         E. Hartford, CT 06108