Data Modeling (w/ classes)

Review
Recall that in previous notes we looked at the concept of data modeling. We saw that by focusing on the data (subjects, nouns) in a problem statement, we can design programs that are object-oriented in nature. We saw that such a design methodology leads to developing code that can be easily reused in other applications. We also noted that object-oriented programming is a design methodology that is independent of programming languages. We developed the following example program using a data-centered methodology using C/C++ structures with functions.

Problem Description

A 30% salt solution is prepared by mixing a 20% salt solution and a 45% solution. How many liters of each must be used to produce 60 liters of the 30% salt solution?

 

We identify the noun, solution, as a primary data model (or object) and store “what a solution knows about” in a C/C++ structure. We then describe “what a solution does” in functions that operate on a single solution (by passing to the function the address of the object to operate on).

 

#include <iostream>

using namespace std;

 

// Stores what a solution knows

struct Solution

{

  float vol;                    // volume

  float per;                    // percentage

};

 

// Initialize a solution

void InitSolution( Solution *m, float vol, float per )

{

  m->vol = vol;

  m->per = per;

}

 

// Mix a solution

void MixSolutions( Solution *x, Solution *y, Solution *f )

{

  y->vol = ( f->per*f->vol - x->per*f->vol ) / ( y->per - x->per );

  x->vol = f->vol - y->vol;

}

 

// Print information about a solution

void PrintSolution( Solution *m )

{

  cout << "Solution:" << endl;

  cout << "  volume: " << m->vol << endl;

  cout << "  percentage: " << m ->per << endl;

}

 

 

Structures and functions together encapsulate the code that defines and describes a solution data model or object. Any application (or program) requiring an object, solution, can use this code. In this way, our data model is a type of stand alone module (or Lego™) that is completely independent of any application and, as such, can be reused for multiple applications. An application to solve the original program statement using objects from this data model would look like the following.

 

main()

{

  // Declare 3 solutions

  Solution x;

  Solution y;

  Solution f;

 

  // Initialize solutions

  InitSolution( &x, 0.0, 0.20 );

  InitSolution( &y, 0.0, 0.45 );

  InitSolution( &f, 60.0, 0.30 );

 

  // Mix solutions

  MixSolutions( &x, &y, &f );

 

  // Print solutions

  PrintSolution( &x );

  PrintSolution( &y );

  PrintSolution( &f );

}

 

Object-oriented design with classes
The C++ programming language contains many features that make it easier to design and write programs using object-oriented methodology. Let’s begin by looking at how classes can be used to define and describe a data model. We’ll use the same problem example above to design a data model for a solution.

#include <iostream>

using namespace std;

 

class Solution

{

private:

  float vol;

  float per;

 

public:

  Solution() 

  {

    vol = 0.0;

    per = 0.0;

  }

  void Init(float v, float p);

  void Print();

  void Mix(Solution *, Solution *);

  void setVol( float v ) { vol = v; }

  void setPer( float p ) { per = p; }

  float getVol() { return vol; }

  float getPer() { return per; }

};

 

// Init method definition

void Solution::Init(float v, float p)

{

  vol = v;

  per = p;

}

 

// Solve method definition

void Solution::Mix(Solution *x, Solution *y)

{

  y->vol = ( per*vol - x->per*vol ) / ( y->per - x->per );

  x->vol = vol - y->vol;

}

 

// Print method definition

void Solution::Print()

{

  cout << “Solution:” << endl;

  cout << “  volume: ” << vol << endl;

  cout << “  percentage: ” << per << endl;

}

An application using this data model to solve the problem statement in C++ would look like the following:

main()

{

  Solution x;

  Solution y;

  Solution f;

 

  x.Init( 0.0, 0.20 );

  y.Init( 0.0, 0.45 );

  f.Init( 60.0, 0.30 );

 

  f.Mix( &x, &y );

 

  x.Print();

  y.Print();

  f.Print();

}

Although there are some differences, let’s look at the similarities between the design using structures and the design using classes by comparing the code side by side.

#include <iostream>

using namespace std;

 

// Knows

struct Solution

{

  float vol;

  float per;

};

 

// Does (declarations)

void InitSolution( Solution *, float, float per );

void MixSolutions( Solution *, Solution *, Solution * );

void PrintSolution( Solution * );

 

 

 

 

 

 

 

 

 

 

// Application

main()

{

  // Declare 3 solutions

  Solution x;

  Solution y;

  Solution f;

 

  // Initialize solutions

  InitSolution( &x, 0.0, 0.20 );

  InitSolution( &y, 0.0, 0.45 );

  InitSolution( &f, 60.0, 0.30 );

 

  // Mix solutions

  MixSolutions( &x, &y, &f );

 

  // Print solutions

  PrintSolution( &x );

  PrintSolution( &y );

  PrintSolution( &f );

}

 

 

// Does (definitions)

void InitSolution( Solution *m, float vol, float per )

{

  m->vol = vol;

  m->per = per;

}

void MixSolutions( Solution *x, Solution *y, Solution *f )

{

  y->vol = ( f->per*f->vol - x->per*f->vol ) /

                  ( y->per - x->per );

  x->vol = f->vol - y->vol;

}

void PrintSolution( Solution *m )

{

  cout << "Solution:" << endl;

  cout << "  volume: " << m->vol << endl;

  cout << "  percentage: " << m ->per << endl;

}

 

#include <iostream>

using namespace std;

 

// Knows

class Solution

{

private:

  float vol;

  float per;

 

// Does (declarations)

public:

  void Init(float v, float p);

  void Mix(Solution *, Solution *);

  void Print();

  Solution() 

  {

     vol = 0.0;

     per = 0.0;

  }

  void setVol( float v ) { vol = v; }

  void setPer( float p ) { per = p; }

  float getVol() { return vol; }

  float getPer() { return per; }

};

 

// Application

main()

{

  // Declare 3 solutions

  Solution x;

  Solution y;

  Solution f;

 

  // Initialize solutions

  x.Init( 0.0, 0.20 );

  y.Init( 0.0, 0.45 );

  f.Init( 60.0, 0.30 );

 

  // Mix solutions

  f.Mix( &x, &y );

 

  // Print solutions

  x.Print();

  y.Print();

  f.Print();

}

 

 

// Does (definitions)

void Solution::Init(float v, float p)

{

  vol = v;

  per = p;

}

void Solution::Mix(Solution *x, Solution *y)

{

  y->vol = ( per*vol - x->per*vol ) /

                  ( y->per - x->per );

  x->vol = vol - y->vol;

}

void Solution::Print()

{

  cout << “Solution:” << endl;

  cout << “  volume: ” << vol << endl;

  cout << “  percentage: ” << per << endl;

}

Although we’ll be looking in depth at each of these object-oriented features, let’s look briefly at some of the main differences from our design with structures:

 

Classes and Member functions

Beginning at the top of our comparison table, we use a class to store what the object knows instead of a structure. Like a structure, we can define the variables used to store what the object knows about. Inside a class, these variables are also called state variables because their values define the state an object at any point in time during the execution of a program.

 

class Solution

{

private:

  float vol;

  float per;

...

 

};

 

Different from our struct, a class can also store the function declarations defining what an object does. Because the functions are inside the class, we refer to them as member functions. In this way, a class brings together what an object knows and does in a much more intuitive and organized manner. Note also how entire function definitions can be stored inside a class. The following member functions below are defined inside the class. Note we’ve also added member functions that set and return (get) values from our data model variables. We’ll refer to them as access functions because they access state variables. As we’ll see later, access functions provide a means of security with information inside data objects.

 

class Solution

{

...

 

public:

Solution() 

{

    vol = 0.0;

    per = 0.0;

}

void setVol( float v ) { vol = v; }

void setPer( float p ) { per = p; }

float getVol() { return vol; }

float getPer() { return per; }

...

 

};

 

When member functions are defined outside the class, a special symbol called a scoping operator (::) is used to associate the function with the class it belongs to.

 

void Solution::Init(float v, float p)

{

  vol = v;

  per = p;

}

 

As members, member functions can access state variables as if they were declared local to the function. Note that in the member function definition above the state variable vol is being accessed and assigned directly.

 

 

 

Constructors

A special type of member function with the same name as the class is called a constructor shown below. Constructors are unique functions that are called automatically when objects are declared within an application. This provides a very useful way to perform routine tasks such as initializing variables within the model.

 

Solution() 

{

    vol = 0.0;

    per = 0.0;

}

 

 

 

Access privileges keywords (private, public)

C++ provides keywords that define programming access to the variables and functions defined within the data model. The keyword public allows access to both class member functions and applications using the class. The keyword private allows access to class member functions only. We’ll see later that this (along with a third keyword, protected) provides a form of information hiding and security for data objects.

 

 

 

Message Passing

Note the difference in how functions are called within the main application. Because functions are not associated with variables inside structs, our data objects are distinct from the functions. As a result, in order for a function to operate on an object, the address of that object was passed to the function.

 

Solution x;

InitSolution( &x, 0.0, 0.20 );           

 

 

With classes, variables and functions are both considered members of the class. They are not independent from one another. When objects are declared and used within a program they can access or “call” their individual member functions. In the example below, the data object x is calling its member function Init (note that this is similar to how structure objects access variables stored within its structure using the member operator). When on object calls a member function in this manner we also refer to this as message passing. A message (instruction) is being sent to an object.

 

Solution x;

x.Init( 0.0, 0.20 );

 

Note that with classes, the address of the object does not need to be passed to the function as in the design using structures. This is because the member function being called in C++ knows which object called it. Inside the function, therefore, any variable being referred to is the variable stored within the object that called it. In the Init function (shown below) called by the object x, the vol and per variables being assigned are the ones stored within the object x.

 

void Solution::Init(float v, float p)

{

  vol = v;

  per = p;

}

 

As an example, the values 0.0 and 0.20 are assigned to vol and per in the object x while the values 60.0 and 0.30 are assigned to vol and per in the object f. Each object declared is allocated separate areas in memory to store these values respectively.

 

x.Init( 0.0, 0.20 );

f.Init( 60.0, 0.30 );