shape*s=list[i];
switch(s->type){
caseisSquare:
drawSquare((square*)s);
break;
caseisCircle:
drawCircle((circle*)s);
break;
}
}
}
增加1个新的图形需要修改哪些地方?
•drawShapes不是封闭的
–switch/case可能需要出现在多个地方
–增加一个图形→修改switch/case
–逻辑复杂
–扩展枚举类型ShapeType→重新编译所有的程序
•这是一个僵化的、脆弱的、具有很高的牢固性的设计
良好的设计:
开闭原则的相对性
软件系统的构建是一个需要不断重构的过程,在这个过程中,模块的功能抽象,模块与模块间的关系,都不会从一开始就非常清晰明了,所以构建100%满足开闭原则的软件系统是相当困难的,这就是开闭原则的相对性。
但在设计过程中,通过对模块功能的抽象(接口定义),模块之间的关系的抽象(通过接口调用),抽象与实现的分离(面向接口的程序设计)等,可以尽量接近满足开闭原则。
参考资料:
Martin,RobertC.(1996January).“TheOpen-ClosedPrinciple”
二、里氏替换原则(LiskovSubstitutionPrinciple,LSP)
所有引用基类的地方必须能透明地使用其子类的对象。
也就是说,只有满足以下2个条件的OO设计才可被认为是满足了LSP原则:
-不应该在代码中出现if/else之类对子类类型进行判断的条件。
以下代码就违反了LSP定义。
if(objtypeofClass1){
dosomething
}elseif(objtypeofClass2){
dosomethingelse
}
-子类应当可以替换父类并出现在父类能够出现的任何地方,或者说如果我们把代码中使用基类的地方用它的子类所代替,代码还能正常工作。
里氏替换原则LSP是使代码符合开闭原则的一个重要保证。
同时LSP体现了:
-类的继承原则:
如果一个派生类的对象可能会在基类出现的地方出现运行错误,则该子类不应该从该基类继承,或者说,应该重新设计它们之间的关系。
-动作正确性保证:
从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入新的错误。
示例:
这里Rectangle是基类,Square从Rectangle继承。
这种继承关系有什么问题吗?
假如已有的系统中存在以下既有的业务逻辑代码:
voidg(Rectangle&r)
{
r.SetWidth(5);
r.SetHeight(4);
assert(r.GetWidth()*r.GetHeight())==20);
}
则对应于扩展类Square,在调用既有业务逻辑时:
Rectangle*square=newSquare();
g(*square);
时会抛出一个异常。
这显然违反了LSP原则。
例如鲸鱼和鱼,应该属于什么关系?
从生物学的角度看,鲸鱼应该属于哺乳动物,而不是鱼类。
没错,在程序世界中我们可以得出同样的结论。
如果让鲸鱼类去继承鱼类,就完全违背了Liskov替换原则。
因为鱼作为父类,很多特性是鲸鱼所不具备的,例如通过腮呼吸,以及卵生繁殖。
那么,二者是否具有共性呢?
有,那就是它们都可以在水中"游泳",从程序设计的角度来说,它们都共同实现了一个支持"游泳"行为的接口。
例如运动员和自行车例子,每个运动员都有一辆自行车,如果按照下面设计,很显然违反了LSP原则。
classBike{
public:
voidMove();
voidStop();
voidRepair();
protected:
intChangeColor(int);
private:
intmColor;
};
classPlayer:
privateBike
{
public:
voidStartRace();
voidEndRace();
protected:
intCurStrength();
private:
intmMaxStrength;
intmAge;
};
如果两个具体的类A,B之间的关系违反了LSP的设计,(假设是从B到A的继承关系),那么根据具体的情况可以在下面的两种重构方案中选择一种:
1.创建一个新的抽象类C,作为两个具体类的超类,将A,B的共同行为移动到C中来解决问题。
2.从B到A的继承关系改为关联关系。
对于长方形和正方形例子,可以构造一个抽象的四边形类,把长方形和正方形共同的行为放到这个四边形类里面,让长方形和正方形都是它的子类,问题就OK了。
对于长方形和正方形,取width和height是它们共同的行为,但是给width和height赋值,两者行为不同,因此,这个抽象的四边形的类只有取值方法,没有赋值方法。
对于鱼和鲸鱼例子,可以按下图重新设计:
对于运动员和自行车例子,可以采用关联关系来重构:
classPlayer
{
public:
voidStartRace();
voidEndRace();
protected:
intCurStrength();
private:
intmMaxStrength;
intmAge;
Bike*abike;
};
在进行设计的时候,我们尽量从抽象类继承,而不是从具体类继承。
如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口。
当然这个只是一个一般性的指导原则,使用的时候还要具体情况具体分析.
动作正确性保证:
因为LSP对子类的约束,所以为已存在的类做扩展构造一个新的子类时,根据LSP的定义,不会给已有的系统引入新的错误。
DesignbyContract
根据BertrandMeyer氏提出的DesignbyContract(DBC:
基于合同的设计)概念的描述,对于类的一个方法,都有一个前提条件以及一个后续条件,前提条件说明方法接受什么样的参数数据等,只有前提条件得到满足时,这个方法才能被调用;同时后续条件用来说明这个方法完成时的状态,如果一个方法的执行会导致这个方法的后续条件不成立,那么这个方法也不应该正常返回。
现在把前提条件以及后续条件应用到继承子类中,子类方法应该满足:
1)前提条件不强于基类.
2)后续条件不弱于基类.
换句话说,通过基类的接口调用一个对象时,用户只知道基类前提条件以及后续条件。
因此继承类不得要求用户提供比基类方法要求的更强的前提条件,亦即,继承类方法必须接受任何基类方法能接受的任何条件(参数)。
同样,继承类必须顺从基类的所有后续条件,亦即,继承类方法的行为和输出不得违反由基类建立起来的任何约束,不能让用户对继承类方法的输出感到困惑。
这样,我们就有了基于合同的LSP,基于合同的LSP是LSP的一种强化。
•矩形的契约
–高度和宽度是独立的,可以修改一个值而另一个值保持不变,面积=高度*宽度
•正方形打破了此契约
在很多情况下,在设计初期我们类之间的关系不是很明确,LSP则给了我们一个判断和设计类之间关系的基准:
需不需要继承,以及怎样设计继承关系。
参考资料:
LiskovSubstitutionPrinciple(LSP)的原文
三、迪米特原则(最少知道原则)(LawofDemeter ,LoD)
迪米特法则(LawofDemeter)又叫最少知道原则(LeastKnowledgePrinciple),1987年秋天由美国NortheasternUniversity的IanHolland提出,被UML的创始者之一Booch等普及。
后来,因为在经典著作《ThePragmaticProgrammer》中提出而广为人知。
迪米特法则可以简单说成:
talkonlytoyourimmediatefriends,只与你直接的朋友们通信,不要跟“陌生人”说话。
对于面向OOD来说,又被解释为下面几种方式:
1)一个软件实体应当尽可能少地与其他实体发生相互作用。
2)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
朋友圈的确定
“朋友”条件:
1)当前对象本身(this)
2)以参量形式传入到当前对象方法中的对象
3)当前对象的实例变量直接引用的对象
4)当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
5)当前对象所创建的对象
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。
迪米特法则的初衷在于降低类之间的耦合。
由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
迪米特法则不希望类直接建立直接的接触。
如果真的有需要建立联系,也希望能通过它的友元类来转达。
因此,应用迪米特法则有可能造成的一个后果就是:
系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系-这在一定程度上增加了系统的复杂度。
例如,购房者要购买楼盘A、B、C中的楼,他不必直接到楼盘去买楼,而是可以通过一个售楼处去了解情况,这样就减少了购房者与楼盘之间的耦合,如图所示。
下面的代码在方法体内部依赖了其他类,这严重违反迪米特法则
classTeacher{
public:
voidcommond(GroupLeadergroupLeader){
listlistGirls=newlist;
for(inti=0;i<20;i++){
listGirls.add(newGirl());
}
groupLeader.countGirls(listGirls);
}
}
方法是类的一个行为,类竟然不知道自己的行为与其他类产生了依赖关系,这是不允许的。
正确的做法是:
classTeacher{
public:
voidcommond(GroupLeadergroupLeader){
groupLeader.countGirls();
}
}
classGroupLeader{
private:
listlistGirls;
public:
GroupLeader(list_listGirls){
this.listGirls=_listGirls;
}
voidcountGirls(){
cout<<"女生数量是:
"<}
}
注意:
一个类只和朋友交流,不与陌生类交流,不要出现getA().getB().getC().getD()这种情况(这种访问在一种极端情况下允许出现,即每一个点号后面的返回类型都相同),类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象。
∙朋友间也是有距离的
一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。
因此,为了保持朋友类间的距离,在设计时需要反复衡量:
是否还可以再减少public方法和属性,是否可以修改为private等。
注意:
迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、protected等访问权限。
∙是自己的就是自己的
如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。
四、单一职责原则(SingleResponsibilityPrinciple,SRP)
永远不要让一个类存在多个改变的理由。
换句话说,如果一个类需要改变,改变它的理由永远只有一个。
如果存在多个改变它的理由,就需要重新设计该类。
单一职责原则原则的核心含意是:
只能让一个类有且仅有一个职责。
这也是单一职责原则的命名含义。
为什么一个类不能有多于一个以上的职责呢?
如果一个类具有一个以上的职责,那么就会有多个不同的原因引起该类变化,而这种变化将影响到该类不同职责的使用者(不同用户):
1,一方面,如果一个职责使用了外部类库,则使用另外一个职责的用户却也不得不包含这个未被使用的外部类库。
2,另一方面,某个用户由于某个原因需要修改其中一个职责,另外一个职责的用户也将受到影响,他将不得不重新编译和配置。
这违反了设计的开闭原则,也不是我们所期望的。
职责的划分
既然一个类不能有多个职责,那么怎么划分职责呢?
Robert.CMartin给出了一个著名的定义:
所谓一个类的一个职责是指引起该类变化的一个原因。
如果你能想到一个类存在多个使其改变的原因,那么这个类就存在多个职责。
SingleResponsibilityPrinciple(SRP)的原文
里举了一个Modem的例子来说明怎么样进行职责的划分,这里我们也沿用这个例子来说明一下:
SRP违反例:
Modem.cpp
classModem{
public:
voiddial(Stringpno); //拨号
voidhangup(); //挂断
voidsend(charc); //发送数据
charrecv(); //接收数据
};
咋一看,这是一个没有任何问题的接口设计。
但事实上,这个接口包含了2个职责:
第一个是连接管理(dial,hangup);另一个是数据通信(send,recv)。
很多情况下,这2个职责没有任何共通的部分,它们因为不同的理由而改变,被不同部分的程序调用。
所以它违反了SRP原则。
下面的类图将它的2个不同职责分成2个不同的接口,这样至少可以让客户端应用程序使用具有单一职责的接口:
让ModemImplementation实现这两个接口。
我们注意到,ModemImplementation又组合了2个职责,这不是我们希望的,但有时这又是必须的。
通常由于某些原因,迫使我们不得不绑定多个职责到一个类中,但我们至少可以通过接口的分割来分离应用程序关心的概念。
事实上,这个例子一个更好的设计应该是这样的,如图:
例如,考虑下图的设计。
Retangle类具有两方法,如图。
一个方法把矩形绘制在屏幕上,另一个方法计算矩形的面积。
有两个不同的Application使用Rectangle类,如上图。
一个是计算几何面积的,Rectangle类会在几何形状计算方面给予它帮助。
另一个Application实质上是绘制一个在舞台上显示的矩形。
这一设计违反了单一职责原则。
Rectangle类具有了两个职责,第一个职责是提供一个矩形形状几何数据模型;第二个职责是把矩形显示在屏幕上。
对于SRP的违反导致了一些严重的问题。
首先,我们必须在计算几何应用程序中包含核心显示对象的模块。
其次,如果绘制矩形Application发生改变,也可能导致计算矩形面积Application发生改变,导致不必要的重新编译,和不可预测的失败。
一个较好的设计是把这两个职责分离到下图所示的两个完全不同的类中。
这个设计把Rectangle类中进行计算的部分一道GeometryRectangle类中。
现在矩形绘制方式的改变不会对计算矩形面积的应用产生影响了。
单一职责原则从职责(改变理由)的侧面上为我们对类(接口)的抽象的颗粒度建立了判断基准:
在为系统设计类(接口)的时候应该保证它们的单一职责性。
参考资料
TheSingleResponsibilityPrinciple(SRP)
byRobertC.Martin.
五、接口分隔原则(InterfaceSegregationPrinciple,ISP)
不能强迫用户去依赖那些他们不使用的接口。
换句话说,使用多个专门的接口比使用单一的总接口总要好。
它包含了2层意思:
-接口的设计原则:
接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。
如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口。
-接口的依赖(继承)原则:
如果一个接口a继承另一个接口b,则接口a相当于继承了接口b的方法,那么继承了接口b后的接口a也应该遵循上述原则:
不应该包含用户不使用的方法。
反之,则说明接口a被b给污染了,应该重新设计它们的关系。
如果用户被迫依赖他们不使用的接口,当接口发生改变时,他们也不得不跟着改变。
换而言之,一个用户依赖了未使用但被其他用户使用的接口,当其他用户修改该接口时,依赖该接口的所有用户都将受到影响。
这显然违反了开闭原则,也不是我们所期望的。
下面我们举例说明怎么设计接口或类之间的关系,使其不违反ISP原则。
假如有一个Door,有lock,unlock功能,另外,可以在Door上安装一个Alarm而使其具有报警功能。
用户可以选择一般的Door,也可以选择具有报警功能的Door。
有以下几种设计方法:
ISP原则的违反例:
方法一:
在Door接口里定义所有的方法。
图:
但这样一来,依赖Door接口的CommonDoor却不得不实现未使用的alarm()方法。
违反了ISP原则。
方法二:
在Alarm接口定义alarm方法,在Door接口定义lock,unlock方法,Door接口继承Alarm接口。
跟方法一一样,依赖Door接口的CommonDoor却不得不实现未使用的alarm()方法。
违反了ISP原则。
遵循ISP原则的例:
方法三:
通过多重继承实现
在Alarm接口定义alarm方法,在Door接口定义lock,unlock方法。
接口之间无继承关系。
CommonDoor实现Door接口,AlarmDoor有2种实现方案:
1)同时实现Door和Alarm接口。
2)继承CommonDoor,并实现Alarm接口。
第2)种方案更具有实用性。
这种设计遵循了ISP设计原则。
方法四:
通过委托实现
在这种方法里,AlarmDoor实现了Alarm接口,同时把功能lock和unlock委让给CommonDoor对象完成。
这种设计遵循了ISP设计原则。
接口分隔原则从对接口的使用上为我们对接口抽象的颗粒度建立