Professional C__ - Marc Gregoire [104]
BUILDING STABLE INTERFACES
Now that you understand all the gory syntax of writing classes in C++, it helps to revisit the design principles from Chapters 3 and 4. Classes are the main unit of abstraction in C++. You should apply the principles of abstraction to your classes to separate the interface from the implementation as much as possible. Specifically, you should make all data members protected or private and provide getter and setter methods for them. This is how the SpreadsheetCell class is implemented. mValue and mString are protected; set(), getValue(), and getString() set and retrieve those values. That way you can keep mValue and mString in synch internally without worrying about clients delving in and changing those values.
Using Interface and Implementation Classes
Even with the preceding measures and the best design principles, the C++ language is fundamentally unfriendly to the principle of abstraction. The syntax requires you to combine your public interfaces and private (or protected) data members and methods together in one class definition, thereby exposing some of the internal implementation details of the class to its clients. The downside of this is that if you have to add new non-public methods or data members to your class, all the clients of the class have to be recompiled. This can become a burden in bigger projects.
The good news is that you can make your interfaces a lot cleaner and hide all implementation details, resulting in stable interfaces. The bad news is that it takes a bit of hacking. The basic principle is to define two classes for every class you want to write: the interface class and the implementation class. The implementation class is identical to the class you would have written if you were not taking this approach. The interface class presents public methods identical to those of the implementation class, but it only has one data member: a pointer to an implementation class object. The interface class method implementations simply call the equivalent methods on the implementation class object. The result of this is that no matter how the implementation changes, it has no impact on the public interface class. This reduces the need for recompilation. None of the clients that use the interface class need to be recompiled if the implementation (and only the implementation) changes.
To use this approach with the Spreadsheet class, simply rename the old Spreadsheet class to SpreadsheetImpl. Here is the new SpreadsheetImpl class (which is identical to the old Spreadsheet class, but with a different name):
#include "SpreadsheetCell.h"
class SpreadsheetApplication; // Forward declaration
class SpreadsheetImpl
{
public:
SpreadsheetImpl(const SpreadsheetApplication& theApp,
int inWidth = kMaxWidth, int inHeight = kMaxHeight);
SpreadsheetImpl(const SpreadsheetImpl& src);
~SpreadsheetImpl();
SpreadsheetImpl &operator=(const SpreadsheetImpl& rhs);
void setCellAt(int x, int y, const SpreadsheetCell& inCell);
SpreadsheetCell getCellAt(int x, int y);
int getId() const;
static const int kMaxHeight = 100;
static const int kMaxWidth = 100;
protected:
bool inRange(int val, int upper);
void copyFrom(const SpreadsheetImpl& src);
int mWidth, mHeight;
int mId;
SpreadsheetCell** mCells;
const SpreadsheetApplication& mTheApp;
static int sCounter;
};
Code snippet from SeparateImpl\SpreadsheetImpl.h
Then define a new Spreadsheet class that looks like this:
#include "SpreadsheetCell.h"
// Forward declarations
class SpreadsheetImpl;
class SpreadsheetApplication;
class Spreadsheet
{
public:
Spreadsheet(const SpreadsheetApplication& theApp, int inWidth,
int inHeight);
Spreadsheet(const SpreadsheetApplication& theApp);
Spreadsheet(const Spreadsheet& src);
~Spreadsheet();
Spreadsheet&