分类: 项目管理
2008-06-27 10:40:43
创建高质量的类,第一步,可能也是最重要的一步,就是创建一个好的接口。这也包括了创建一个可以通过接口来展现的合理的抽象,并确保细节仍被隐藏在抽象背后。
Good Abstraction
好的抽象
正如第5.3节“形成一致的抽象”中所述,抽象是一种以简化的形式来看待复杂操作的能力。类的接口为隐藏在其后的具体实现提供了一种抽象。类的接口应能提供一组明显相关的子程序。
你可以有一个实现雇员(Employee)这一实体的类。其中可能包含雇员的姓名、地址、电话号码等数据,以及一些用来初始化并使用雇员的服务子程序。看上去可能是这样的
C++示例: 展现良好抽象的类接口
class Employee {
public:
// public constructors and destructors
Employee();
Employee(
FullName name,
String address,
String workPhone,
String homePhone,
TaxId taxIdNumber,
JobClassification jobClass
);
virtual ~Employee();
// public routines
FullName GetName() const;
String GetAddress() const;
String GetWorkPhone() const;
String GetHomePhone() const;
TaxId GetTaxIdNumber() const;
JobClassification GetJobClassification() const;
...
private:
...
};
在类的内部还可能会有支持这些服务的其他子程序和数据,但类的使用者并不需要了解它们。类接口的抽象能力非常有价值,因为接口中的每个子程序都在朝着这个一致的目标而工作。
一个没有经过良好抽象的类可能会包含有大量混杂的函数,就像下面这个例子一样:
C++示例:展现不良抽象的类接口
class Program {
public:
...
// public routines
void InitializeCommandStack();
void PushCommand( Command command );
Command PopCommand();
void ShutdownCommandStack();
void InitializeReportFormatting();
void FormatReport( Report report );
void PrintReport( Report report );
void InitializeGlobalData();
void ShutdownGlobalData();
...
private:
...
};
假设有这么一个类,其中有很多个 子程序,有用来操作命令栈的,有用来格式化报表的,有用来打印报表的,还有用来初始化全局数据的。在命令栈、报表和全局数据之间很难看出什么联系。类的接 口不能展现出一种一致的抽象,因此它的内聚性就很弱。应该把这些子程序重新组织到几个职能更专一的类里去,在这些类的接口中提供更好的抽象。
如果这些子程序是一个叫做Program类的一部分,那么可以这样来修改它,以提供一种一致的抽象:
C++示例:能更好展现抽象的类接口
class Program {
public:
...
// public routines
void InitializeUserInterface();
void ShutDownUserInterface();
void InitializeReports();
void ShutDownReports();
...
private:
...
};
在清理这一接口时,把原有的一些子程序转移到其他更合适的类里面,而把另一些转为InitializeUserInterface()和其他子程序中使用的私用子程序。
这种对类的抽象进行评估的方法是基于类所具有的公用(public)子程序所构成的集合——即类的接口。即使类的整体表现了一种良好的抽象,类内部的子程序也未必就能个个表现出良好的抽象,也同样要把它们设计得可以表现出很好的抽象。你可以在第7.2节“在子程序层次上的设计”里获得相关的指导建议。
为了追求设计优秀,这里给出一些创建类的抽象接口的指导建议:
类的接口应该展现一致的抽象层次 在考虑类的时候有一种很好的方法,就是把类看做一种用来实现抽象数据类型(ADT,见第6.1节)的机制。每一个类应该实现一个ADT,并且仅实现这个ADT。如果你发现某个类实现了不止一个ADT,或者你不能确定究竟它实现了何种ADT,你就应该把这个类重新组织为一个或多个定义更加明确的ADT。
在下面这个例子中,类的接口不够协调,因为它的抽象层次不一致:
C++示例:混合了不同层次抽象的类接口
class EmployeeCensus: public ListContainer {
public:
...
// public routines
void AddEmployee( Employee employee );
void RemoveEmployee( Employee employee );
Employee NextItemInList();
Employee FirstItem();
Employee LastItem();
...
private:
...
};
这个类展现了两个ADT:Employee和ListContainer。出现这种混合的抽象,通常是源于程序员使用容器类或其他类库来实现内部逻辑,但却没有把“使用类库”这一事实隐藏起来。请自问一下,是否应该把使用容器类这一事实也归入到抽象之中?这通常都是属于应该对程序其余部分隐藏起来的实现细节,就像下面这样:
C++示例:有着一致抽象层次的类接口
class EmployeeCensus {
public:
...
// public routines
void AddEmployee( Employee employee );
void RemoveEmployee( Employee employee );
Employee NextEmployee();
Employee FirstEmployee();
Employee LastEmployee();
...
private:
ListContainer m_EmployeeList;
...
};
有的程序员可能会认为从ListContainer继承更方便,因为它支持多态,可以传递给以ListContainer对象为参数的外部查询函数或排序函数来使用。然而这一观点却经不起对“继承”合理性的主要测试:“继承体现了‘是一个……(is a)’关系吗?”如果从ListContainer中继承,就意味着EmployeeCensus“是一个”ListContainer,这显然不对。如果EmployeeCensus对象的抽象是它能够被搜索或排序,这些功能就应该被明确而一致地包含在类的接口之中。
如果把类的公用子程序看做是潜水艇上用来防止进水的气锁阀(air lock), 那么类中不一致的公用子程序就相当于是漏水的仪表盘。这些漏水的仪表盘可能不会让水像打开气锁阀那样迅速进入,但只要有足够的时间,它们还是能让潜艇沉 没。实际上,这就是混杂抽象层次的后果。在修改程序时,混杂的抽象层次会让程序越来越难以理解,整个程序也会逐步堕落直到变得无法维护。
一定要理解类所实现的抽象是什么 一些类非常相像,你必须非常仔细地理解类的接口应该捕捉的抽象到底是哪一个。我曾经开发过这样一个程序,用户可以用表格的形式来编辑信息。我们想用一个简单的栅格(grid)控件,但它却不能给数据输入单元格换颜色,因此我们决定用一个能提供这一功能的电子表格(spreadsheet)控件。