《软件设计的五大原则.ppt》由会员分享,可在线阅读,更多相关《软件设计的五大原则.ppt(51页珍藏版)》请在三一办公上搜索。
1、软件设计中的5大原则,1.单一职责原则(SRP),陈述:就一个类而言,应该只有一个导致其变化的原因分析:一个职责就是一个变化的轴线一个类如果承担的职责过多,就等于将这些职责耦合在一起。一个职责的变化可能会虚弱或者抑止这个类完成其它职责的能力多职责将导致脆弱性的臭味,Rectangle类具有两个职责:计算矩形面积的数学模型将矩形在一个图形设备上描述出来,示例1:,Rectangle类违反了SRP,具有两个职能计算面积和绘制矩形这种对SRP的违反将导致两个方面的问题:包含不必要的代码一个应用可能希望使用Retangle类计算矩形的面积,但是却被迫将绘制矩形相关的代码也包含进来一些逻辑上毫无关联的原
2、因可能导致应用失败如果GraphicalApplication的需求发生了变化,从而对Rectangle类进行了修改。但是这样的变化居然会要求我们重新构建、测试以及部署ComputationalGeometryApplication,否则其将莫名其妙的失败。,修改后的设计如下:,Modem类(可能)有两个职责:拨号通信,示例2:一个Modem的接口:Class Modempublic:virtual void dail(char*pno)=0;virtual void hangup()=0;virtual void send(char c)=0;virtual void recv()=0;,什
3、么是职责?职责是“变化的原因”。上面的例子可能存在两种变化的方式:连接和通信可能独立变化在这种情况下,应该将职责分开。例如,应用的变化导致了连接部分方法的签名(signature)发生了变化,那么使用数据连接的部分也需要重新编译、部署,这会相当麻烦,使得设计僵化。连接和通信同时变化这种情况下,不必将职责分开。反而分离可能导致“不必要的复杂性”的臭味,刻舟求剑是错误的。王亚沙,修改后的设计如下:,有一点需要注意:在ModemImplementation中实际还是集合了两个职责。这是我们不希望的,但是有时候却是必须的。但是我们注意到,对于应用的其它部分,通过接口的分离我们已经实现了职责的分离。Mo
4、demImplementation已经不被其它任何程序所依赖。除了main以外,其他所有程序都不需要知道这个函数的存在。,常见错误提醒:持久化与业务规则的耦合。例如:,业务规则经常变化,而持久化方法却一般不变。将这两个职责耦合在一起,将导致每次因为业务规则变化调整Employee类时,所有持久化部分的代码也要跟着变化,2.开放封闭原则(OCP),陈述:软件实体(类、模块、函数等)应该是可以扩展的,同时还可以是不必修改的,更确切的说,函数实体应该:(1)对扩展是开放的当应用的需求变化时,我们可以对模块进行扩展,使其具有满足改变的新的行为即,我们可以改变模块的功能(2)对更改是封闭的对模块进行扩展
5、时,不必改动已有的源代码或二进制代码。分析:世界是变化的(而且变化很快),软件是对现实的抽象软件必须能够扩展如果任何修改都需要改变已经存在的代码,那么可能导致牵一发动全身现象,进而导致雪崩效应,使软件质量显著下降,实现OCP的关键是抽象:例子1,class clientserver,class serverint serverData;public:void ServerFunc();,例子1(续)这个程序出了什么问题?,client和server都是具体类,接口与实现没有实现分离。如果我们想要让client调用一个新的server类,那么我们不得不修改client的源代码从而带来编译、链接、
6、部署等一系列的问题。,见下页程序,2.开放封闭原则(OCP),例子1(续),class clientserver,class serverint serverData;public:void ServerFunc();,class server1int serverData;public:void ServerFunc();,class clientserver1,2.开放封闭原则(OCP),例子1(续)修改后的设计,class clientClientInterface,class ClientInterfacevirtual void ServerFunc()=0;class server
7、:public ClientInterfaceint serverData;public:void ServerFunc();,例子1(续)一个问题:为什么上述的ClientInterface这个类要取这么个名字,叫做AbastractServer岂不更好?其实这里面蕴含了一个思想:client类中更多的描述了高层的策略,而Server类中是对这些策略的一种具体实现。而接口是策略的一个组成部分,他根client端的关系更加密切我们应该这样想问题:ClientInterface中定义了client期 望Server做什么,而server具体类是对client这种要求的 一种具体实现。OCP原则其
8、实是要求我们清晰的区分策略和策略的具体实现形式。允许 扩展具体的实现形式(开放),同时将这种扩展与策略隔离开来,使 其对上层的策略透明(封闭)。,例子2C语言程序,-shape.h-emum ShapeTypecircle,square;struct ShapeShapeType itsType;-circle.h-struct CircleShapeType itsType;double itsRadius;CPoint itscenter;-square.h-struct SquareShapeType itsType;double itsSide;CPoint itsTopLeft;,-
9、drawAllShapes.cpp-typedef struct Shape*ShapePointer;void DrawAllShapes(ShapePointer list,int n)int i;for(i=0;iitsType)case square:s-Square();break;case circle:s-DrawCircle();break;,例子2(续)批判这个程序不符合OCP,如果需要处理的几何图形中再加入“三角形”将引发大量的修改僵化的增加Triangle会导致Shape、Square、Circle以及DrawAllShapes的重新编译和部署脆弱的因为存在大量的既难以查
10、找又难以理解的Switch和If语句,修改稍有不慎,程序就会莫明其妙的出错牢固的想在一个程序中复用DrawAllShapes,都必须带上Circle、Square,即使那个程序不需要他们,例子2(续)修改后的设计,class Shapepublic:virtual void Draw()const=0;class Square:public Shapepublic:virtual void Draw()const;class Circle:public Shapepublic:virtual void Draw()const;,void DrawAllShapes(Vector,例子2(续)再
11、看这些批判再加入“三角形”将变得十分简单:僵化的增加Triangle会导致Shape、Square、Circle以及DrawAllShapes的重新编译和部署脆弱的因为存在大量的既难以查找又难以理解的Switch和If语句,修改稍有不慎,程序就会莫明其妙的出错牢固的想在一个程序中复用DrawAllShapes,都必须带上Circle、Square,即使那个程序不需要他们,谎言:上述代码并不完全封闭“如果我们希望正方形在所有圆之前绘制”会怎么样?对绘图的顺序无法实现封闭更糟糕的是,刚才的设计反而成为了实现“正方形在所有圆之前绘制”功能的障碍。,真实的谎言:一般而言,无论模块多么“封闭”,都会存在
12、一些无法对之封闭的变化没有对所有变化的情况都封闭的模型我们怎么办?既然不可能完全封闭,我们必须有策略的对待此问题对模型应该封闭那类变化作出选择,封闭最可能出现的变化 这需要对领域的了解,丰富的经验和常识错误的判断反而不美,因为OCP需要额外的开销(增加复杂度)敏捷的思想我们预测他们,但是直到我们发现他们才行动,回到例2:要实现对排序的封闭应该如何设计?,class Shapepublic:virtual void Draw()const=0;virtual bool Precedes(const Shape,对于各个Shape的派生类,需要实现具体的排序规则Circle类的排序规则实现如下:B
13、ool Circle:Precedes(const Shape这个程序符合OCP吗?,利用“数据驱动”的方法获得封闭性,#include#include#include using namespace std;class Shapepublic:virtual void Draw()const=0;virtual bool Precedes(const Shape,bool Shape:Precedes(const Shape,通过上述方法,我们成功地做到了一般情况下DrawAllShapes函数对于顺序问题的封闭,也使得每个Shape派生类对于新的Shape派生类的创建者或者给予类型的Sha
14、pe对象排序规则的改变是封闭的。对于不同的Shapes的绘制顺序的变化不封闭的唯一部分就是表本身。可以把表放置在一个单独的模块中,和所有其他模块隔离,因此对于表的改动不会影响其他任何模块。事实上在C中我们可以在链接时选择要使用的表。,3.LisKov替换原则(LSP),陈述:子类型(Subtype)必须能够替换他们的基类型(Basetype)Barbara Liskov对原则的陈述:若对每个类型S的对象o1,都存在一个类型T的对象o2,使得在所有针对T编写的程序P中,用o1替换o2后,程序P的行为功能不变,则S是T的子类型。分析:违法这个职责将导致程序的脆弱性和对OCP的违反例如:基类Base
15、,派生类Derived,派生类实例d,函数f(Base*p);f(&d)会导致错误显然D对于f是脆弱的。如果我们试图编写一些测试,以保证把d传给f时可以使f具有正确的行为。那么这个测试违反了OCP因为f无法对Base的所有派生类都是封闭的,示例1:,struct Pointdouble x,y;struct shapeenum ShapeTypesquare,circle itsType;shape(ShapeType t):itsType(t);struct Circle:public ShapeCircle():Shape(circle);void Draw()const;Point it
16、sCenter;double itsRadius;struct Square:public ShapeSquare():Shape(square);void Draw()const;Point itsTopLeft;double itsSide;,void DrawShape(const Shape,显然,DrawShape违反了OCP,为什么?因为Circle和Square违反了LSP,示例2:一次更加奇妙的违规,class RectanglePoint topLeft;doulbe width;double height;public:void setWidth(double w)widt
17、h=w;void setHeight(double h)height=h;double getWidth()constreturn width;double getHeight()constreturn height;class Square:public Rectanglepublic:void setWidth(double w);void setHeight(double h);,void Square:setWidth(double w)Rectangle:setWidth(w);Rectangle:setHeight(w);void Square:setHeight(double h
18、)Rectangle:setWidth(h);Rectangle:setHeight(h);,问题的第一步分析:看下面这个函数,void f(Rectangle,问题:显然,当我们将一个Square的实例传给f时,将可能导致其height与width不等,破坏了其完整性违反了LSP,要改正上述问题,很简单,我们只要将SetWidth和SetHeight两个函数设置成virtual函数即可添加派生类需要修改基类,通常意味着设计上的缺陷,但是并非所有人都同意上述的分析,反方:真正的设计缺陷是忘记把SetWidth和SetHeight两个函数作为virtual函数,正方:设置长方形的长宽是非常基本的
19、操作,不是预见到有正方形这样的派生类,怎么会想到要将其设成虚函数?,放下这个争论,我们先将SetWidth和SetHeight改作虚函数看看,class RectanglePoint topLeft;doulbe width;double height;public:virtual void setWidth(double w)width=w;virtual void setHeight(double h)height=h;double getWidth()constreturn width;double getHeight()constreturn height;class Square:p
20、ublic Rectanglepublic:void setWidth(double w);void setHeight(double h);,void Square:setWidth(double w)Rectangle:setWidth(w);Rectangle:setHeight(w);void Square:setHeight(double h)Rectangle:setWidth(h);Rectangle:setHeight(h);,看起来,很不错!,真正的问题:,void g(Rectangle,函数g不能操作Square的实例,Square不能替换Rectangle,所以违反了L
21、SP,LSP告诉我们:孤立的看,我们无法判断模型的有效性考虑一个设计是否恰当时,不能孤立的看待并判断,应该从此设计的使用者所作出的假设来审视它!,事先的推测是困难的,我们采用敏捷的思想推迟这个判断“一个模型是否违反LSP”。直到出现问题的时候我们才解决它。,更加深入的思索:,这个看似明显正确的模型怎么会出错呢?“正方形是一种长方形”地球人都知道错在哪里?,对不是g函数的编写者而言,正方形可以是长方形,但是对g函数的编写者而言,Square绝对不是Rectangle!,OOD中对象之间是否存在IS-A关系,应该从行为的角度来看待。而行为可以依赖客户程序做出合理的假设。,基于契约(和约)的设计DB
22、C(Deign by Contract)“合理的假设”使人郁闷。我怎么知道是否合理呢?使用DBC,类的编写者需要显示的规定针对该类的契约。客户代码的编写者可以通过契约获悉行为的依赖方式。契约通过为每一个方法规定前置条件(preconditions)和后置条件(postconditions)来指定的。要使一个方法执行,前置条件一定要为真(对客户的要求);函数执行后要保证后置条件为真(对函数编写者的要求)。,基于契约(和约)的设计DBC(Deign by Contract)(续)例如:在上面的例子中,Rectangle:SetWidth(double w)的后置条件可以看作是:assert(its
23、Width=w)基类和派生类在前置条件和后置条件上的关系是:如果在派生类中重新申明了基类中已有的成员函数,这个函数只能使用相等或更弱的前置条件来替换原有的前置条件;并且,只能使用相等或更强的后置条件来替换原有的后置条件。派生类必须接受基类已经接受的一切;并且,派生类不能违反基类已经确定的规则。在一些语言中明确的支持契约,例如Eiffel,你申明它们,运行时系统会自动的检查。在Jave和C标准中尚未支持,我们必须自己考虑。,4.依赖倒置原则(DIP),陈述:高层模块不应该依赖于低层模块。二者应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。分析:所谓“倒置”是相对于传统的开发方法(例如结
24、构化方法)中总是倾向于让高层模块依赖于低层模块而言的软件结构而言的。高层包含应用程序的策略和业务模型,而低层包含更多的实现细节,平台相关细节等。高层依赖低层将导致:难以复用。通常改变一个软硬件平台将导致一些具体的实现发生变化,如果高层依赖低层,这种变化将导致逐层的更改。难以维护。低层通常是易变的。,层次化:“所有良构的OO体系结构都具有清晰的层次定义,每个层次通过一个定义良好的、受控的接口向外提供了一组内聚的服务。”Booch对上述论述可能存在两种不同的理解:简单的理解,u1()u2(),g()u1();u2();,p()g();,层次化(续):更好的理解,依赖关系倒置下层的实现,依赖于上层的
25、接口接口所有权倒置客户拥有接口,而服务者则从这些接口派生,依赖不倒置的开发自顶向下首先设计整个软件的分解结构然后首先实现下层的功能再实现上层的功能,并使上层调用下层函数依赖倒置的开发首先设计上层需要调用的接口,并实现上层然后低层类从上层接口派生,实现低层接口属于上层,示例1(Button与Lamp):Button(开关)感知外界的变化。当接受到Poll(轮询)消息时,判断其是否被“按下”。这个按下是抽象的(不关心通过什么样的机制去感知):可能是GUI上的一个按钮被鼠标单击可能是一个真正的按钮被手指按下可能是一个防盗装置检测到了运动Lamp(灯)根据指示,收到turn on消息显示某种灯光,收到
26、turn off消息关闭灯光可能是计算机控制台的LED可能是停车场的日光灯可能是激光打印机中的激光应该如何设计程序来用Button控制Lamp呢?,一个不成熟的设计Button对象直接依赖Lamp对象,从而:Lamp的任何变化都会影响到Button,导致其改写或者重新编译黑盒方式重用Button来控制一个Motor类变得不可能,class ButtonLamp*itsLamp;public:void poll()if(/*some condition*/)itsLamp-turnOn();.;,一个依赖倒置的设计依赖于抽象什么是高层策略?就是应用背后的抽象背后的抽象是检测用户的开/关指令用什么
27、机制检测用户的指令?无关紧要目标对象是什么?无关紧要他们不会影响到抽象的具体细节改进后的设计:,Button依赖于抽象的接口ButtonServer(向该接口发消息)。ButtonServer提供一些抽象的方法,Button类通过这些接口可以开启或关掉一些东西。Lamp也依赖于ButtonServer接口(从此接口派生),提供具体的实现。,部分代码:,/Button.h#include ButtonServer.hclass ButtonButtonServer*bs;public:void poll();/Button.cppvoid Button:poll()if(/*mechanism
28、for detecting turnOn command*/)bs-turnOn();else if(/*mechanism for detecting turnOff command*/)bs-turnOff();,/ButtonServer.hclass ButtonServerpublic:virtual void turnOn()=0;virtual void turnOff()=0;/lamp.hclass Lamp:public ButtonServerpublic:void turnOn();void turnOff();/lamp.cppvoid Lamp:turnOn()/*
29、codes for turn on a specific device*/void Lamp:turnOff()/*codes for turn off a specific device*/,分析:上述设计使得Button可以控制所有愿意实现ButtonServer接口的设备,甚至是一个尚未开发出来的设备。,质疑:这样的设计是不是强加了这样一个约束所有需要被Button控制的对象一定要实现ButtonServer类。如果我的设备还希望能够被另一个对象控制,比如Switch控制,怎么办?这种设计是不是将Button对Lamp的依赖转嫁成了Lamp对Button的依赖呢?(毕竟Lamp只能被一种
30、Button控制也是不好的),抗辩:上述质疑不成立。Button依赖于ButtonServer接口,但是接口并不依赖于Button,也就是说任何知道如何操作ButtonServer接口的对象都可以操作Lamp。也许需要改进的仅仅是ButtonServer这样一个有些“误导性”的名字,我们可以将这个名字该得更加抽象一些,例如:SwitchableDevice,5.接口隔离原则(ISP),陈述:不应该强迫客户依赖于他们不用的方法一个类的不内聚的“胖接口”应该被分解成多组方法,每一组方法都服务于一组不同的客户程序。,先说一个例子:,class Doorpublic:virtual void Lock
31、()=0;virtual void Unlock()=0;virtual bool IsDoorOpen()=0;,Door可以加锁、解锁、而且可以感知自己是开还是关;Door是抽象基类,客户程序可以依赖于抽象而不是具体的实现,增加功能,如果门打开时间过长,它就会报警。(比如宾馆客房的门),为了实现上述新增功能,我们要求Door与一个已有的Timer对象进行交互,class Timerpublic:void Register(int timeout,TimerClient*client);class TimerClientpublic:virtual void TimerOut();,如果一个
32、对象希望得到超时通知,它可以调用Timer的Register函数。该函数有两个参数,一个是超时时间,另一个是指向TimerClient对象的指针,此对象的TimerOut函数会在超时时被调用,我们如何将TimerClient和TimedDoor联系起来?,问题,一种常见的解决方案如下:,问题接口污染在Door接口中加入新的方法(Timeout),而这个方法仅仅只为它的一个子类带来好处。如果每次子类需要一个新方法时它都被加到基类接口中,基类接口将很快变胖。,胖接口将导致SRP,LSP被违反,从而导致脆弱、僵化,客户的反作用力:通常接口的变化将导致client的改变但是很多时候,接口之所以变化是因
33、为客户需要他们变化 Client对interface具有反作用力!,class Timerpublic:void Register(int timeout,int timeOutId,TimerClient*client);class TimerClientpublic:virtual void TimerOut(int timeOutId);,TimedDoor的多个超时请求问题,导致Timer接口做出下面的调整:,TimedDoor对Timer接口的影响会传递到Door接口,从而导致所有Door都受到此影响,而且这一影响还会影响到Door的所有Clients牵一发动全身,解决之道:使用委托,Class TimedDoor:public Doorpublic:virtual void DoorTimeOut(int timeOutID);class DoorTimeAdapter:public TimerClientTimedDoor,解决之道(续):使用多继承,Class TimedDoor:public Door,public TimerClientpublic:virtual void DoorTimeOut(int timeOutID);,