The Principle of Encapsulation

 < Free Open Study > 



At the heart of any object language is the principle of encapsulation. This trait boils down to a language's ability to hide unnecessary implementation details from the object user. To borrow a classic example illustrating the reasoning behind encapsulation, consider your own real-world automobile. Every day you "send messages" to your car through the gas pedal, brakes, car radio, window wipers, and so forth. You rest assured that as you send these messages the car kindly responds (unless your warranty has just expired). You need not know or care exactly how the car accelerates, or how the radio plays the music. The details of these operations are encapsulated from view.

In object-oriented languages, a class also encapsulates the details of how it performs in response to the messages you send to it. For example, assume a co-worker has given you a brand new C++ class modeling a number of database-centric operations. As you examine the header file of this class (and the lavish comments your co-worker has included), you discover you can instantiate a new database object, create a file, and add columns using an overloaded constructor and a single method:

// Create a new CDataBaseObj object. CDataBaseObj dbObj("CarsDataBase.txt"); dbObj.AddColumn("Pet Name");              // Send AddColumn message... dbObj.AddColumn("Max Speed");             // Send another AddColumn message... dbObj.AddColumn("Current Speed");         // Send yet another AddColumn message...

Ah, the beauty of encapsulation. As the object user, you have no need to worry about the numerous lines of code that create, open, and manipulate this new text file. You simply create an object and send it messages. Contrast this to traditional C, where we might have a DATABASE structure, and two global functions to create and add columns to the database. This demands that the developer understand not only the data involved, but the set of related functions as well.

Encapsulation allows us to have a tight coupling between data and related functionality. Typically, a class has instance data stored in the private sector. The values of the data held in the private sector constitute the current state of the object. Public member functions of the class (as well as any internal helper functions) operate on the state data internally behind the scenes whenever the object user sends messages. For example, Figure 1-7 illustrates three instances of the CDataBaseObj class, each maintaining a unique state:

click to expand
Figure 1-7: Each instance of a class typically maintains a unique state.

Specifying Encapsulation in C++

In C++, encapsulation is enforced at a syntactic level with the public, private, and protected keywords. Here is a breakdown of each level of visibility:

  • Public: Any item listed under the public sector is directly accessible from the object itself, the object user, or any subclass.

  • Private: Any data point or method listed in the private sector is usable only by the object itself. Neither object users nor any subclass can directly access these items.

  • Protected: Methods and data points listed in the protected sector are accessible from the object itself as well as any descendents in the direct inheritance chain. The object user cannot access members in the protected sector.

In C++, the default visibility of a class is always private. Therefore, if you do not specify otherwise, each and every method or data point defined in your class will be inaccessible from an object instance:

// Visibility of a class is defaulted to private! class CEmployee {      // Can't create this object (implicit private declaration).      CEmployee();      CEmployee(int ID, char* name, float startingSalary);      virtual ~ CEmployee(); };

As a rule of thumb, data will reside in the private sector of a class. In this way, the state of the object is safe from invalid data assignments. The public sector typically provides any number of functions to retrieve and set this private data. Traditionally these methods are called accessors and mutators. Finally, when creating object hierarchies, a protected sector can be used to specify the set of inherited functionality (as we will see soon):

// Visibility levels for a typical class. class CEmployee { public:      // Visible methods. private:      // Hidden methods and data points. protected:      // Data and methods visible to this class and any sub-classes. };

Accessors and Mutators: Safely Changing an Object's State Data

While constructors allow us to initialize an object to a default state, eventually some of the private state data defined by a class will need to be modified and retrieved by the object user over time. For example, assume the CDataBaseObj class maintains a private string data member that holds the current name of the data file. To keep the object's data safe from harm, we do not wish to define data directly in the public sector. Rather, we keep data private and provide public accessors (Get methods) and mutators (Set methods). In this way, we can safely return internal data to the object user and perform any data validations before we make assignments, as illustrated in Figure 1-8:

click to expand
Figure 1-8: Safe access to the private sector of a class.

To preserve the encapsulation of the private data members, the class designer might create a class definition that looks something like the following:

// Definition of the CDataBaseObj class. class CDataBaseObj { public:      CDataBaseObj(char* dbName);      CDataBaseObj();      virtual ~ CDataBaseObj();      // Accessor/mutator pair for internal string.      void SetDBName(char* newName);      char* GetDBName();      void AddColumn(char* newCol); private:      // Instance data for the class.      char m_dbName [MAX_LENGTH];      int m_currCol; };

As the object user can never directly access private members, the following code is illegal:

// Compiler error. Can't access private data. CDataBaseObj myDB; cout << myDB.m_dbName << endl;

Implementing Class Methods: The Scope Resolution Operator

Recall that when implementing class methods, you typically make use of the scope resolution operator (a double colon) to bind a given function implementation to a specific class. Here, we need to scope the SetDBName() and GetDBName() methods to the CData- BaseObj class. The implementation of the mutator method allows us to check the incoming data for a valid range, type, case, or whatnot, before changing the state of our object. The accessor method exists simply to return a copy of our private data to the object user:

// Mutator for a private data point. // The SetDBName() method is bound to the CDataBaseObj class. void CDataBaseObj::SetDBName(char* newName) {      // Here you may interrogate newName against any constraints before      // assigning it to your data type. As well, this method may close down      // the current data base, rename it, and open the new data file to accomplish      // the task at hand.      strcpy(m_dbName, newName) } // Accessor for a private data point. char* CDataBaseObj::GetDBName() {      return m_dbName;  // Safely return the buffer. }

If you accidentally forget to bind a method implementation to a given class, you are bound to introduce a number of unresolved external errors. For example:

// This is not bound to the CDataBaseObj class... // You just created a global function! char* GetDBName() {      return m_dbName;   // Compiler error. No global data point named m_dbName. }

However, with the accessor and mutator methods correctly in place, the object user can now safely set and retrieve the object's instance data:

// Using an accessor and mutator set to safely work with a piece of private data. void main(void) {       // Construct an object.      CDataBaseObj myDB;       // Set the name.      myDB.SetDBName("MyNewDataBase.txt");      // Get the name.      cout << "The database is called:" << myDB.GetDBName(); }

Now that we have a solid understanding of encapsulation in C++, let's rework the previous CarInC program into an object-based solution.

Lab 1-2: A Well-Encapsulated Car

In this lab you will rework the functionality of the CarInC.exe application into an object- based solution using C++. Here you will create a class named (of course) CCar, blending together the global functions and CAR struct from the previous lab into a single cohesive unit. The key point of this lab is to illustrate the encapsulation of data and the use of accessors and mutators in your class design.

 On the CD   The solution for this lab can be found on your CD-ROM under:
Labs\Chapter 01\CarInCPP

Step One: Prepare the Project Workspace and Insert the CCar Class

Start up Visual C++ and create a new empty Win32 Console Application named CarInCPP. Insert a new empty file named main.cpp into the project workspace. We will be making use of C++ IO streams, so include <iostream.h> before defining an empty main loop:

// Entry point for program. #include <iostream.h> void main(void) { } 

Next, we must insert a new class into the program to represent our automobile. The simplest way to do this in the Visual C++ IDE is to right-click on the project node from ClassView and select New Class... from the context menu (see Figure 1-9).


Figure 1-9: Accessing the New Class Wizard.

click to expand
Figure 1-10: The Visual C++ New Class Wizard.

From the resulting New Class Wizard utility, type in the name for this new class and click OK:

This tool will automatically define a default constructor and virtual destructor for the class as well as wrap the class definition with preprocessor calls to prevent multiple redefinition errors. As we will use the same program design notes as CarInC.exe, define some constant data for the speed upper limit and maximum pet name length. You can define these right inside the CCar header file (outside of the class scope), this time making use of the type safe C++ const keyword rather than C #define syntax:

// Program constants. const     int MAX_LENGTH      = 100; const     int MAX_SPEED       = 500;

Step Two: Define CCar's Public Interface

Next we must specify the public interface of our class. In other words, if some namespace in the application creates an instance of our CCar class, what methods are directly accessible to the object user? Define the CCar class to support the following public interface:

// Car.h const int MAX_LENGTH    = 100; const int MAX_SPEED     = 500; #include <iostream.h>          // For C++ IO functions. #include <stdio.h>             // For the gets() function. #include <string.h>            // String stuff... class CCar { public:       // Public interface to the class.      CCar();      virtual ~CCar();      void   DisplayCarStats();      void   SpeedUp();      // It is common to define access functions inline.      int    GetCurrSpeed()   { return m_currSpeed;}      int    GetMaxSpeed()    { return m_maxSpeed;}      char*  GetPetName()     { return m_petName;}      // Generalized 'Set' function.      void   CreateACar(); private:      // Private instance data.      char         m_petName[MAX_LENGTH];      int          m_maxSpeed;      int          m_currSpeed; };

Here, we have moved the global functions from the C example into our class's public sector. Notice how the signatures of these methods have cleaned up in the process: We have no need to send in any parameters to the DisplayCarStats() or SpeedUp() methods, as they internally operate on the object's private state data.

To finish up the class definition, you may wish to add some overloaded constructors that allow the object user to pass in data at the time of declaration. For this lab, the CreateACar() method is called by the object user as a secondary creation step to set all private in a single step (that is why we won't bother with individual Set functions for the data or overloaded constructors). Whatever additional modifications you may add to your CCar class, the point to remember here is that we have physically joined related data and functionality into a well-encapsulated unit.

Step Three: Implement the Methods of CCar

Implementing the methods of CCar is trivial. Add the following to your car.cpp file. The default constructor simply assigns the object's state data to safe empty values, with help from the member initialization list:

// CCar constructor. CCar::CCar() : m_currSpeed(0), m_maxSpeed(0) {      // You could assign the above data to zero within the constructor block,      // however savvy C++ folks prefer the more exotic member initialization list.      strcpy(m_petName, ""); }

CreateACar() prompts for a pet name and maximum speed using gets(), cout, and cin:

// Create a new car based on user input. void CCar::CreateACar() {      char   buffer[MAX_LENGTH];      int    spd = 0;      cout << "Please enter a pet-name for your car: " << flush;      gets(buffer);        // Check against MAX if desired.      strcpy(m_petName, buffer);      do                   // Be sure speed isn't beyond reality...      {           cout << "Enter the max speed of this car: " << flush;           cin >> spd;           cout << endl;      }while(spd > MAX_SPEED);      m_maxSpeed = spd; } 

The only noticeable change to the DisplayCarStats() method is that we use the more elegant and OO-based cout as opposed to printf(). In addition, because this function operates on internal private state data, DisplayCarStats() needs no parameters.

// Implementation of DisplayCarStats. void CCar::DisplayCarStats() {      cout << "***********************************" << endl;      cout << "PetName is: " << m_petName << endl;      cout << "Max Speed is: " << m_maxSpeed << endl;      cout << "***********************************" << endl << endl; }

And finally, SpeedUp() does just as you would expect: increases the current speed by 10 and prints the result using cout:

// Implementation of SpeedUp. void CCar::SpeedUp() {      if(m_currSpeed <= m_maxSpeed)      {           m_currSpeed = m_currSpeed + 10;           cout << "Speed is: " << m_currSpeed << endl;      } }

This completes the class definition and implementation of the CCar class. Go ahead and compile to ensure you have no typos. Now we need to take this class out for a test drive (pun intended).

Step Four: Implement the main() Loop

With our new OO version of the Car functionality complete, the client-side code in main() will clean up quite a bit from the original program in C. All we need to do is send messages to a new CCar instance:

// Program entry point. #include "car.h"      // Don't forget this! #include <iostream.h> void main(void) {      cout << "***********************************" << endl;      cout << "The Amazing Car Application via CPP"<< endl;      cout << "***********************************"<< endl;      CCar myCar;                    // Calls default constructor.      myCar.CreateACar();            // Prompts user for input.      myCar.DisplayCarStats();       // Show state.      // Rev that engine!      while(myCar.GetCurrSpeed() <= myCar.GetMaxSpeed())           myCar.SpeedUp();      // Explosion!      cout << myCar.GetPetName() << " has blown up! Lead foot!" << endl; }

click to expand
Figure 1-11: The C++ car application.

At this point, we have moved a trivial C application into the world of objects in C++. The car example has served us well, and will continue to do so throughout this book. The next time we see our programmatic automobile will be in Chapter 3, where we develop a COM-based DLL to model our automobile logic.

So much for a lonely C++ class living in isolation. Robust OO systems usually involve numerous classes living together, often using one another in the process. In other words, classes can be defined by leveraging existing classes. This, of course, is the story of inheritance, which is the second pillar of OOP.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net