软件设计的五大原则

上传人:1395****376 文档编号:240724313 上传时间:2024-05-03 格式:PPT 页数:52 大小:1.70MB
返回 下载 相关 举报
软件设计的五大原则_第1页
第1页 / 共52页
软件设计的五大原则_第2页
第2页 / 共52页
软件设计的五大原则_第3页
第3页 / 共52页
点击查看更多>>
资源描述
软件设计的五大原则软件设计的五大原则1.单一职责原则(单一职责原则(SRP)陈述:陈述:就一个类而言,应该只有一个导致其变化的原因就一个类而言,应该只有一个导致其变化的原因分析:分析:一个职责就是一个变化的轴线一个职责就是一个变化的轴线一个类如果承担的职责过多,就等于将这些职责耦合在一个类如果承担的职责过多,就等于将这些职责耦合在一起。一个职责的变化可能会虚弱或者抑止这个类完成一起。一个职责的变化可能会虚弱或者抑止这个类完成其它职责的能力其它职责的能力多职责将导致多职责将导致脆弱脆弱性的臭味性的臭味Rectangle+draw()+area():doubleComputational Geometry ApplicationGraphical ApplicationGUIRectangleRectangle类具有两个职责:类具有两个职责:1.1.计算矩形面积的数学模型计算矩形面积的数学模型2.2.将矩形在一个图形设备上描述出来将矩形在一个图形设备上描述出来示例示例1 1:RectangleRectangle类违反了类违反了SRPSRP,具有两个职能,具有两个职能计算面积和计算面积和绘制矩形绘制矩形这种对这种对SRPSRP的违反将导致两个方面的问题:的违反将导致两个方面的问题:包含不必要的代码包含不必要的代码一个应用可能希望使用一个应用可能希望使用RetangleRetangle类计算矩形的面积,但是却被迫类计算矩形的面积,但是却被迫将绘制矩形相关的代码也包含进来将绘制矩形相关的代码也包含进来一些逻辑上毫无关联的原因可能导致应用失败一些逻辑上毫无关联的原因可能导致应用失败如果如果GraphicalApplicationGraphicalApplication的需求发生了变化,从而对的需求发生了变化,从而对RectangleRectangle类进行了修改。但是这样的变化居然会要求我们重新构建、测试类进行了修改。但是这样的变化居然会要求我们重新构建、测试以及部署以及部署ComputationalGeometryApplicationComputationalGeometryApplication,否则其将莫名其,否则其将莫名其妙的失败。妙的失败。修改后的设计如下:修改后的设计如下:ModemModem类(可能)有两个职责:类(可能)有两个职责:1.1.拨号拨号2.2.通信通信示例示例2 2:一个一个ModemModem的接口:的接口:Class ModemClass Modempublic:public:virtual void dail(char*pno)=0;virtual void dail(char*pno)=0;virtual void hangup()=0;virtual void hangup()=0;virtual void send(char c)=0;virtual void send(char c)=0;virtual void recv()=0;virtual void recv()=0;什么是职责?什么是职责?职责是职责是“变化的原因变化的原因”。上面的例子可能存在两种变化的方式:上面的例子可能存在两种变化的方式:连接和通信可能独立变化连接和通信可能独立变化在这种情况下,应该将职责分开。例如,应用的变化导致了连接部分在这种情况下,应该将职责分开。例如,应用的变化导致了连接部分方法的签名(方法的签名(signaturesignature)发生了变化,那么使用数据连接的部分也需)发生了变化,那么使用数据连接的部分也需要重新编译、部署,这会相当麻烦,使得设计要重新编译、部署,这会相当麻烦,使得设计僵化僵化。连接和通信同时变化连接和通信同时变化这种情况下,不必将职责分开。反而分离可能导致这种情况下,不必将职责分开。反而分离可能导致“不必要的复杂性不必要的复杂性”的臭味的臭味刻舟求剑是错误的。刻舟求剑是错误的。王亚沙王亚沙修改后的设计如下:修改后的设计如下:有一点需要注意:在有一点需要注意:在ModemImplementationModemImplementation中中实际还是集合了两个职实际还是集合了两个职责。这是我们不希望的,责。这是我们不希望的,但是有时候却是必须的。但是有时候却是必须的。但是我们注意到,对于但是我们注意到,对于应用的其它部分,通过应用的其它部分,通过接口的分离我们已经实接口的分离我们已经实现了职责的分离。现了职责的分离。ModemImplementationModemImplementation已已经不被其它任何程序所经不被其它任何程序所依赖。除了依赖。除了mainmain以外,以外,其他所有程序都不需要其他所有程序都不需要知道这个函数的存在。知道这个函数的存在。常见错误提醒:常见错误提醒:持久化与业务规则的耦合。持久化与业务规则的耦合。例如:例如:业务规则经常变化,而业务规则经常变化,而持久化方法却一般不变。持久化方法却一般不变。将这两个职责耦合在一将这两个职责耦合在一起,将导致每次因为业起,将导致每次因为业务规则变化调整务规则变化调整EmployeeEmployee类时,所有持类时,所有持久化部分的代码也要跟久化部分的代码也要跟着变化着变化 2.开放封闭原则(开放封闭原则(OCP)陈述:陈述:软件实体(类、模块、函数等)应该是可以扩展的,同时还可以是软件实体(类、模块、函数等)应该是可以扩展的,同时还可以是不必修改的,更确切的说,函数实体应该:不必修改的,更确切的说,函数实体应该:(1 1)对扩展是开放的)对扩展是开放的当应用的需求变化时,我们可以对模块进行扩展,使其具有满足改当应用的需求变化时,我们可以对模块进行扩展,使其具有满足改变的新的行为变的新的行为即,我们可以改变模块的功能即,我们可以改变模块的功能(2 2)对更改是封闭的)对更改是封闭的对模块进行扩展时,不必改动已有的源代码或二进制代码。对模块进行扩展时,不必改动已有的源代码或二进制代码。分析:分析:世界是变化的(而且变化很快),软件是对现实的抽象世界是变化的(而且变化很快),软件是对现实的抽象软件必须能够扩展软件必须能够扩展如果任何修改都需要改变已经存在的代码,那么可能导致牵一发动如果任何修改都需要改变已经存在的代码,那么可能导致牵一发动全身现象,进而导致雪崩效应,使软件质量显著下降全身现象,进而导致雪崩效应,使软件质量显著下降实现实现OCPOCP的关键是抽象:的关键是抽象:例子例子1 1class clientserver&s;public:client(server&SER):s(SER)void useServer()s.ServerFunc();class serverint serverData;public:void ServerFunc();例子例子1 1(续)(续)这个程序出了什么问题?这个程序出了什么问题?clientclient和和serverserver都是具体类,接口与实现没有实现分离。如果我们都是具体类,接口与实现没有实现分离。如果我们想要让想要让clientclient调用一个新的调用一个新的serverserver类,那么我们不得不修改类,那么我们不得不修改clientclient的源代码的源代码从而带来编译、链接、部署等一系列的问题。从而带来编译、链接、部署等一系列的问题。见下页程序2.开放封闭原则(开放封闭原则(OCP)例子例子1 1(续)(续)class clientserver&s;public:client(server&SER):s(SER)void useServer()s.ServerFunc();class serverint serverData;public:void ServerFunc();class server1int serverData;public:void ServerFunc();class clientserver1&s;public:client(server&SER):s(SER)void useServer()s.ServerFunc();2.开放封闭原则(开放封闭原则(OCP)例子例子1 1(续)(续)修改后的设计修改后的设计class clientClientInterface&ci;public:client(ClientInterface&CI):ci(CI)void useServer()ci.ServerFunc();class ClientInterfacevirtual void ServerFunc()=0;class server:public ClientInterfaceint serverData;public:void ServerFunc();例子例子1 1(续)(续)一个问题:一个问题:为什么上述的为什么上述的ClientInterfaceClientInterface这个类要取这么个名字,叫做这个类要取这么个名字,叫做AbastractServerAbastractServer岂不更好?岂不更好?其实这里面蕴含了一个思想:其实这里面蕴含了一个思想:clientclient类中更多的描述了高层的策略,而类中更多的描述了高层的策略,而ServerServer类中是对这些策略的一种类中是对这些策略的一种具体实现。具体实现。而接口是策略的一个组成部分,他根而接口是策略的一个组成部分,他根clientclient端的关系更加密切端的关系更加密切我们应该这样想问题:我们应该这样想问题:ClientInterfaceClientInterface中定义了中定义了clientclient期期 望望ServerServer做什么,而做什么,而serverserver具体类是对具体类是对clientclient这种要求的这种要求的 一种具体实现。一种具体实现。OCPOCP原则其实是要求我们清晰的区分策略和策略的具体实现形式。允原则其实是要求我们清晰的区分策略和策略的具体实现形式。允许许 扩展具体的实现形式(开放),同时将这种扩展与策略隔离开来,使扩展具体的实现形式(开放),同时将这种扩展与策略隔离开来,使 其对上层的策略透明(封闭)。其对上层的策略透明(封闭)。例子例子2 2C C语言程序语言程序-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;-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 2(续)(续)批判批判这个程序不符合这个程序不符合OCPOCP,如果需要处理的几何图形中再加入,如果需要处理的几何图形中再加入“三角形三角形”将将引发大量的修改引发大量的修改僵化的僵化的增加增加TriangleTriangle会导致会导致ShapeShape、SquareSquare、CircleCircle以及以及DrawAllShapesDrawAllShapes的重新编的重新编译和部署译和部署脆弱的脆弱的因为存在大量的既难以查找又难以理解的因为存在大量的既难以查找又难以理解的SwitchSwitch和和IfIf语句,修改稍有不慎,语句,修改稍有不慎,程序就会莫明其妙的出错程序就会莫明其妙的出错牢固的牢固的想在一个程序中复用想在一个程序中复用DrawAllShapesDrawAllShapes,都必须带上,都必须带上CircleCircle、SquareSquare,即使,即使那个程序不需要他们那个程序不需要他们例子例子2 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&list)vector:iterator i;for(i=list.begin();i!=list.end();i+)(*i)-Draw();例子例子2 2(续)(续)再看这些批判再看这些批判再加入再加入“三角形三角形”将变得十分简单:将变得十分简单:僵化的僵化的增加增加TriangleTriangle会导致会导致ShapeShape、SquareSquare、CircleCircle以及以及DrawAllShapesDrawAllShapes的重新编的重新编译和部署译和部署脆弱的脆弱的因为存在大量的既难以查找又难以理解的因为存在大量的既难以查找又难以理解的SwitchSwitch和和IfIf语句,修改稍有不慎,语句,修改稍有不慎,程序就会莫明其妙的出错程序就会莫明其妙的出错牢固的牢固的想在一个程序中复用想在一个程序中复用DrawAllShapesDrawAllShapes,都必须带上,都必须带上CircleCircle、SquareSquare,即使,即使那个程序不需要他们那个程序不需要他们谎言:谎言:上述代码并不完全封闭上述代码并不完全封闭“如果我们希望正方形在所有圆之前绘如果我们希望正方形在所有圆之前绘制制”会怎么样?会怎么样?对绘图的顺序无法实现封闭对绘图的顺序无法实现封闭更糟糕的是,刚才的设计反而成为了实现更糟糕的是,刚才的设计反而成为了实现“正方形在所正方形在所有圆之前绘制有圆之前绘制”功能的障碍。功能的障碍。真实的谎言:真实的谎言:一般而言,无论模块多么一般而言,无论模块多么“封闭封闭”,都会存在一些无法对之封闭的,都会存在一些无法对之封闭的变化变化没有对所有变化的情况都封闭的模型没有对所有变化的情况都封闭的模型我们怎么办?我们怎么办?既然不可能完全封闭,我们必须有策略的对待此问题既然不可能完全封闭,我们必须有策略的对待此问题对模型应该对模型应该封闭那类变化作出选择,封闭最可能出现的变化封闭那类变化作出选择,封闭最可能出现的变化 这需要对领域的了解,丰富的经验和常识这需要对领域的了解,丰富的经验和常识错误的判断反而不美,因为错误的判断反而不美,因为OCPOCP需要额外的开销需要额外的开销(增加复杂度)(增加复杂度)敏捷的思想敏捷的思想我们预测他们,但是直到我们发现他们才行动我们预测他们,但是直到我们发现他们才行动回到例回到例2 2:要实现对排序的封闭应该如何设计?要实现对排序的封闭应该如何设计?class Shapeclass Shapepublic:public:virtual void Draw()const=0;virtual void Draw()const=0;virtual bool Precedes(const Shape&)const=0;virtual bool Precedes(const Shape&)const=0;bool operatorconst Shape&s)return Precedes(s);bool operatorconst Shape&s)return Precedes(s);templatetemplateclass Lesspclass Lessppublic:bool operator()const P p,const P q)return(*p)(*q);public:bool operator()const P p,const P q)return(*p)(*q);void DrawAllShapes(vector&list)void DrawAllShapes(vector&list)vector orderedList=list;vector orderedList=list;sort(orderedList.begin(),orderedList.end(),Lessp();sort(orderedList.begin(),orderedList.end(),Lessp();vector:const_iterator I;vector:const_iterator I;for(i=orderedList.begin();i!=orderedList.end();i+)for(i=orderedList.begin();i!=orderedList.end();i+)(*i)-Draw();(*i)-Draw();对于各个对于各个ShapeShape的派生类,需要实现具体的排序规则的派生类,需要实现具体的排序规则CircleCircle类的排序规则实现如下:类的排序规则实现如下:Bool Circle:Precedes(const Shape&s)constBool Circle:Precedes(const Shape&s)constif(dynamic_cast(s)if(dynamic_cast(s)return true;return true;elseelsereturn false;return false;这个程序符合这个程序符合OCPOCP吗?吗?利用利用“数据驱动数据驱动”的方法获得封闭性的方法获得封闭性#include#include#include using namespace std;class Shapepublic:virtual void Draw()const=0;virtual bool Precedes(const Shape&)const;bool operator 0)&(thisOrd 0)done=true;else/table entry=0done=true;return thisOrd argOrd;通过上述方法,我们成功地做到了一般情况下通过上述方法,我们成功地做到了一般情况下DrawAllShapesDrawAllShapes函数对于顺序问函数对于顺序问题的封闭,也使得每个题的封闭,也使得每个ShapeShape派生类对于新的派生类对于新的ShapeShape派生类的创建者或者给予类派生类的创建者或者给予类型的型的ShapeShape对象排序规则的改变是封闭的。对象排序规则的改变是封闭的。对于不同的对于不同的ShapesShapes的绘制顺序的变化不封闭的唯一部分就是表本身。可以把表的绘制顺序的变化不封闭的唯一部分就是表本身。可以把表放置在一个单独的模块中,和所有其他模块隔离,因此对于表的改动不会影响放置在一个单独的模块中,和所有其他模块隔离,因此对于表的改动不会影响其他任何模块。其他任何模块。事实上在事实上在C C中我们可以在链接时选择要使用的表。中我们可以在链接时选择要使用的表。3.LisKov替换原则(替换原则(LSP)陈述:陈述:子类型(子类型(SubtypeSubtype)必须能够替换他们的基类型)必须能够替换他们的基类型(Basetype)(Basetype)Barbara LiskovBarbara Liskov对原则的陈述:对原则的陈述:若对每个类型若对每个类型S S的对象的对象o o1 1,都存在一个类型都存在一个类型T T的对象的对象o o2 2,使得在所有针,使得在所有针对对T T编写的程序编写的程序P P中,用中,用o o1 1替换替换o o2 2后,程序后,程序P P的行为功能不变,则的行为功能不变,则S S是是T T的子类型。的子类型。分析:分析:违法这个职责将导致程序的违法这个职责将导致程序的脆弱脆弱性和对性和对OCPOCP的违反的违反例如:基类例如:基类BaseBase,派生类,派生类DerivedDerived,派生类实例,派生类实例d,d,函数函数f(Base*p);f(Base*p);f(&d)f(&d)会导致错误会导致错误显然显然D D对于对于f f是脆弱的。是脆弱的。如果我们试图编写一些测试,以保证把如果我们试图编写一些测试,以保证把d d传给传给f f时可以使时可以使f f具有正确的行具有正确的行为。那么这个测试违反了为。那么这个测试违反了OCPOCP因为因为f f无法对无法对BaseBase的所有派生类都是的所有派生类都是封闭的封闭的示例示例1 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 itsCenter;double itsRadius;struct Square:public ShapeSquare():Shape(square);void Draw()const;Point itsTopLeft;double itsSide;void DrawShape(const Shape&s)if(s.itsType=Shape:square)static_cast(s).Draw();else if(s.itsType=Shape:circle)static_cast(s).Draw();显然,显然,DrawShapeDrawShape违反了违反了OCPOCP,为什么?为什么?因为因为CircleCircle和和SquareSquare违反了违反了LSPLSP示例示例2 2:一次更加奇妙的违规:一次更加奇妙的违规class RectanglePoint topLeft;doulbe width;double height;public:void setWidth(double w)width=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)Rectangle:setWidth(h);Rectangle:setHeight(h);问题的第一步分析:问题的第一步分析:看下面这个函数看下面这个函数void f(Rectangle&r)r.SetWidth(32);问题:显然,当我们将一个Square的实例传给f时,将可能导致其height与width不等,破坏了其完整性违反了LSP要改正上述问题,很简单,我们只要将要改正上述问题,很简单,我们只要将SetWidthSetWidth和和SetHeightSetHeight两个函数设置成两个函数设置成virtualvirtual函数即可函数即可添加派生类需要修改基类,通常意味着设计上的缺陷添加派生类需要修改基类,通常意味着设计上的缺陷但是并非所有人都同意上述的分析但是并非所有人都同意上述的分析反方:真正的设计缺陷是忘记把反方:真正的设计缺陷是忘记把SetWidthSetWidth和和SetHeightSetHeight两个函数作为两个函数作为virtualvirtual函数函数正方:设置长方形的长宽是非常基本的操作,不是预见到有正方形这样的派生类,怎么正方:设置长方形的长宽是非常基本的操作,不是预见到有正方形这样的派生类,怎么会想到要将其设成虚函数?会想到要将其设成虚函数?放下这个争论,我们先将放下这个争论,我们先将SetWidthSetWidth和和SetHeightSetHeight改作虚函数看看改作虚函数看看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: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)Rectangle:setWidth(h);Rectangle:setHeight(h);看起来,很不错!真正的问题:真正的问题:void g(Rectangle&r)r.setWidth(5);r.setHeight(4);assert(r.Area()=20);函数g不能操作Square的实例,Square不能替换Rectangle,所以违反了LSPLSPLSP告诉我们:告诉我们:孤立的看,我们无法判断模型的有效性孤立的看,我们无法判断模型的有效性考虑一个设计是否恰当时,不能孤立的看待并判断,应该从此设计的使用考虑一个设计是否恰当时,不能孤立的看待并判断,应该从此设计的使用者所作出的假设来审视它!者所作出的假设来审视它!事先的推测是困难的,我们采用敏捷的思想事先的推测是困难的,我们采用敏捷的思想推迟这个判断推迟这个判断“一个模型是否违反一个模型是否违反LSPLSP”。直到出现问题的时候我们才解。直到出现问题的时候我们才解决它。决它。更加深入的思索:更加深入的思索:这个看似明显正确的模型怎么会出错呢?这个看似明显正确的模型怎么会出错呢?“正方形是一种长方形正方形是一种长方形”地球人都知道地球人都知道错在哪里?错在哪里?对不是对不是g g函数的编写者而言,正方形可以是长方形,但是对函数的编写者而言,正方形可以是长方形,但是对g g函数的编写者而言,函数的编写者而言,SquareSquare绝对不是绝对不是RectangleRectangle!OODOOD中对象之间是否存在中对象之间是否存在IS-AIS-A关系,应该从行关系,应该从行为的角度来看待。为的角度来看待。而行为可以依赖客户程序做出合理的假设。而行为可以依赖客户程序做出合理的假设。基于契约(和约)的设计基于契约(和约)的设计DBCDBC(Deign by ContractDeign by Contract)“合理的假设合理的假设”使人郁闷。使人郁闷。我怎么知道是否合理呢?我怎么知道是否合理呢?使用使用DBCDBC,类的编写者需要显示的规定针对该类的契约。客户代码的编写者,类的编写者需要显示的规定针对该类的契约。客户代码的编写者可以通过契约获悉行为的依赖方式。可以通过契约获悉行为的依赖方式。契约通过为每一个方法规定前置条件(契约通过为每一个方法规定前置条件(preconditionspreconditions)和后置条件)和后置条件(postconditionspostconditions)来指定的。要使一个方法执行,前置条件一定要为真)来指定的。要使一个方法执行,前置条件一定要为真(对客户的要求);函数执行后要保证后置条件为真(对函数编写者的要(对客户的要求);函数执行后要保证后置条件为真(对函数编写者的要求)。求)。基于契约(和约)的设计基于契约(和约)的设计DBCDBC(Deign by ContractDeign by Contract)(续)(续)例如:例如:在上面的例子中,在上面的例子中,Rectangle:SetWidth(double w)Rectangle:SetWidth(double w)的后置条件可以看作是:的后置条件可以看作是:assert(itsWidth=w)&(itsHeight=old.itsHeight);assert(itsWidth=w)&(itsHeight=old.itsHeight);基类和派生类在前置条件和后置条件上的关系是:基类和派生类在前置条件和后置条件上的关系是:如果在派生类中重新申明了基类中已有的成员函数,这个函数只能使用相如果在派生类中重新申明了基类中已有的成员函数,这个函数只能使用相等或更弱的前置条件来替换原有的前置条件;并且,只能使用相等或更强等或更弱的前置条件来替换原有的前置条件;并且,只能使用相等或更强的后置条件来替换原有的后置条件。的后置条件来替换原有的后置条件。派生类必须接受基类已经接受的一切;并且,派生类不能违反基类已经派生类必须接受基类已经接受的一切;并且,派生类不能违反基类已经确定的规则。确定的规则。在一些语言中明确的支持契约,例如在一些语言中明确的支持契约,例如EiffelEiffel,你申明它们,运行时系统会,你申明它们,运行时系统会自动的检查。在自动的检查。在JaveJave和和C C标准中尚未支持,我们必须自己考虑。标准中尚未支持,我们必须自己考虑。4.依赖倒置原则(依赖倒置原则(DIP)陈述:陈述:高层模块不应该依赖于低层模块。二者应该依赖于抽象。高层模块不应该依赖于低层模块。二者应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。分析:分析:所谓所谓“倒置倒置”是相对于传统的开发方法(例如结构化方法)中总是是相对于传统的开发方法(例如结构化方法)中总是倾向于让高层模块依赖于低层模块而言的软件结构而言的。倾向于让高层模块依赖于低层模块而言的软件结构而言的。高层包含应用程序的策略和业务模型,而低层包含更多的实现细节,高层包含应用程序的策略和业务模型,而低层包含更多的实现细节,平台相关细节等。高层依赖低层将导致:平台相关细节等。高层依赖低层将导致:难以复用。通常改变一个软硬件平台将导致一些具体的实现发生变化,难以复用。通常改变一个软硬件平台将导致一些具体的实现发生变化,如果高层依赖低层,这种变化将导致逐层的更改。如果高层依赖低层,这种变化将导致逐层的更改。难以维护。低层通常是易变的。难以维护。低层通常是易变的。层次化:层次化:“所有良构的所有良构的OOOO体系结构都具有清晰的层次定义,每个层次通过一个体系结构都具有清晰的层次定义,每个层次通过一个定义良好的、受控的接口向外提供了一组内聚的服务。定义良好的、受控的接口向外提供了一组内聚的服务。”BoochBooch对上述论述可能存在两种不同的理解:对上述论述可能存在两种不同的理解:简单的理解简单的理解u1()u2()g()u1();u2();p()g();层次化层次化(续):续):更好的理解更好的理解依赖关系倒置依赖关系倒置下层的实现,依赖于下层的实现,依赖于上层的接口上层的接口接口所有权倒置接口所有权倒置客户拥有接口,而服客户拥有接口,而服务者则从这些接口派务者则从这些接口派生生依赖不倒置的开发依赖不倒置的开发自顶向下首先设计整个软件的分自顶向下首先设计整个软件的分解结构解结构然后首先实现下层的功能然后首先实现下层的功能再实现上层的功能,并使上层调再实现上层的功能,并使上层调用下层函数用下层函数依赖倒置的开发依赖倒置的开发首先设计上层需要调用的接口,首先设计上层需要调用的接口,并实现上层并实现上层然后低层类从上层接口派生,实然后低层类从上层接口派生,实现低层现低层接口属于上层接口属于上层示例示例1 1(ButtonButton与与LampLamp):):ButtonButton(开关)感知外界的变化。(开关)感知外界的变化。当接受到当接受到PollPoll(轮询)消息时,判断其是否被(轮询)消息时,判断其是否被“按下按下”。这个按下是抽象的(不关。这个按下是抽象的(不关心通过什么样的机制去感知):心通过什么样的机制去感知):可能是可能是GUIGUI上的一个按钮被鼠标单击上的一个按钮被鼠标单击可能是一个真正的按钮被手指按下可能是一个真正的按钮被手指按下可能是一个防盗装置检测到了运动可能是一个防盗装置检测到了运动LampLamp(灯)根据指示,收到(灯)根据指示,收到turn onturn on消息显示某种灯光,收到消息显示某种灯光,收到turn offturn off消息关闭灯光消息关闭灯光可能是计算机控制台的可能是计算机控制台的LEDLED可能是停车场的日光灯可能是停车场的日光灯可能是激光打印机中的激光可能是激光打印机中的激光应该如何设计程序来用应该如何设计程序来用ButtonButton控制控制LampLamp呢?呢?一个不成熟的设计一个不成熟的设计ButtonButton对象直接依赖对象直接依赖LampLamp对象,对象,从而:从而:LampLamp的任何变化都会影响到的任何变化都会影响到ButtonButton,导致其改写或者重新,导致其改写或者重新编译编译黑盒方式重用黑盒方式重用ButtonButton来控制一来控制一个个MotorMotor类变得不可能类变得不可能class ButtonLamp*itsLamp;public:void poll()if(/*some condition*/)itsLamp-turnOn();.;一个依赖倒置的设计一个依赖倒置的设计依赖于抽象依赖于抽象什么是高层策略?就是应用背后的抽象什么是高层策略?就是应用背后的抽象背后的抽象是检测用户的开背后的抽象是检测用户的开/关指令关指令用什么机制检测用户的指令?无关紧要用什么机制检测用户的指令?无关紧要目标对象是什么?无关紧要目标对象是什么?无关紧要他们不会影响到抽象的具体细节他们不会影响到抽象的具体细节改进后的设计:改进后的设计:ButtonButton依赖于抽象的接口依赖于抽象的接口ButtonServerButtonServer(向该接口发消息)(向该接口发消息)。ButtonServerButtonServer提供一些抽象的提供一些抽象的方法,方法,ButtonButton类通过这些接口可类通过这些接口可以开启或关掉一些东西。以开启或关掉一些东西。LampLamp也依赖于也依赖于ButtonServerButtonServer接口接口(从此接口派生),提供具体的(从此接口派生),提供具体的实现。实现。部分代码:部分代码:/Button.h#include ButtonServer.hclass ButtonButtonServer*bs;public:void poll();/Button.cppvoid Button:poll()if(/*mechanism 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()/*codes for turn on a specific device*/void Lamp:turnOff()/*codes for turn off a specific device*/分析:分析:上述设计使得上述设计使得ButtonButton可以控制所有愿意实现可以控制所有愿意实现ButtonServerButtonServer接口的设接口的设备,甚至是一个尚未开发出来的设备。备,甚至是一个尚未开发出来的设备。质疑:质疑:这样的设计是不是强加了这样一个约束这样的设计是不是强加了这样一个约束所有需要被所有需要被ButtonButton控制的对象一定要实控制的对象一定要实现现ButtonServerButtonServer类。类。如果我的设备还希望能够被另一个对象控制,比如如果我的设备还希望能够被另一个对象控制,比如SwitchSwitch控制,怎么办?控制,怎么办?这种设计是不是将这种设计是不是将ButtonButton对对LampLamp的依赖转嫁成了的依赖转嫁成了LampLamp对对ButtonButton的依赖呢?(毕竟的依赖呢?(毕竟LampLamp只能被一种只能被一种ButtonButton控制也是不好的)控制也是不好的)抗辩:抗辩:上述质疑不成立。上述质疑不成立。ButtonButton依赖于依赖于ButtonServerButtonServer接口,但是接口并不依赖于接口,但是接口并不依赖于ButtonButton,也就是说任何知道如何操作也就是说任何知道如何操作ButtonServerButtonServer接口的对象都可以操作接口的对象都可以操作LampLamp。也许需要改进的仅仅是也许需要改进的仅仅是ButtonServerButtonServer这样一个有些这样一个有些“误导性误导性”的名字,我们可以将的名字,我们可以将这个名字该得更加抽象一些,例如:这个名字该得更加抽象一些,例如:SwitchableDeviceSwitchableDevice5.接口隔离原则(接口隔离原则(ISP)陈述:陈述:不应该强迫客户依赖于他们不用的方法不应该强迫客户依赖于他们不用的方法一个类的不内聚的一个类的不内聚的“胖接口胖接口”应该被分解成多组方法,每一组方法应该被分解成多组方法,每一组方法都服务于一组不同的客户程序。都服务于一组不同的客户程序。先说一个例子:先说一个例子:class Doorpublic:virtual void Lock()=0;virtual void Unlock()=0;virtual bool IsDoorOpen()=0;Door可以加锁、解锁、而且可以感知自己是开还是关;Door是抽象基类,客户程序可以依赖于抽象而不是具体的实现增加功能增加功能如果门打开时间过长,它就会报警。如果门打开时间过长,它就会报警。(比如宾馆客房的门)(比如宾馆客房的门)为了实现上述新增功能,我们要求为了实现上述新增功能,我们要求DoorDoor与一个已有的与一个已有的TimerTimer对象进行交互对象进行交互class Timerpublic:void Register(int timeout,TimerClient*client);class TimerClientpublic:virtual void TimerOut();如果一个对象希望得到超时通知,它可以调用Timer的Register函数。该函数有两个参数,一个是超时时间,另一个是指向TimerClient对象的指针,此对象的TimerOut函数会在超时时被调用我们如何将我们如何将TimerClient和和TimedDoor联系起来?联系起来?问题问题一种常见的解决方案如下:一种常见的解决方案如下:问题接口污染在Door接口中加入新的方法(Timeout),而这个方法仅仅只为它的一个子类带来好处。如果每次子类需要一个新方法时它都被加到基类接口中,基类接口将很快变胖。胖接口将导致胖接口将导致SRP,LSP被违反,从而导致脆弱、僵被违反,从而导致脆弱、僵化化客户的反作用力:客户的反作用力:通常接口的变化将导致通常接口的变化将导致clientclient的改变的改变但是很多时候,接口之所以变化是因为客户需要他们变化但是很多时候,接口之所以变化是因为客户需要他们变化 Client Client对对interfaceinterface具有反作用力!具有反作用力!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&itsTimedDoor;public:DoorTimerAdapter(TimedDoor&theDoor):itsTimedDoor(theDoor)vitual void TimeOut(int timeOutId)itsTimedDoor.DoorTimeOut(timeOutId);解决之道(续):解决之道(续):使用多继承使用多继承Class TimedDoor:public Door,public TimerClientpublic:virtual void DoorTimeOut(int timeOutID);
展开阅读全文
相关资源
相关搜索

最新文档


当前位置:首页 > 图纸专区 > 课件教案


copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!