Lecture 2: Principles of good programming

 

Objectives of this lecture

q       Learn (Review) principles of good programming by studying a non-trivial example.

 

The Game of life problem

q       The best way to teach principles of good programming is by example.  In this lecture and the next, we study, design and implement an interesting simulation game called, the game of life, developed by a British mathematician, J.H. Conway

q       As we go through this process, we shall highlight some of the very important principles that are necessary for the development of good software.

 

Rules for the game of life:

q       The game takes place on an unbounded rectangular grid in which cells become alive (occupied) or dead (unoccupied) from generation to generation according to the following rules:

 

1.    The neighbors of a cell are the eight cells that touch it horizontally, vertically or diagonally.

2.    A living cell stays alive in the next generation if it has either 2 or 3 living neighbors;  It dies if it has 0,1,4 or more living neighbors.

3.    A dead cell becomes alive in the next generation if it has exactly 3 living neighbors.

4.    All births and deaths takes place at exactly the same time, so that a dying cell can help give birth to another but cannot prevent the death of others, nor can cells being born either preserve or kill cells living in the previous generation.

 

q       The following configurations for example alternate between generations.

q      

 

Goal:

q       Our Goal is to write a program that will show how an initial community will change from generation to generation – following the principles of good programming

 

Solution:

q       The problem could be solved by using a 2-D array, with 1 and 0 representing living cells and dead cells respectively.  We can obtain the next generation by using loop to count the neighbors of each cell and apply the rules. 

q       However, we should be careful not to violate the last rule by allowing changes made earlier to affect the count for cells considered letter. 

q       This can be solved by using a temporary array to hold the next generation as we process the present.

 

The algorithm

Initialize the array map to contain the initial configuration

LOOP for as long as desired
     LOOP for each cell in the array map

a.    count the number of living neighbors

b.    if the count is 0, 1, 4, 5 ,6 ,7 or 8 , set the corresponding cell in array newmap to dead; if the count is 3, set the corresponding cell in array newpap to living; if the count is 2, set the corresponding cell  in array newmap to the same status as the current cell.

 

A C implementation of the above algorithm could be as follows:

 

/* Simulation of Conway's game of Life on a bounded grid

 

Pre: The user must supply an initial configuration of living cells

Post: The program prints a sequence of maps showing the changes in

      the configuration according to the rules for the game

Uses: functions Initialize,WriteMap,NeighborCount, and UserSaysYes

*/

#include   "common.h" /*common include files and definitions */

#include   "life.h"   /*Life's defines, typedefs, and prototypes*/

 


void main(void)

{  int  row, col;

    Grid map;                       /* current generation   */

    Grid newmap;                    /* next generation      */

 

    Initialize(map);

    WriteMap(map);

    printf("This is the initial configuration you have chosen.\n"

           "Press <Enter> to continue.\n");

    while(getchar() != '\n')

        ;

    do {for (row = 1; row <= MAXROW;    row++)

            for (col = 1; col <= MAXCOL; col++)

                switch(NeighborCount(map, row, col)) {

                case 0:

                case 1:

                    newmap[row][col]= DEAD;

                    break;

                case 2:

                    newmap[row][col]= map[row][col];

                    break;

                case 3:

                    newmap[row][col]= ALIVE;

                    break;

                case 4:

                case 5:

                case 6:

                case 7:

                case 8:

                    newmap[row][col]= DEAD;

                    break;

                }

        CopyMap(map, newmap);

        WriteMap(map);

        printf("Do you wish to view the next generations");

    } while (UserSaysYes());

}

Notes:

1.      The file common.h contains the definitions and #include statements for the standard files that appear in many programs and will be used throughout this course.  It includes:

 

#include <stdio.h>

#include <stdlib.h>

typedef enum boolean { FALSE, TRUE } Boolean;

void Error(char *); //display an error message and halts program

void Warning(char *); // Error: report program error.

2.      The file life.h contains the definitions and the function prototypes for the life program.  It includes:

 

#define MAXROW 20       /* maximum row range        */

#define MAXCOL 60       /* maximum column range    */

 

typedef enum state { DEAD, ALIVE } State;

typedef State Grid[MAXROW+2][MAXCOL+2];

 

void    CopyMap(Grid map, Grid newmap);

Boolean UserSaysYes(void);

void    Initialize(Grid map);

int     NeighborCount(Grid map, int row, int column);

void    WriteMap(Grid map);

 

Good Programming principles

You will study the construction of the functions required for the completion of the life game problem (see section 1.4) of the book.  However, we can derive the following good programming principles from what we have done so far:

 

1.    Analysis of the problem:  This should always be the stating point.  Before you start anything, make sure you thoroughly understand the specification of the problem you are about to solve.

 

2.    Design the solution:  Never start coding any problem without first using pencil and paper to design the solution.  For reasonably small programs such as the life game problem, algorithms may suffice.  However, for large software system, software engineering methods  must be applied. 

 

3.    Document program specification:  The conditions required to hold when a program begins and when it finishes called respectively, the pre-conditions and post-conditions should be documented at the beginning of every function.

 

4.    Document List of functions used:  These should also be documented at the beginning of every functions

 

5.    Use meaningful names for identifiers:  Always give names to your variables, functions, user-defined types, etc, such that the names suggest their purpose.

 

6.    Documentation:   Give concise and descriptive comments throughout your program.  As a guide:\

Ø      Indicate at the beginning, the purpose of the function and for main program you may include the author’s name, Date last modified, version number, etc.

Ø      Explain the purpose of each significant section and indicates its end

Ø      Update your documentation as you modify the code.

 

7.    Indentation:  Insert spaces, black lines and indentation to group related statements especially structured statements such as if, while and for.

 

8.    Top-down design:  This is the key to writing large programs.  Try to look at the problem at a high level and break it into smaller components so that the individual components are treated one at a time.  This approach has many advantages and in fact is a must in mordern software development.  Some of the advantages are:

Ø      Easy to code: --concentrate on a simple task at a time

Ø      Easy to detect and correct errors

Ø      Re-userbility: --The same sub-task is often required in solving other problems

Ø      Maintainability:-  Easy to modify a small component

Ø      Division of Work:-  different programmers can be working on different tasks

Ø      Etc.

 

9.    Stubs:  After coding the main program, most programmers will wish to complete the coding of functions to see if the system will work.  For large systems, this is usually not possible, in fact not advisable.  It is much easier to debug the main program (and indeed each function) as soon as it is written.  To compile the program correctly however, there must be something in place of each function that is used.  Thus, we must put at least dummy functions – called stubs.  E.g:

 

/* Initialize:  initialize grid map.     */

void Initialize(Grid map)

{

}

 

/* WriteMap:  write grid map.           */

void WriteMap(Grid map)

{

}

 

/* NeighborCount:  count neighbors of row,col.      */

int NeighborCount(Grid map, int row, int col)

{

    return 1;

}

 

Even with these stubs, we can at least compile the program and make sure that the declaration of types and variables are syntactically correct.

However, normally, each stub should at least print a message to indicate that the function was invoked.

 

10.          Drivers:  For large projects, it is also desirable to test each function as soon as it is written.  To achieve this, short main programs are written to provide the necessary input for the function, call the function and print the result.  Such a program is called a driver for the function.  For example, the following is a driver for the NeighborCount function:

 

/* Driver:  test NeighborCount().

 

Pre:   The user must supply an initial configuration of living cells.

Post: The program repeatedly invokes NeighborCount and

 displays the values returned.

 */

int main(void)

{

    Grid map;

    int  i, j;

    Initialize(map);

    for (i = 1; i <= MAXROW; i++) {

        for (j = 1; j <= MAXCOL; j++)

            printf("%3d", NeighborCount(map, i, j));

        printf("\n");

    }

    return 0;

}

 

11.          Program Testing:  After coding all the functions and assembling them into a complete system, it is necessary to test the entire system by using carefully chosen test data.  While selecting test data, take note of the following guides:

Ø      Easy values:-choose data values that can easily be checked

Ø      Typical Values:-choose data that will normaly be used with the program

Ø      Extreme Values:- check the boundary (limit) values

Ø      Illegal values:-ensure that the program will not crash on encountering illegal values.