Decomposing Large Projects

Goals

By the end of this lab you should:

Introduction

One of the most difficult skills that you will need to learn in order to become effective programmers is the ability to take a large programming task and developing a plan of attack for how you are going to complete that task. To help you develop this skill we will be working on a single large project for the majority of the assignments this quarter. In fact, the last two assignments were part of this project, though you may not have realized it at the time. The project that you will be working on is a game suite. You can download an example of the program here. Like the example programs for the assignment you will likely need to change the permissions on the file before you can execute it. Take a few minutes to explore the game suite before proceeding.

The game suite contains many aspects. It has a menu system that is navigateable through the use of buttons, a text viewer for reading the instructions for the games, a handful of graphical elements including icons for the games and and the suits for a deck of cards, and others; not to mention the games of tic-tac-toe and memory.

Getting started on such a large project can often be quite daunting; especially for a beginning programmer. All too often students delay starting a project because they feel overwhelmed by the complexity of the project. Then when they do start the program, they half-hazardly throw code into the editor in desperation with the hope that maybe they will get some part of the program working and at least get partial credit. It's a pattern that is repeated over and over again, and not only by students taking lower division courses. In fact students in lower division courses seldom encounter very large projects and then are taken by supprise when they hit the upper division courses and are assigned large projects and given very little guidence.

One of the goals of the teaching staff for this course ( your Professor and TAs ) is to equip you with this skill early on so that you are prepared to tackle large projects as you proceed through your education. ( Whether it be in Computer Science or not. ) In order to do this we have partitioned the project into many smaller tasks. Hopefully by understanding that the tasks that you are performing are ultimately designed to bring you closer toward the goal of the game suite, you will learn by example how to decompose other projects into managable tasks.

Below are some suggestions for how a project can be deconstructed in order to make it easier to accomplish. These methods were used when we partitioned the game_suite into weekly assignments, and can be used by you when you do those assignments.

Understanding the Requirements

If you ever get the oppertunity to speak with a practicing software engineer, they will likely tell you that the most difficult part of their job is soliciting the requirements of a software project. The client for as software program very seldom uses the same terminology as the team that is writing the software. They may not be able to communicate well what the requirements of the software are, or may assume that it will do things even though they never specified that such things needed to be included. Ultimately however, if the software does not do what the client expects the software engineer is to blame. It is the job of the software engineer to "pull" the requirements out of the client and to translate the words of the client into words that make sense in terms of the software that is to be constructed. If the software engineer does not clearly understand what the client wants, the project is doomed to be a failure before a single line of code is even written.

You are particularly lucky when you take a computer science class in that it is likely that the person requesting the software ( assignment ) is going to give a good description of what the sofware is actually supposed to do. Notice the word likely here. Sometimes the write-up for an assignment can be vague. It is your job to figure out what your client really wants. Before beginning on any assignment, take the time to carefully read the assignment description and do not start anything else until you are sure you understand completely what you are required to do. Sometimes you may even need to read it several times, or take notes in order to be sure that you understand it fully. If you are having dificulty deciphering what is required you should send e-mail to the class mailing list. Just like the practicing software engineer, sometimes you may need to "pull" the information from your client.

Establishing the Prerequisites

Once you understand what the requirements of the project are, it is often helpful to establish a list of prerequisites for the project. Make a list of the things that the program needs to be able to do and whether you are confident that you know how to do them. Often times there will be a few prerequisites that you are unclear on. For instance, you may be required to read input from, or write output to a file, or maybe you will have to be able to draw shapes of varying sizes. If you are unsure of how to do these things in a computer program, you should figure them out before you try to tackle the rest of the project. Most of this work will fall into the categories of researching and prototyping.

Researching

Often times in computer science you will be assigned a task that you don't know how to do. Perhaps, using the example from above, you need to know how to read input from a file. In such a situation you may need to pick up your book and do a bit of reading, or google it. Searching for "C++ read input from file" returns a wealth of useful information. Just be careful about plagerism. If you use a technique that you got from a web resource; make sure to give credit to that resource in your comments.

Prototyping

Another common scenario that you will run up against while designing and developing computer programs is that you will have a vague idea of how something could be done, but you're not completely sure that it will work, or how much effort it will take to accomplish. If you find yourself in such a situation, you should write a prototype. A prototype is a small program that you use to test out an idea or new technique.

Say you want to be able to draw a shape or icon at multiple different sizes but don't want to have to write a different function for each size that you want to draw it. If you are unsure how to do this, you might try writing a small program that does only that task.

One might ask the question, "Why should I write a whole new program to test something out? Why don't I just try it in the program that I am writing?" The answer is something that computer scientists refer to as "sandboxing". What sandboxing something means is to put into a controlled environment. If you test out things within the program that you are trying to write it can be very difficult to tell whether incorrect behaviour is due to the part of the program you are prototyping, or the remainder of the program. Also, the results of the prototyping may have a profound effect on the design of the program in the end; perhaps changing the design entirely. It is much easier to discover that your design needs to be altered before you start the program than after you have written half of it already.

Decomposition

Once you have established the prerequisites of a project it's time to decompose the project into a set of tasks that must be accomplished in order to complete the project. In addition to the tasks that are directly oriented toward the completion of the project, this may include doing some research or prototyping things that you are unsure about.

There is no simple formula for determining how a program should be decomposed, but here are a few suggestions.

For the game suite, the project will be broken up into the following tasks.

Incremental Developement

It is typically exponentially more difficult to find a bug in 20 lines of code than it is to find a bug in 10 lines of code. Also it is much easier to find a logical flaw in a description of what your code is doing than in the code itself. These two facts lead the two extremely beneficial pieces of advice.

  1. Write all of your comments before writing the code.
  2. Always be compiling and testing your code.

Write Your Comments First

It is highly recommended that you write the comments for your code before you write a single line of source code. This will accomplish two things. First, it will be much more likely that your comments describe what the program does rather than being a description of the syntax of C++ ( .// this is a function. is not an acceptable comment ). Second, it will make it much easier for you to write the source code. All you have to do is follow the instructions that you just wrote.

You would not believe how effective this is in improving the readability of your code and reducing the number of bugs in your programs and the time that you spend writing your programs.

Always be Compiling and Testing

Especially when you are first learning to program, the compiler can be one of your best tools for debugging your programs. Whenever you finish a "thought" in your code, quickly compile it to see if it's syntactically correct, or if the compiler gives you warnings about something that you have just written. Keeping on top of syntax errors as they happen is much easier than trying to hunt them down in a large block of code. Also, if the code that you have just written will generate some sort of output, run it to make sure that it is behaving as you had anticipated.

Unit Tests

If you want your program to work correctly, you can expect to spend half or more of the time spent on the program testing and debugging. This process can be made much more enjoyable ( or at least less excruciating ) by doing "Unit Testing". A unit test is a test that checks the functionality of a small portion of the project to ensure that it works correctly. Typically this is a small program who's sole purpose is to make sure a single class or set of functions works the way that you anticipate that they will.

A common mistake that program developers make is to say to themselves, "Why should I test this by itself? I'll know if it works or not when I try it in my program." This is a misconception. Consider what happens when you take 5 different pieces that have never been tested and combine them. You run the program which uses these 5 pieces and to your suprise the program doesn't work. Now you are saddled with the task of trying to determine which one doesn't work. Often you end up blindly poking around your code rapidly hacking out something and rewriting it with no real evidence that it was the problem in the first place. The process of hurriedly makeing changes inevitably causes even more bugs than you had in the first place. Even if the current bug is found, other bugs may still be hiding in your program because the conditions under which the bug appears hasn't been tried yet.

At a minimum you should write a unit test for each class that you write which calls every method of the class and verifies that the methord performs as expected. This won't catch all of the bugs in your code, but it will definately get the majority of them.

Component Integration

As pointed out before, dumping all of your code together and hoping that it works makes debugging things a complete nightmare. Wherever possible you should try to add in components ( which have already been unit tested ) one at a time. Sometimes the integration of components may require testing a set of components together before you add them to your program. Another technique that is often used is to write a "stub" which acts as a standin for a component that has not yet been developed.

For example, your may be writing a program which calculates the quadradic roots of a polynomial equation. Just for example, lets say that you split the task of calculating the numerator and the denomenator into two separate functions and have written the function that calculates the numerator, but not the function which calculates the denominator. In order to test that the numerator calculating function is working properly when integrated into the program, you could write a temporary denominator calculating function wich simply returns 1. You can then look at the output of your program and see if it is calculating the roots as the numerator of the roots. This example is somewhat contrived, but the method of writing stubs in order to test components one at a time is an extremely effective technique.