可以工作的类(P126)
这是关于更高效,更合理抽象的一些建议
1.类的基础:抽象数据类型(ADTs)
ADT即abstract data type,抽象数据类型。
ADT可以让你像在现实世界一样操作实体,而不必在低层的实现上摆弄实体。
比如:
currentFont.size = PointsToPixels(12)
currentFont.bold = True
currentFont.attribute = CurrentFont.attribute or BOLD
如果这样做,程序中的很多地方会充斥着类似的代码,需要修改起来的工作量可能会非常大
如果改成
<span style="font-size:12px;">currentFont.SetSizeInPoints(sizeInPoints)
currentFont.SetSizeInPixels(sizeInPixels)
currentFont.SetBoldOn()
currentFont.SetBoldOff()
currentFont.SetItalicOn()
currentFont.SetItalicOff()
currentFont.SetTypeFace(faceName)</span>
好处有
- 可以隐藏实现细节
- 改动不会影响到整个程序
- 让接口能提供更多信息
- 更容易提高性能
- 让程序的正确性更显而易见
- 程序更具自我说明性
- 无须在程序内到处传递数据
- 可以像在现实世界中那样操作实体,而不用在低层实现上操作它
2.良好的类接口
1)好的抽象
一个设计良好的接口也是一个合理的抽象,并确保了细节被隐藏在抽象背后
一个不好的例子:
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:
...
};
在这段代码里,包含了大量混杂的函数,在命令栈、报表和全局数据之间很难看出什么联系。
类的接口不能展现出一种一致的抽象,因此它的内聚性就很弱。
应该把这些子程序重新组织到几个职能更专一的类里去,在这些类的接口中提供更好的抽象。
比如:
class Program{
public:
...
// public routines
void InitializeCommandStack();
void ShutdownCommandStack();
void InitializeReportFormatting();
void ShutdownGlobalData();
...
private:
...
};
同时,类的接口应该展现一致的抽象层次
下面这个例子中,类的接口不够协调,因为它的抽象层次不一致
混合了不同层次抽象的类接口:
class Program{
public:
...
// public routines
//以下子程序的抽象在“雇员”这一层次上
void AddEmployee( Employee employee);
void RemoveEmployee( Employee employee);
//以下子程序的抽象在“列表”这一层次上
Employee NextItemInList();
Employee FirstItem();
Employee LastItem();
...
private:
...
};
这个类展现了两个ADT:Employee和ListContainer。出现这种混合的抽象,通常是源于程序员使用容器类或者其他类库来实现内部逻辑,但却没有把“使用类库”这一事实隐藏起来。
有着一致抽象层次的类接口:
class Program{
public:
...
// public routines
//所有这些子程序的抽象现在都是在“雇员”这一层次上了
void AddEmployee( Employee employee);
void RemoveEmployee( Employee employee);
Employee NextEmployee();
Employee FirstEmployee();
Employee LastEmployee();
...
private:
//使用ListContainer库这一实现细节现在已经被隐藏起来了
ListContainer m_EmployeeList
...
};
封装是一个比抽象更强的概念,强烈阻止你看到其中的细节
比如:
float x;
float y;
float z;
暴露成员数据会破坏封装性,从而限制对其控制能力,因为调用方代码可以自由地使用Point类里面的数据,而Point类却甚至连这些数据什么时候被改动过都不知道。
然而,如果Point类暴露的是这些方法的话:
float GetX();
float GetY();
float GetZ();
void SetX( float x);
void SetY( float x);
void SetZ( float x);
那它还是封装完好的。
如果出现问题都在于,它们让调用方代码不是依赖于类的公开接口,而是依赖于类的私用实现。每当你发现自己是通过查看类的内部实现来得知该如何使用这个类的时候,你就不是在针对接口编程了,而是在透过接口针对内部实现编程了。如果你透过接口来编程的话,封装性就被破坏了,而一旦封装性开始遭到破坏,抽象能力也就快遭殃了。
3.有关设计和实现的问题
1)包含(“有一个.....”的关系)
包含是一个非常简单的概念,它表示一个类有一个基本数据元素或对象。与包含相比,关于继承的论述要好得多,这是因为继承需要更多的技巧,而且更容易出错,而不是因为继承要比包含更好。包含才是面向对象编程中的主力技术。
2)继承(“是一个.....”的关系)(P144)
- 用public继承来实现“是一个......”的关系
- 要么使用继承并进行详细说明,要么就不要用它
- 遵循Liskov替换原则
即“派生类必须能通过基类的接口而被使用,且使用者无须了解两者之间的差异。” - 确保只继承需要继承的部分
- 不要“覆盖”一个不可覆盖的成员函数
即在private情况下,不要重名定义 - 把共用的接口、数据及操作放到继承树中尽可能高的位置
- 只有一个实例的类是值得怀疑的
- 只有一个派生类的基类也值得怀疑
- 派生后覆盖了某个子程序,但在其中没有做任何操作,这种情况也值得怀疑
- 避免让继承体系过深
“神奇数字7+-2”。不过按经验而言,同时应付超过2到3层继承时就有麻烦了。 - 尽量使用多态,避免大量的类型检查
比如频繁出现的case语句,多半应该用多态来替代 - 让所有数据都是private(而非protected)
除非派生类真的需要访问基类的属性
3)成员函数和数据成员
- 让类中子程序的数量尽可能少
- 禁止隐式地产生你不需要的成员函数和运算符
- 减少类所调用的不同子程序的数量
高扇出,底扇入 - 对其他类的子程序的间接调用要尽可能少
比如这样类型的调用尽量避免
account.ContactPerson().DaytimeContactInfo().phoneNumber()
- 一般来说,应尽量减小类和类之间相互合作的范围
4)构造函数
- 如果可能,应该在所有的构造函数中初始化所有的数据成员(防御式编程)
- 用private构造函数来强制实现单件属性(全局类)
public class maxId{ //constructors and destructors private MaxId(){ ... } ... //public routines public static MaxId GetInstance(){ return m_instance; } ... //private members private static final MaxId m_instance = new MaxId(); ... }
- 优先采用深层副本,除非论证可行,才采用浅层副本
即实例化比较多