类设计五项基本原则
原则:
单一职责原则
开放封闭原则
Liskov替换原则
依赖倒置原则
接口隔离原则
第8章 单一职责原则 ( SRP )
就一个类而言.应该仅有一个引起它变化的原因.
一个class就其整体应该只提供单一的服务 如果一个class提供多样的服务,那么就应该把它拆分,反之,如果一个在概念上单一的功能却由几个class负责,这几个class应该合并
第9章 开放-封闭原则 ( OCP )
软件实体(类. 模块. 函数等等)应该是可以扩展的. 但是不可修改的.
例如.把一个类的功能抽象出来.形成一个抽象接口.然后对该接口编程.这样当需要扩展时只要从该接口派生一个
新类就可以完成扩展的功能.
看一个例子: 一个保存形状的链表. 打印其中的每个形状. 形状可能是圆.可能是矩形.要求先打印所有的圆形.
第一种方案:
struct 形状{
bool 是否圆形?
//其他数据
};
形状 [N] ; //数组;
for(i=0; i<N; ++i){
if ( 是圆形 ) 绘制圆形.
else 绘制矩形;
}
这就是一个糟糕的设计. 如果增加第三种图形如三角形. 改动是很麻烦的.
第2种方案:
struct 图形 {
virtual void draw() = 0;
};
struct 圆形 : public 图形{
void draw() { 绘制圆 }
//...
};
struct 矩形 : public 图形{
void draw() { 绘制矩形 }
//...
};
vector<图形*> v;
foreach(v.begin(), v.end(), mem_fun(&图形::draw) );
这种设计 . 如果要增加三角形的种类. 只要从图形类派生就可以了. 绘制部分不用改动.
第三种设计:
前两种设计都暂时没有考虑"先输出圆形"这个要求.
我们为vector<图形*> 排序设计了一个比较图形*的函数:
struct 图形{
bool bijiao(const 图形& s) const {
if (dynamic_cast<矩形*>(&s) ) return true;
else return false;
}
//..
};
这个bijiao的函数经过包装就可以用在sort()中对vector<图形*>排序.
但...这个函数不具有封闭性.如果增加一个三角形类. 这个函数还要改动..
另一种设计:
将bijiao函数使用"表格驱动"的方法.获得排序功能的封闭性:
class Shape {
public:
virtual void draw() const = 0;
bool bijiao(const Shape&) const;
private:
static const char* typeOrderTable[];
};
const char* Shape::typeOrderTable[] = {
typeid(Circle).name(),
typeid(Square).name(),
0
};
然后在bijiao函数中. 根据 typeid(*this).name() 和 typeid(s).name()在表格中的位置.来比较.
这样如果增加了三角形. 想调整输出的顺序为 先三角形. 再圆. 再矩形. 只要添加三角形类.
并修改类型名表格就可以了.
一个设计并实现好的class,应该对扩充的动作开放,而对修改的动作封闭 也就是说,这个class应该是允许扩充的,但不允许修改 如果需要功能上的扩充,一般来说应该通过添加新类实现,而不是修改原类的代码 添加新类不单可以通过直接继承,也可以通过组合
第10章 Liskov 替换原则 ( LSP )
Barbara Liskov说: 所有针对基类编的程序. 在用派生类替换后. 程序的行为不变.
违反这一规则的例子是 : 让正方形从矩形派生.
虽然正方形看起来 IsA 矩形. 但它们的行为不是. 例如:
class Rectangle { //矩形
void setwidth( double ); //这两个函数对正方形来说. 行为和矩形不同.
void setheight( double );
//...
};
这就是一个违法LSP规则的设计. 因为把正方形作为矩形的派生类. 但它没有"可替换性".
第11章 依赖倒置原则 ( DIP )
高层模块不应该依赖于低层模块. 二者都应该依赖于抽象.
抽象不应该依赖于细节. 细节应该依赖于抽象.
为什么叫"倒置"呢. 因为在结构化分析和设计的传统开发方法里.经常是高层依赖低层模块.而这在
面向对象设计时是糟糕的. 因为那意味着对低层模块的改动会直接影响到高层模块.从而迫使高层整个
作出改动.
高层模块不能依赖于低层模块. 这是"框架设计"时的核心原则.
"高层模块应该依赖于抽象接口.而不应该依赖于具体类." 根据这一规则:
任何变量都不应该持有一个指向具体类的指针或引用.
任何类都不应该从具体类派生.
任何方法都不应该覆写它的任何基类中已经实现了的方法.
事实上. 对于象Java中的String这样低层的类. 高层的模块可以依赖它. 因为它是稳定的.
例如: 有个按钮类 (Button) 控制 灯类 (Lamp) 的打开(turnOn)和关闭(trunOff).
糟糕的设计(违反了DIP) :
public class Button {
private Lamp itsLame; //依赖具体的类 Lamp
public void poll() {
if (...) itsLamp.turnOn();
}
}
上边的Button类依赖于低层的 Lamp 类. 要解除对Lamp的依赖. 我们抽象出一个抽象接口:
interface ButtonServer; 然后Button只操作ButtonServer接口. 而让Lamp类从该
接口派生.
第12章 接口隔离原则 (ISP)
有些对象.它们的接口不是内聚的. ISP建议将它们分为多个具有内聚接口的抽象基类.
例如: 有个Door对象. 它可以被锁和被解锁. 如:
class Door {
public:
virtual void Lock() = 0;
virtual void Unlock() = 0;
virtual bool IsDoorOpen() = 0;
};
现在需要一个 TimedDoor 类. 它会在门打开一定时间后发出警报声. 自动提醒关门.
现在有个 Timer 类:
class Timer {
public:
//向Timer 对象 注册一个 TimerClient对象. 当指定的时间timeout到达时. 它自动
//向TimerClient对象发送 TimeOut() 消息;
void Register( int timeout, TimerClient* client);
};
class TimerClient{
public:
virtual void TimeOut() = 0;
};
现在设计一个TimedDoor 类. 下边是个糟糕的设计:
让 Door 接口 继承自 TimerClient . 这样Door就有了 TimerOut()纯虚函数.
然后让 TimedDoor 类 实现 Door 接口. 就可以将 TimedDoor对象向Timer对象注册.
这个设计有个问题. 让 Door 接口 继承自 TimerClient . 但并不是所有的Door都要有定时功能.
所以在这个设计中. Door的接口变"胖" . 被 TimeOut()函数 污染了 .
解决的办法是分离接口. 下边有两种办法:
1. 使用委托分离接口:
创建一个派生自TimerClient的对象. 并把对该对象的请求委托给TimedDoor. 如:
class TimedDoor : public Door {
public:
virtual void DoorTimeOut ( );
};
class DoorTimeAdapter : public TimerClient {
public:
DoorTimerAdapter( TimedDoor& theDoor) : itsTimedDoor(theDoor) {}
virtual void TimeOut() {
itsTimedDoor.DoorTimeOut();
}
private:
TimedDoor& itsTimedDoor;
};
这样. 如果有:
TimedDoor td;
并有个Timer 的对象 tm . 就可以用委托类 DoorTimeAdapter 来注册 :
tm.Register( new DoorTimeAdapter(td) );
2. 使用多继承分离接口
使TimedDoor继承自 Door 和 TimerClient :
class TimeDoor : public Door, public TimerClient {
public:
virtual void DoorTimeOut();
};