Borland Online And The Cobb Group Present:


December, 1994 - Vol. 1 No. 12

C++ Design guideline - Avoiding set/get member functions

As you become fluent in C++, you'll probably start to avoid placing data members in the public section of a class's declaration and to move the data to the protected or private section of the declaration instead. However, most C++ programmers who do this include some simple access functions in the form SetValue( ) and GetValue( ) (in the public section), which they can use to manipulate those data members.

Unfortunately, using set/get member functions to manipulate an object's data members can make the class difficult to understand and can later lead to maintenance problems. In this article, we'll show how you can design classes that won't need set/get member functions, and why you should do so.

Abstract principles

When you create a class that contains public data members, you're granting other parts of your program access to those members. However, once another part of your program begins using those public data members, you've made that part of the program dependent on the class's data members staying the same. If you change a data member's type­­from a float to a char*, for example­­the other parts of your program that use the data member will need to change accordingly.

First, data abstraction

To minimize the effects of changing data members, many C++ programmers move the public data members into the protected or private section of a class's declaration. As a result, you won't have access to these data members in other parts of your program. To get around this, you might be tempted to write public access functions that modify and retrieve the variables' values, as in the BankAccount class shown in Figure A.


Figure A - The BankAccount class provides set/get member functions for manipulating the balance data member.

class BankAccount
{
 private:
  double balance;

 public:
  double getBalance( );
  void setBalance(double newBalance); 

  // We've intentionally omitted the remainder of
  // the class (constructors, destructors, etc.).
};

Initially, this seems like a valuable step. First of all, you've made it possible to change the data member's type without affecting the other parts of the program, thus implementing what's commonly known as data abstraction. You could, for example, change the balance data member to two int values (dollars and cents) and simply convert those to and from a double value in the setBalance( ) and getBalance( ) access functions.

Unfortunately, these public access functions don't suggest anything to you about the class or how the program will use it. These functions tell you only about the data type you can use to modify the object (a double value) and possibly the name of the underlying data member the functions change.

If, for example, you'll allow an account to create a deficit balance but you'll charge an overdraft fee, that action isn't suggested by this class's public member functions. Whether or not the account will accept additional deposits or allow withdrawals isn't indicated anywhere in the body of this class. To manage withdrawals from an account, you'll need a function (either global or a member of a different class) similar to the one shown in Figure B.


Figure B - The WithdrawFromAccount( ) function depends on the existence of the getBalance( ) member function.

void WithdrawFromAccount(BankAccount& account,
                         double amount)
{
  double tempBalance = account.getBalance( );
  if(amount > tempBalance) // Overdraft...
  {
    tempBalance -= 10.00;  // Penalty charge
  }

  tempBalance -= amount;
  account.setBalance(tempBalance);
} 

What's worse, the presence of these functions implies that the underlying data member will always exist as the class evolves. For the BankAccount class, this may be a reasonable assumption. For other classes, though, it may be more likely that you'll remove data members from the class as you develop the program.

In addition, if you add other data members to the class, you'll be inclined to add similar access functions for those members. If you provide new access functions, you'll again need to change any part of the program that updates or displays the data members for the class.

Finally, providing access to a data member that's a pointer is particularly dangerous. Once another part of the program has a pointer member, assuming the function doesn't return a const pointer or const data, the pointer might as well be a public data member.

Data hiding as an alternative

Instead of creating classes that implement simple data abstraction, you can design your classes to use a more advanced concept called data hiding. To implement data hiding, you'll create public member functions that don't provide any significant clues or details about the internal structure of the class (what its private or protected data members are).

The public member functions you create should instead perform actions that indicate to you (or others) what an object of this class does. Very rarely does it make sense for an object simply to return an internal data member's value.

For example, a program that uses the BankAccount class from Figure A might need to make deposits to and withdrawals from the account. If so, these actions probably need to occur inside member functions of the BankAccount class.

In this spirit, each of the public member functions, like deposit( ), withdraw( ), and PrintBalance( ), would be appropriate for the BankAccount class. However, if the member functions setBalance( ) and getBalance( ) exist merely to let other parts of the program make adjustments to the account balance, they probably don't belong in the BankAccount class.

Now, let's write a simple program in Borland C++ 3.1 that uses a class with a clear, meaningful set of public member functions. To illustrate the idea, we'll write a program that uses an improved version of the BankAccount class.

Class action

To begin, launch the Borland C++ 3.1 DOS Integrated Development Environment (IDE) by entering BC at a DOS command prompt. When the DOS IDE's main screen appears, choose New from the File menu and enter the code from Listing A. (If you'd rather use Borland C++ 4.0, you can create this program as a DOS or EasyWin application.)


Listing A: HIDEDATA.CPP

#include <iostream.h>

class BankAccount
{ private:
   double balance;
   static double overdraftFee;
   long  accountNumber;

  public:
   BankAccount(long acctNum) :
   accountNumber(acctNum) 
     { balance = 0; }
   void deposit(double amount);
   void withdraw(double amount);
   void printBalanceInfo( );
};

double 
BankAccount::overdraftFee = 10.00;

void 
BankAccount::deposit(double amount)
{
  balance += amount;
  cout << "Account #";
  cout << accountNumber << " ";
  cout << "Deposit $";
  cout << amount << endl;
}

void 
BankAccount::withdraw(double amount)
{
  cout << "Account #";
  cout << accountNumber << " ";
  cout << "Withdraw $";
  cout << amount << endl;
  if(amount > balance)
  {
    balance -= overdraftFee;
    cout << " (added overdraft charge ";
    cout << "of $" << overdraftFee;
    cout << ")" << endl;
  }
  balance -= amount;
}

void 
BankAccount::printBalanceInfo( )
{
  cout << "Account #";
  cout << accountNumber << " ";
  cout << "Balance $";
  cout << balance << endl;
}

int 
main( )
{
 BankAccount a(144020469);
 BankAccount b(330102881);

 a.deposit(236.29);
 b.deposit(500.00);

 cout << endl;

 a.withdraw(250.00);
 b.withdraw(750.00);

 cout << endl;

 a.printBalanceInfo( );
 b.printBalanceInfo( );

 return 0;
}

When you've finished entering this code, choose Save from the File menu. In the Save As dialog box, enter HIDEDATA.CPP in the Save File As entry field and click OK. Then, choose Run from the Run menu to build and execute the program.

After the program runs, choose Output from the Window menu. You'll see the following:

Account #144020469 Deposit $236.29
Account #330102881 Deposit $500

Account #144020469 Withdraw $250
 (added overdraft charge of $10)
Account #330102881 Withdraw $750
 (added overdraft charge of $10)

Account #144020469 Balance $-23.71
Account #330102881 Balance $-260

By moving all the balance calculations inside the BankAccount class, you've made it possible to keep more of the information that relates to accounts in the class.

If you later decide to keep track of the transactions for each account inside the class, you could remove the cout statements from the deposit( ) and withdraw( ) member functions. You could then modify the printBalanceInfo( ) function to display a summary of recent transactions along with the account balance.

By keeping the deposit, withdraw, and printing behavior inside the BankAccount class, you've also made it easier to remember what part of the program is responsible for these actions. (Many programmers would put all the BankAccount class's member functions in the same source file.) If you used global functions similar to the WithdrawFromAccount( ) function in Figure B, that function might be in a different source file than a similar DepositToAccount( ) function elsewhere.

When to use set/get functions

So far, we've discussed only why you should avoid set/get functions. There are, however, some valid reasons to include this type of function in a class.

First, you might use set/get functions to modify model-specific parameters. A model-specific parameter is a value or state that directly describes the abilities of an object in terms of the system or item the class models.

For example, if you derived a new class InterestBearingAccount from the BankAccount class, you'd need to monitor and modify the interest rate each account uses. Since the interest rate for an account is part of the real-world system the class represents, it may be appropriate for something outside the class to request or adjust this value.

Another reason for using set/get functions is to allow a class to cooperate with existing library functions or classes. However, if you're providing a getString( ) member function to pass an internal string to some external routine­­the MessageBox( ) or OutputDebugString( ) Windows API functions, for example­­you should try to prevent other parts of the program from changing the string by declaring the function's return value as const *.

Conclusion

To avoid placing data members in the public section of a class declaration, you may use set/get member functions that allow other parts of the program to use those internal data members. If, instead, you intentionally avoid set/get member functions, you'll force yourself to write member functions that are more meaningful for the behavior of a particular class.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.