CSC/ECE 517 Fall 2009/wiki3 13 is

From Expertiza_Wiki
Jump to navigation Jump to search

Self-Documenting Code

Introduction

One of the characteristics of elegant, well-designed code is that it should be easy for someone to read the code and understand exactly what the code does. It should be possible to understand the code by using the names and comments within the code without needing any additional documentation. Source code that is easy to read and understand is known as self-documenting code.

There are numerous advantages to self-documenting code. One of the biggest advantages is the fact that it is often quite easy to maintain or extend code that is self-documenting. Another advantage is the fact that it is not essential to have external sources of documentation to understand how the code works. Self-documentation can also help prevent certain types of bugs or defects from being introduced in the code due to the fact that developers usually have a clearer understanding of what the code does.

Techniques

By following a few simple techniques and guidelines developers can produce self-documenting code fairly easily.

Whitespace

Appropriate use of blank lines and indentation can greatly improve the readability of source code. Blank lines should be inserted between logical sections or blocks of code. In most languages code contained within a block should be indented to make it clear that it is part of a block of code. Consider the following examples:

Example 1

// only draw if we have good values
if (Double.isNaN(transX0) || Double.isNaN(transY0)
|| Double.isNaN(transX1) || Double.isNaN(transY1)) {return;}
// create a line to use to determine where the threshold is crossed
Line2D.Double tline = new Line2D.Double();
if (orientation == PlotOrientation.HORIZONTAL)
tline.setLine(threshY0,threshX0,threshY1,threshX1);
else if (orientation == PlotOrientation.VERTICAL)
tline.setLine(threshX0,threshY0,threshX1,threshY1);
// calculate the point where the threshold is crossed
Point2D.Double tintersect = lineIntersection(tline,state.workingLine);
if (tintersect == null)
return;

Example 2

// only draw if we have good values
if (Double.isNaN(transX0) || Double.isNaN(transY0)
    || Double.isNaN(transX1) || Double.isNaN(transY1)) {
    return;
}

// create a line to use to determine where the threshold is crossed
Line2D.Double tline = new Line2D.Double();

if (orientation == PlotOrientation.HORIZONTAL)
    tline.setLine(threshY0,threshX0,threshY1,threshX1);
else if (orientation == PlotOrientation.VERTICAL)
    tline.setLine(threshX0,threshY0,threshX1,threshY1);

// calculate the point where the threshold is crossed
Point2D.Double tintersect = lineIntersection(tline,state.workingLine);
if (tintersect == null)
    return;

The second example is much easier to read because of the proper use of whitespace. It is usually best to avoid putting more than one statement of code on a single line unless the statements are very short and very simple. Long statements can (and should) be broken into multiple lines with indentation to indicate that the additional lines belong to the same statement.

Some languages have style guidelines that describe where and when whitespace should be used, such has which lines of a method should be indented and where block identifiers such as { or } should be located. Links to style guidelines for various languages are included below.

Naming Convention

Choosing good names is critical to writing self-documenting code. In many languages objects, variables, and methods are identified by their name. Good code uses names that are clear and descriptive. Descriptive names do not have to be long or verbose and it is often better to use shorter names with logical abbreviations instead of a really long name. For instance, the variable names userIdentifcationValue and userID are both fairly descriptive but userID is shorter and easier to type and is just as clear as userIdentifcationValue. Bad code uses names that are generic or that provide no indication what the item is used for such as i, j, n, num, r, x, or cv. Consider the following examples:

Example 1

void ML::d(const Canvas *vc,
					TS* ts, 
					PointType tb,
					PointType to,
					RectangleType dr)
{
	Int16 sdx = dr.tl.x - to.x; 
	Int16 sdy = dr.tl.y - to.y;

	for(UInt16 x = tb.x; x < m_ms.x; x++)
	{
		for(UInt16 y = tb.y; y < m_mapSize.y; y++)
		{
			T* t = ts->g(m_lb[(x * m_ms.y) + y]);
			if(t) 
				t->d(vc, sdx, sdy);
			
			sdy += ts->gh();
			
			if(sdy >= dr.tl.y + dr.e.y)
				break;
		}
		
		sdy = dr.tl.y - to.y;
		sdx += ts->gw();
		
		if(sdx >= dr.tl.x + dr.e.x)
			break;
	}
}

Example 2

void MapLayer::draw(const Canvas *viewportCanvas,
					TileSet* tset, 
					PointType tileBegin,
					PointType tileOffset,
					RectangleType drawRect)
{
	Int16 screenDrawX = drawRect.topLeft.x - tileOffset.x; 
	Int16 screenDrawY = drawRect.topLeft.y - tileOffset.y;

	for(UInt16 x = tileBegin.x; x < m_mapSize.x; x++)
	{
		for(UInt16 y = tileBegin.y; y < m_mapSize.y; y++)
		{
			Tile* tile = tset->getTileAtIndex(m_layerBytes[(x * m_mapSize.y) + y]);
			if(tile) 
				tile->draw(viewportCanvas, screenDrawX, screenDrawY);
			
			screenDrawY += tset->getTileHeight();
			
			if(screenDrawY >= drawRect.topLeft.y + drawRect.extent.y)
				break;
		}
		
		screenDrawY = drawRect.topLeft.y - tileOffset.y;
		screenDrawX += tset->getTileWidth();
		
		if(screenDrawX >= drawRect.topLeft.x + drawRect.extent.x)
			break;
	}
}

It is very difficult to figure out what the first code section does because none of the variables, objects, or methods have descriptive names. The second code section uses descriptive names and is much easier to read and understand.

Many programming languages have established naming conventions that are used to distinguish a variable from an object or a method. It is important for a programmer to follow the established naming convention for the language that he or she is writing in. Links to naming conventions for various languages are included below.

Code Structure and Comments

There are often many different ways of writing code to accomplishing a particular task. Generally a simple approach to a problem produces code that is more readable and easier to understand than a complex solution.

Some complex algorithms can be greatly simplified by moving pieces of code into separate methods or functions. Code that has well defined methods generally exhibits good object oriented design in addition to being easier to understand and maintain. Consider the following examples:

Example 1

protected void drawLine() {
	// ** SNIP **

	// determine whether the line starts above or below the threshold
	if (y1>y0) {
		// line starts below the threshold
		state.workingLine.setLine(transX0, transY0, tintersect.getX(), 
			tintersect.getY());
		g2.setStroke(getItemStroke(series, item));
		g2.setPaint(getItemPaint(series, item));
		g2.draw(shape);

		state.workingLine.setLine(tintersect.getX(), tintersect.getY(), 
			transX1, transY1);
		g2.setStroke(getItemStroke(series, item));
		g2.setPaint(getItemPaint(thresholdPaint));
		g2.draw(shape);
	}
	else {
		// line starts above the threshold
		state.workingLine.setLine(transX0, transY0, tintersect.getX(), 
			tintersect.getY());
		g2.setStroke(getItemStroke(series, item));
		g2.setPaint(getItemPaint(thresholdPaint));
		g2.draw(shape);

		state.workingLine.setLine(tintersect.getX(), tintersect.getY(), 
			transX1, transY1);
		g2.setStroke(getItemStroke(series, item));
		g2.setPaint(getItemPaint(series, item));
		g2.draw(shape);
	}
}

Example 2

protected void drawLine() {
	// ** SNIP **

	// determine whether the line starts above or below the threshold
	if (y1>y0) {
		// line starts below the threshold
		state.workingLine.setLine(transX0, transY0, tintersect.getX(), 
			tintersect.getY());
		drawFirstPassShape(g2, pass, series, item, state.workingLine, false);

		state.workingLine.setLine(tintersect.getX(), tintersect.getY(), 
			transX1, transY1);
		drawFirstPassShape(g2, pass, series, item, state.workingLine, true);
	}
	else {
		// line starts above the threshold
		state.workingLine.setLine(transX0, transY0, tintersect.getX(), 
			tintersect.getY());
		drawFirstPassShape(g2, pass, series, item, state.workingLine, true);

		state.workingLine.setLine(tintersect.getX(), tintersect.getY(), 
			transX1, transY1);
		drawFirstPassShape(g2, pass, series, item, state.workingLine, false);
	}
}

protected void drawFirstPassShape(Graphics2D g2, int pass, int series,
                                  int item, Shape shape, boolean overthreshold) {
    g2.setStroke(getItemStroke(series, item));
    
    if (overthreshold==true)
        g2.setPaint(thresholdPaint);
    else
        g2.setPaint(getItemPaint(series, item));
        
    g2.draw(shape);
}

The first example has a lot of code to handle the details of drawing lines. The extra code makes it harder to understand what else the code is doing. The second example moves the task of drawing the lines to a separate drawing method which helps simplify the code and makes it easier to follow.

Generally the simpler the code is, the easier it is to understand it. When given the choice between a simple implementation and a complex one the simpler implementation will almost always be easier to debug and maintain.

Some coding problems are very difficult to solve in a straightforward, easy to understand manner. In these situations it is important to thoroughly document the code with comments that explain exactly what is being done and how. In some cases it is also a good idea to include comments that describe why the section was implemented the way it is.

Naming Conventions and Style Guides

C/C++

C#

Java

Perl

PHP

Ruby