Part A - Introduction

Modular Programming

Partition the source code for a programming solution into modules
Compile the set of modules on Linux and Windows platforms

"Decide which modules you want; partition the program so that data is hidden within modules" (Stroustrup, 1997)

Modules | Stages of Compilation | Example | Unit Tests | Summary | Exercises


We store object-oriented source code in modules.  The technique of modularization predates object-oriented languages and is found in languages like C.  C lets us access much functionality outside the core language through library modules.  For instance, its stdio module provides input and output support, while hiding the implementation of scanf() and printf() in a separate file.  In object-oriented solutions we store class definitions and the implementation of their logic in modules. 

This chapter describes how to create a set of modules, compile the source code in each one, and link the compiled code into an executable binary.  This sets the stage for storing related classes in their own module.  The chapter concludes with an example of unit tests on a module.


Modules

A well-designed module is a highly cohesive unit that is loosely coupled to other modules.  It handles one aspect of a solution and hides as much detail as possible.  In an object-oriented language like C++, a module contains the unit of source code that the compiler translates independently into a unit of binary code. 

A modular design for the retail store order application described in the first chapter is illustrated below.  The main module accesses the Order module and the iostream module.  The Order module contains the Order and Special Order classes.  The Order module accesses the EAN module.  The EAN module contains the GS1 Prefix and EAN classes.  The iostream module contains the classes that describe the standard input and output objects. 

modularity

In translating any module the compiler only needs enough information to identify the names that are defined in any other modules.  For this, we store the source code for each module in two separate files:

  • the header file - declares to the client the class definitions and the function prototypes
  • the implementation file - defines the functions that hold the logic

The extension .h (or .hpp) identifies the header file.  The extension .cpp identifies the implementation files. 

Note that the names of the header files for the standard C++ libraries do not include a file extension.  The <iostream> header file contains the class definitions for cout and cin.  To inform the compiler that these object names are valid, we include the header file for the iostream module. 

Example

There are three modules in the design illustrated above.  The implementation file for the main module includes the header files for the itself and the Order and iostream modules, but not their implementation files.  The header file for the Order module includes the header file for the EAN module, but not the implementation file.  In other words, a module's header or implementation file only ever includes the header files of other modules.

header

The implementation file of each module compiles separately and only once.  That is, we compile .cpp files, but not the header files.  We do not need to compile iostream's implementation file since its compiled version is in the system library.

header


Stages of Compilation

Complete compilation consists of three independent and consecutive stages (as shown in the figure below):

  1. Pre-processing - inserts the contents of the header files into the implementation files and substitutes all #define macros to create a single translation unit
  2. Compiling - compiles each translation unit separately and creates an independent binary for that unit
  3. Linkage - assembles the various binaries for the translation units along with the system binaries to create the executable binary

compile and link


A Trivial Example

As an example of modular design and compilation, consider a trivial accounting application that accepts journal transaction data from the standard input device and displays that data on the standard output device without any intermediate modification.  We shall refine the source code for this application in our exercises as we proceed through the next few chapters. 

Our design consists of two modules:

  • Main - supervises the input and output for all transactions
  • Transaction - defines the logic for inputting data and outputting data for one single transaction

Transaction Module

Let us start with a C-style structure that holds the information for a single transaction and two global functions

  • enter() - accepts transaction data from the standard input device
  • display() - displays transaction data on the standard output device

Transaction.h

The header file defines our Transaction structure and declares the two function prototypes: 

 // Modular Example
 // Transaction.h

 struct Transaction {
     int acct;      // account number
     char type;     // credit 'c' debit 'd'
     double amount; // transaction amount
 };

 void enter(struct Transaction* tr);
 void display(const struct Transaction* tr); 

Note the UML naming convention and the extension on the file's name. 

Transaction.cpp

The implementation file defines the two functions.  It includes the system header file that defines the cout and cin objects and the header file that defines the Transaction structure.  The implementation file has a .cpp extension: 

 // Modular Example
 // Transaction.cpp

 #include <iostream>
 using namespace std;
 #include "Transaction.h"

 // prompts for and accepts Transaction data
 //
 void enter(struct Transaction* tr) {

     cout << "Enter the account number : ";
     cin  >> tr->acct;
     cout << "Enter the account type (d debit, c credit) : ";
     cin  >> tr->type;
     cout << "Enter the account amount : ";
     cin  >> tr->amount;
 }

 // displays Transaction data
 //
 void display(const struct Transaction* tr) {

     cout << "Account " << tr->acct;
     cout << ((tr->type == 'd') ? " Debit $" : " Credit $") << tr->amount; 
     cout << endl;
 }

Main Module

The main module defines one Transaction object and calls the global functions defined in our Transaction module. 

main.h

The main module's header file #defines the number of transactions to be processed: 

 // Modular Example
 // main.h

 #define NO_TRANSACTIONS 3

main.cpp

The main module's implementation file defines the main() function.  We #include the header file for the Transaction module to inform the compiler that the Transaction structure is a valid structure and that the function calls are valid calls:

 // Modular Example
 // main.cpp

 #include "main.h"
 #include "Transaction.h"

 int main() {
     int i;
     struct Transaction tr;

     for (i = 0; i < NO_TRANSACTIONS; i++) { 
         enter(&tr);
         display(&tr);
     }
 }

Command Line Compilation

Linux

Our Linux platform hosts the GNU g++ compiler.  To compile our application on it, we enter the command

 g++ -o accounting main.cpp Transaction.cpp 

The -o option identifies the name of the executable binary.  The names of the two implementation files follow this option.

To run the executable binary, we enter

 accounting

Visual Studio

Our Windows platform hosts the Visual Studio compiler.  To compile our application at the command-line, we enter the command

 cl -oaccounting main.cpp Transaction.cpp 

You can access this compiler through the Visual Studio command prompt window.  To open the window, press Start > All Programs and search for the prompt in the Visual Studio Tools sub-directory. 

To run the executable, we enter

 accounting

Unit Tests

Modular programs are well suited to unit testing.  A unit test is a code snippet that tests a single assumption in a work unit of a complete program.  A work unit is a single logical component with a simple interface.  A typical work unit is a function.  A suite of unit tests examines a program's work units and can be rerun after each upgrade.  We store the test suite in a separate module.

For example, consider a Calculator module, which includes the capability to raise an integer to the power of an integer exponent and to determine the integer exponent to which an integer base has been raised to obtain a given result.  The header file for the Calculator module includes the prototypes for these two work units:

 // Calculator.h
 // ...
 int power(int, int);
 int exponent(int, int);

The suite of unit tests checks if the implementations return the expected results.  The header file for the Tester module contains:

 // Tester.h

 int testSuite(int BASE, int EXPONENT, int RESULT); 

The implementation file for the Tester module contains:

 // Tester.cpp

 #include<iostream>
 using namespace std;
 #include "Calculator.h"

 int testSuite(int BASE, int EXPONENT, int RESULT) {
     int passed = 0;
     int result;
     result = power(BASE, EXPONENT);
     if (result == RESULT) {
         cout << "Raise to Power Test Passed" << endl; 
         passed++;
     }
     else {
         cout << "Raise to Power Test Failed" << endl;
     }
     result = exponent(RESULT, BASE);
     if (result == EXPONENT) {
         cout << "Find Exponent Test Passed" << endl;
         passed++;
     }
     else {
         cout << "Find Exponent Test Failed" << endl;
     }
     return passed;
 }

A first attempt at implementing the Calculator module look like:

 // Calculator.cpp

 #include "Calculator.h"

 int power(int base, int exp) {
     int i, result = 1;
     for (i = 0; i < exp; i++)
         result *= base;
     return result;
 }

 int exponent(int result, int base) { 
     int exp = 0;
     while(result >= base) {
         exp++;
         result /= base;
     }
     return exp;
 }

In this case, the following test main produces the results shown on the right:

 // Test Main
 // testmain.cpp

 #include<iostream>
 using namespace std;
 #include "Tester.h"

 int main() {
     int passed = 0;
     passed += testSuite(5, 3, 125);
     passed += testSuite(5, -3, 125);
     cout << passed << " Tests Passed" << endl;
 }








 Raise to Power Test Passed
 Find Exponent Test Passed
 Raise to Power Test Failed 
 Find Exponent Test Failed
 2 Tests Passed

Clearly, the implementation needs to be upgraded to handle bases that are negative. 

Recommended Approach

One recommended approach to coding any implementation is to write the suite of unit tests for the work units in a module as soon as we have defined its header file and before starting to code the bodies of the work units in the implementation file.  As we fill in the implementation details, we can continue testing the module for the results that we expect.


Summary

  • a module consists of a header file and an implementation file
  • a module's header file declares the names that other modules may reference
  • a module's implementation file defines the module's logic
  • we only include a module's header files in the source code for other modules
  • three stages involved in creating an executable binary are pre-processing, compiling, and linking
  • the suite of unit tests for a module should be written before the implementation is complete

Exercises

Previous Reading  Previous: Object Terminology Next: Rudiments   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License