软件设计的五大原则
合集下载
相关主题
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
– 例子2
---------shape.h----------------emum ShapeType{circle,square}; struct Shape{ ShapeType itsType; }; ---------circle.h----------------struct Circle{ ShapeType itsType; double itsRadius; CPoint itscenter; }; ---------square.h----------------struct Square{ ShapeType itsType; double itsSide; CPoint itsTopLeft; };
– 例子2(续)
• 修改后的设计
class Shape{ public: virtual void Draw() const=0; }; class Square:public Shape{ public: virtual void Draw() const; }; class Circle:public Shape{ public: virtual void Draw() const; }; void DrawAllShapes(Vector<Shape*>& list){ vector<Shape*>::iterator i; for(i=list.begin();i!=list.end();i++) (*i)->Draw(); }
•
真实的谎言:
– 一般而言,无论模块多么“封闭”,都会存在一些无法对之封闭的 变化 没有对所有变化的情况都封闭的模型 – 我们怎么办?
• 既然不可能完全封闭,我们必须有策略的对待此问题——对模型应该 封闭那类变化作出选择,封闭最可能出现的变化
这需要对领域的了解,丰富的经验和常识 错误的判断反而不美,因为OCP需要额外的开销(增加复杂度)
– 这种对SRP的违反将导致两个方面的问题:
• 包含不必要Biblioteka 代码– 一个应用可能希望使用Retangle类计算矩形的面积,但是却被迫 将绘制矩形相关的代码也包含进来
• 一些逻辑上毫无关联的原因可能导致应用失败
– 如果GraphicalApplication的需求发生了变化,从而对Rectangle 类进行了修改。但是这样的变化居然会要求我们重新构建、测试 以及部署ComputationalGeometryApplication,否则其将莫名其 妙的失败。
public:
virtual void dail(char* pno)=0; virtual void hangup( ) =0; virtual void send(char c) =0; virtual void recv( ) =0; }; Modem类(可能)有两个职责: 1. 拨号 2. 通信
– 什么是职责? 职责是“变化的原因”。 – 上面的例子可能存在两种变化的方式:
• 连接和通信可能独立变化 在这种情况下,应该将职责分开。例如,应用的变化导致了连接部分 方法的签名(signature)发生了变化,那么使用数据连接的部分也 需要重新编译、部署,这会相当麻烦,使得设计僵化。 • 连接和通信同时变化 这种情况下,不必将职责分开。反而分离可能导致“不必要的复杂性” 的臭味
2. 开放封闭原则(OCP)
• 陈述:
– 软件实体(类、模块、函数等)应该是可以扩展的,同时还可以是 不必修改的,更确切的说,函数实体应该: (1)对扩展是开放的 当应用的需求变化时,我们可以对模块进行扩展,使其具有满足改 变的新的行为——即,我们可以改变模块的功能
(2)对更改是封闭的
对模块进行扩展时,不必改动已有的源代码或二进制代码。
刻舟求剑是错误的。
——王亚沙
– 修改后的设计如下:
<<interface>> Data Channel +send(:char ) +recv():char <<interface>> Connection +dail(pno:String ) +hangup()
有一点需要注意:在 ModemImplementation中 实际还是集合了两个职 责。这是我们不希望的, 但是有时候却是必须的。 但是我们注意到,对于 应用的其它部分,通过 接口的分离我们已经实 现了职责的分离。 ModemImplementation已 经不被其它任何程序所 依赖。除了main以外, 其他所有程序都不需要 知道这个函数的存在。
– 例子2(续)
• 再看这些批判
再加入“三角形”将变得十分简单:
– 僵化的 增加Triangle会导致Shape、Square、Circle以及DrawAllShapes的重新编 译和部署
– 脆弱的
因为存在大量的既难以查找又难以理解的Switch和If语句,修改稍有不慎, 程序就会莫明其妙的出错 – 牢固的
C语言程序
---------drawAllShapes.cpp---------typedef struct Shape * ShapePointer; void DrawAllShapes(ShapePointer list[], int n){ int i; for(i=0;i<n;i++){ struct Shape* s=list[i]; switch (s->itsType){ case square: s->Square(); break; case circle: s->DrawCircle(); break; } }}
class ClientInterface{ virtual void ServerFunc()=0; }; class server:public ClientInterface{ int serverData; public: void ServerFunc(); };
– 例子1(续)
• 一个问题: 为什么上述的ClientInterface这个类要取这么个名字,叫做AbastractServer 岂不更好?? 其实这里面蕴含了一个思想: ——client类中更多的描述了高层的策略,而Server类中是对这些策略的一种 具体实现。 而接口是策略的一个组成部分,他根client端的关系更加密切 我们应该这样想问题:ClientInterface中定义了client期 望Server做什么,而server具体类是对client这种要求的 一种具体实现。 OCP原则其实是要求我们清晰的区分策略和策略的具体实现形式。允许 扩展具体的实现形式(开放),同时将这种扩展与策略隔离开来,使 其对上层的策略透明(封闭)。
class server1{ int serverData; public: void ServerFunc(); };
2. 开放封闭原则(OCP)
– 例子1(续) 修改后的设计
client <<interface>> ClientInterface
server
class client{ ClientInterface& ci; public: client(ClientInterface & CI):ci(CI){} void useServer(){ ci.ServerFunc(); } };
想在一个程序中复用DrawAllShapes,都必须带上Circle、Square,即使 那个程序不需要他们
•
谎言:
– 上述代码并不完全封闭——“如果我们希望正方形在所有圆之前绘 制”会怎么样? ——对绘图的顺序无法实现封闭 更糟糕的是,刚才的设计反而成为了实现“正方形在所 有圆之前绘制”功能的障碍。
bool operator<const Shape& s) {return Precedes(s);}
};
template<typename P> class Lessp{ public: bool operator() {const P p,const P q){ return (*p)<(*q);} } void DrawAllShapes(vector<Shape*>& list){ vector<Shape*> orderedList=list; sort(orderedList.begin(), orderedList.end(), Lessp<Shape*>()); vector<Shape*>::const_iterator I; for(i=orderedList.begin(); i!=orderedList.end(); i++) (*i)->Draw(); }
软件设计中的5大原则
1. 单一职责原则(SRP)
• 陈述:
– 就一个类而言,应该只有一个导致其变化的原因
• 分析:
– 一个职责就是一个变化的轴线
– 一个类如果承担的职责过多,就等于将这些职责耦合在 一起。一个职责的变化可能会虚弱或者抑止这个类完成 其它职责的能力 – 多职责将导致脆弱性的臭味
• 示例1:
Computational Geometry Application Rectangle + draw( ) +area( ):double Graphical Application
GUI
Rectangle类具有两个职责: 1. 计算矩形面积的数学模型 2. 将矩形在一个图形设备上描述出来
– Rectangle类违反了SRP,具有两个职能——计算面积和 绘制矩形
从而带来编译、链接、部署等一系列的问题。
见下页程序
2. 开放封闭原则(OCP)
– 例子1(续)
class client{ server& s; public: client(server& SER):s(SER){} void useServer(){ s.ServerFunc(); } }; class client{ server1& s; public: client(server& SER):s(SER){} void useServer(){ s.ServerFunc(); } }; class server{ int serverData; public: void ServerFunc(); };
class server{ int serverData; public: void ServerFunc(); };
– 例子1(续) 这个程序出了什么问题? client和server都是具体类,接口与实现没有实现分离。如果我们 想要让client调用一个新的server类,那么我们不得不修改client 的源代码
敏捷的思想——我们预测他们,但是直到我们发现他们才行动
•
回到例2:
– 要实现对排序的封闭应该如何设计?
class Shape{ public: virtual void Draw( ) const=0; virtual bool Precedes(const Shape&) const=0;
– 修改后的设计如下:
Computational Geometry Application Graphical Application
Geometric Rectangle +area( ):double
Rectangle GUI +draw( )
•
示例2:
一个Modem的接口: Class Modem{
Modem Implementation
•
常见错误提醒:
– 持久化与业务规则的耦合。 例如:
Employee Persistence Subsystem +CalculatePay +Store
业务规则经常变化,而 持久化方法却一般不变。 将这两个职责耦合在一 起,将导致每次因为业 务规则变化调整 Employee类时,所有持 久化部分的代码也要跟 着变化
– 例子2(续)
• 批判
这个程序不符合OCP,如果需要处理的几何图形中再加入“三角形” 将引发大量的修改
– 僵化的 增加Triangle会导致Shape、Square、Circle以及DrawAllShapes的重新编 译和部署 – 脆弱的 因为存在大量的既难以查找又难以理解的Switch和If语句,修改稍有不慎, 程序就会莫明其妙的出错 – 牢固的 想在一个程序中复用DrawAllShapes,都必须带上Circle、Square,即使 那个程序不需要他们
•
分析:
– 世界是变化的(而且变化很快),软件是对现实的抽象
软件必须能够扩展 – 如果任何修改都需要改变已经存在的代码,那么可能导致牵一发动 全身现象,进而导致雪崩效应,使软件质量显著下降
•
实现OCP的关键是抽象:
– 例子1
client
server
class client{ server& s; public: client(server& SER):s(SER) {} void useServer(){ s.ServerFunc(); } };