C++开发初级


1145 浏览 5 years, 4 months

11 创建稳定的接口

版权声明: 转载请注明出处 http://www.codingsoho.com/

创建稳定的接口

现在您己经理解了在C++中编写类的所有语法,重新回顾前面的设计原则会对此有所帮助。在C++中类是主要的抽象单元,应该将抽象原则应用到类,从而尽可能分离接口以及实现。确切地讲,应该将所有数据成员设置为protected或者private,并提供相应的getter以及setter方法。这就是SpreadsheetCell类将mValue以及mString设置为protected的原因,set(),getValue()以及getString()用于设置或者获取这些值。

这样就可以使mValue以及mString在内部保持同步,同时也不需要担心用户无意中修改这些值。

使用接口类以及实现类

即使提前进行了估算并且采用了最佳的设计原则,C++语言本质上对抽象原则并不友好。其语法要求将public接口以及private(或者protected)数据成员以及方法放在一个类定义中,从而将类的某些内部实现细节向客户公开。

这种做法的缺点在于,如果您不得不在类中加入新的非公有方法或者数据成员,所有的客户都必须重新编译,对于比较大的项目这是一个负担。 有一个好消息:您可以创建清晰的接口并且将实现细节隐藏,从而得到一个稳定的接口。还有一个坏消息:这样做有点繁琐。基本原则是为您想编写的每个类都定义两个类:接口类以及实现类

实现类与您己经编写的类相同(如果您没有采用这种方法),接口类给出了与实现类一样的public方法,但是只有一个数据成员:指向实现类对象的一个指针。接口类方法的实现只是简单地调用实现类对象的等价方法。这样做的结果是无论实现如何改变,都不会影响类的public接口类,从而降低了重新编译的必要性。当实现(只有实现)改变的时候,使用接口类的客户不需要重新编译。

为了将这种方法应用到Spreadsheet类,只需要简单地将旧Spreadsheet类重新命名为SpreadsheetImpl。下面是新的SpreadsheetImpl类(与己有的Spreadsheet类一样,但是名称不同):

// SpreadsheetImpl.h
#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& cell);
  SpreadsheetCell getCellAt(int x, int y);

  int getId() const;

  // Initializing here doesn't work in some compilers
  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;
}; 

代码取自 SeparateImpl\SpreadsheetImpl.h

新的Spreadsheet类的定义如下:

#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& operator=(const Spreadsheet& rhs);
  void setCellAt(int x, int y, const SpreadsheetCell& inCell);
  SpreadsheetCell getCellAt(int x, int y);
  int getId() const;

 protected:
  SpreadsheetImpl* mImpl;
};

代码取自 SeparateImpl\Spreadsheet.h

这个类现在只包含一个数据成员:指向SpreadsheetImpl的一个指针。public方法与旧的Spreadsheet相同,区别只有一个:采用默认参数的Spreadsheet构造函数被分为两个构造函数,因为作为默认参数值的const成员不再存在于Spreadsheet类中,而是由SpreadsheetImpl类提供默认值。

Spreadsheet方法的实现(例如setCellAt()以及getCellAt())只是将请求传递给底层的Spreadsheet-Impl对象:

void Spreadsheet::setCellAt(int x, int y, const SpreadsheetCell& inCell)
{
  mImpl->setCellAt(x, y, inCell);
}

SpreadsheetCell Spreadsheet::getCellAt(int x, int y)
{
  return mImpl->getCellAt(x, y);
}

int Spreadsheet::getId() const
{
  return mImpl->getId();
}

代码取自 SeparateImpl\Spreadsheet.cpp

Spreadsheet的构造函数必须创建一个新的SpreadsheetImpl来完成这个任务,析构函数还必须释放动态分配的内存。注意Spreadsheetimpl类只有一个具有默认参数的构造函数。Spreadsheet类的普通构造函数都调用SpreadsheetImpl类的这个构造函数:

Spreadsheet::Spreadsheet(const SpreadsheetApplication &theApp, int inWidth,
             int inHeight) 
{
  mImpl = new SpreadsheetImpl(theApp, inWidth, inHeight);
}

Spreadsheet::Spreadsheet(const SpreadsheetApplication& theApp) 
{
  mImpl = new SpreadsheetImpl(theApp);
}

Spreadsheet::Spreadsheet(const Spreadsheet& src)
{
  mImpl = new SpreadsheetImpl(*(src.mImpl));
}

Spreadsheet::~Spreadsheet()
{
  delete mImpl;
  mImpl = nullptr;
}

代码取自 SeparateImpl\Spreadsheet.cpp

复制构造函数看上去有点奇怪,因为需要从源Spreadsheet复制底层的SpreadsheetImpl。由于复制构造函数采用了一个SpreadsheetImpl的引用而不是指针,因此为了获取对象本身,必须对mImpl指针解除引用,这样构造函数就可以使用它的引用做参数。

Spreadsheet赋值运算符必须采用类似方式将值传递给底层的SpreadsheetImpl:

Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
  *mImpl = *(rhs.mImpl);
  return *this;
}

代码取自 SeparateImpl\Spreadsheet.cpp

赋值运算符的第一行看上去有点奇怪。您或许会试图用下面的行替换:

mImpl = rhs.mImpl; //Incorrect assignment

代码可以编译并运行,但是结果并不是您想要的。这行代码只是复制了指针,因此左右两边的Spreadsheets指针都指向同一个SpreadsheetImpl。

如果其中一个改变了SpreadsheetImpl,这一改动在另一个指针也会表现出来。如果其中一个执行了销毁,另一个就成为悬挂指针,因此不能只对指针赋值。您必须强制运行SpreadsheetImpl赋值运算符,而这个运算符只有复制直接对象的时候才会执行。通过对mImpl指针解除引用,会强制使用直接对象赋值,从而调用赋值运算符。注意您只能这么做,因为在构造函数中己经为mImpl分配了内存。

真正地将接口以及实现分离的技术功能强大。尽管开始的时候有一点笨拙,一旦您适应了这种技术您就会觉得这么做很自然。然而,在多数工作环境中这并不是常规做法,因此您会发现这么做的时候会遇到来自同事的一些阻力。

支持这种方法最有力的论据不是将接口分离的美感,而是类的实现改变后彻底地重新生成一次所付出的代价,大型项目彻底地重新生成一次可能耗费数小时的时间。使用接口稳定的类,可以最大限度地缩短重新生成时间,预编译头文件的概念可以进一步降低生成的开销。