敏捷软件开发原则,模式与实践书摘。
拙劣设计的症状:
- 僵化性(Rigidity):很难对系统进行改动,因为每个改动都会迫使许多对系统其它部分的其它改动
- 脆弱性(Fragility):对系统的改动会导致系统中和改动的地方在概念上无关的许多地方出现问题
- 牢固性(Immobility):很难解开系统的纠结,使之成为一些可在其他系统中重用的组件
- 粘滞性(Viscosity):做正确的事情比做错误的事情要困难
- 不必要的复杂性(Needless Complexity):设计中包含不具任何直接好处的基础结构
- 不必要的重复(Needless Repetition):设计中包含有重复的结构,而该重复的结构本可以使用单一的抽象进行统一
- 晦涩性(Opacity):很难阅读,理解。没有很好地表现出意图
面向对象设计原则:
-
单一职责原则(The Single Responsibility Principle, 简称SRP)
- 就一个类而言,应该仅有一个引起它变化的原因(一个类一个职责)
- 例子1:有两个不同的应用程序使用Rectangle类。一个有关计算几何学方面的,Rectangle类会在几何形状计算方面为它提供帮助,它从来不会在屏幕上绘制矩形。另外一个应用程序实质上是有关图形绘制方面的,它可能也会进行一些计算几何方面的工作,但是它肯定在屏幕上绘制矩形。
例子1:图示3
-
例子2:Line和LineSegment的例子。如代码段1。最初看到这两个类时,会觉得它们之间有自然的公有继承关系。LineSegment需要Line中的每一个成员变量和成员函数。此外,LineSegment新增了一个自己的成员函数GetLength,并override了IsOn函数。但是这两个类还是以微妙的方式违反了LSP。Line的使用者可以期望和该Line具有线性对应关系的所有点都在该Line上。例如由Intercept函数返回的点就是线和y轴的交点。由于这个点和线具有线性对于关系,所以Line的使用这可以期望IsOn(Intercept()) == true。然而,对于许多LineSegment的实例,这条声明会失效。这为什么是一个重要的问题呢什么不简单地让LineSegment从Line派生并忍受这个微妙的问题呢是一个需要进行判断的问题。在大多数情况下,接受一个多态行为中的微妙错误都不会比试着修改设计使之完全符合LSP更为有利。接受缺陷而不是去追求完美这是一个工程上的权衡问题。好的工程师知道何时接受缺陷比追求完美更有利。不过,不应该轻易放弃对于LSP的遵循。总是保证子类可以代替它的基类是一个有效的管理复杂性的方法。一旦放弃了这一点,就必须要单独来考虑每个子类。
-
解决方案:用提取公共部分的方法代替继承。有一个简单的方案可以解决Line和LineSegment的问题,该方案也阐明了一个OOD的重要工具。如果既要使用类Line又要使用类LineSegment,那么可以把这两个类的公共部分提取出来作为一个抽象基类。如代码2所示。
-
提取公共部分是一个设计工具,最好在代码不是很多的时候应用。当然,如果Line已经存在很多clients,那么提取出LinearObject就不会这么轻松(绝对需啊测试套件的支持)。不过在有可能时,它仍然是一个有效的工具。
#ifndef GEOMETRY_LINE_H
#define GEOMETRY_LINE_H
#include “geometry/point.h”class Line
{
public:
Line(const Point& p1, const Point& p2);double GetSlope() const;
double GetIntercept() const;
double GetP1() const;
double GetP2() const;
virtual bool IsOn(const Point&) const;
private:
Point itsP1;
Point itsP2;
};#endif
//
#ifndef GEOMETRY_LINESEGMENT_H
#define GEOMETRY_LINESEGMENT_Hclass LineSegment: public Line
{
public:
LineSegment(const Point& p1, const Point& p2);
double GetLength() const;
virtual bool IsOn(const Point&) const;
};#endif
代码段1 – 有问题的代码
- #ifndef GEOMETRY_LINEAR_OBJECT_H
#define GEOMETRY_LINEAR_OBJECT_H
#include “geometry/point.h”class LinearObject
{
public:
LinearObject(const Point& p1, const Point& p2);double GetSlope() const;
double GetIntercept() const;
double GetP1() const;
double GetP2() const;
virtual bool IsOn(const Point&) const = 0; // Abstract
private:
Point itsP1;
Point itsP2;
};#endif
#ifndef GEOMETRY_LINE_H
#define GEOMETRY_LINE_Hclass LineSegment: public LinearObject
{
public:
Line(const Point& p1, const Point& p2);
virtual bool IsOn(const Point&) const;
};#endif
#ifndef GEOMETRY_LINESEGMENT_H
#define GEOMETRY_LINESEGMENT_Hclass LineSegment: public LinearObject
{
public:
LineSegment(const Point& p1, const Point& p2);
double GetLength() const;
virtual bool IsOn(const Point&) const;
};#endif
#ifndef GEOMETRY_RAY_H
#define GEOMETRY_RAY_Hclass Ray: public LinearObject
{
public:
Ray(const Point& p1, const Point& p2);
virtual bool IsOn(const Point&) const;
};#endif
代码段2 – 重构后的代码
-
- 就一个类而言,应该仅有一个引起它变化的原因(一个类一个职责)
-
依赖倒置原则(The Dependency Inversion Principle, 简称DIP)
- 高层模块不应该依赖于底层模块。二者都应该依赖于抽象。
- 抽象不应该依赖于细节。细节应该依赖于抽象。
- 一个设计良好的面向对象程序,其依赖程序结构相对于传统的过程式方法设计的通常结构而言是被“倒置了。
考虑一下当高层模块依赖于底层模块时意味着什么。高层模块包含了一个应用程序中的重要的策略选择和业务模型。正是这些高层模块才使得其所在的应用程序区别于其它。然而,如果这些高层模块依赖于底层模块,那么对底层模块的改动就会直接影响到高层模块,从而迫使它们依次做出改动。
这种情形是非常荒谬的!本应该是高层的策略设置模块去影响底层的细节实现模块的。包含高层业务规则的模块应该优先并独立于包含实现细节的模块。无论如何高层模块不应该依赖于底层模块。
此外,我们更希望能够重用的是高层的策略设置模块。我们已经非常擅长于通过子程序库的形式来重用底层模块。如果高层模块依赖于底层模块,那么在不同的上下文中重用高层模块就会变得非常困难。然而,如果高层模块独立于底层模块,那么高层模块就可以非常容易地被重用。该原则是framework设计的核心原则。
-
依赖于抽象。,这个启发式规则建议不应该依赖于具体类——也就是说,程序中所有的依赖关系都应该终止于抽象类或这接口。根据这个启发式规则,可以推断出下面三条规则。
- 任何变量都不应该持有一个指向具体类的指针或这引用
- 任何类都不应该从具体类派生
- 任何方法都不应该override它的任何基类中的已经实现了的方法
- 当然,每个程序中都会有违反该启发规则的情况。有时必须要创建具体类的实例,而创建这些实例的模块将会依赖它们。此外,该启发规则对于那些虽是具体但却稳定(nonvolatile)的类来说似乎不太合理。如果一个具体类不太会改变,并且也不会创建其它类似的派生类,那么依赖于它并不会造成损害(其实到时,进行合理的重构还是可以解决这一问题)。比如,在大多数系统中,描述字符串的类都是具体的。但该类是稳定的,不太会改变。因此,直接依赖于它不会造成损害。
- 然而,我们在应用程序中所编写的大多数具体类都是不稳定的。我们不想直接依赖于这些不稳定的具体类。通过把它们隐藏在抽象接口的后面,可以隔离它们的不稳定性。但这并不是一个完美的解决放啊。常常,如果一个不稳定类的接口必须要变化时,这个变化一定会影响到表示该类的抽象接口。这种变化破坏了由抽象接口维系的隔离性。
- 由此可知,该启发规则对问题的考虑有点简单了。另外一方面,如果看到更远一点,认为是有客户类来声明他们需要的服务接口,那么仅当客户需要时才会对接口进行改变。这样,改变实现抽象接口的类就不会影响到客户。
-
例子1:依赖倒置可以应用于任何存在一个类向另一个类发送消息的地方。例如,Button对象和Lamp对象之间的情形。Button对象感知外部环境的变化。但接收到Poll消息时,它会判断是否被用户“按下”。它不关心是通过什么样的机制去感知的。可能是GUI上的按钮图标,也可能是一个能够用手指按下的真正按钮,甚至可能是一个家庭安全系统中的运动检测器。Button对象可以检测到用户激活或关闭它。
Lamp对象会影响外部环境。当接收到TurnOn消息时,它显示某种灯光。当接收到TurnOff消息时,它把灯光熄灭。它可以是计算机控制台的LED,也可以是停车场的水银灯,甚至是激光打印机中的激光。
该如何设计一个用Button对象控制Lamp对象的系统呢图1展示了一个不成熟的设计。Button对象接收Poll消息,判断按钮是否被按下,接着简单地发送TurnOn或者TurnOff消息给Lamp对象。这个模型的相应代码如下代码段1所示。请注意Button类直接依赖于Lamp类。这个依赖意味着,当Lamp类改变时,Button类会受到影响。此外,想要重用Button来控制一个Motor对象是不可能的。在这个设计中,Button对象控制着Lamp对象,并且也只能控制Lamp对象。这个方案违反了DIP。应用程序的高层策略没有和底层实现分离。抽象没有和具体细节分离。没有这种分离,高层策略就自动地依赖于底层模块,抽象就会自动地依赖于具体细节。 -
接口隔离原则(The Interface Segregation Principle, 简称ISP)
- 不应该强迫客户依赖于它们不用的方法(胖接口)。
- 如果强迫客户程序依赖于那些它们不使用的方法,那么这些客户程序就面临着由于这些未使用方法的改变所带来的变更。这无意中导致了所有客户程序之间的耦合。换种说法,如果一个客户程序依赖于一个包含它不使用的方法的类,但是其它客户程序却要使用这些方法,那么当其它客户要求这个类改变时,就会影响到这个客户程序。应该尽可能避免这种耦合,因此需要分离接口(拆分为多个具有内聚接口的抽象基类)。
-
例子1:在一个安全系统中,有一些Door对象,它们可以被加锁和解锁,并且Door对象知道自己是开着还是关着。如程序片段1所示。现在考虑这样的实现,TimedDoor,如果门开着的时间过长,它就会发出警 声。为了做到这一点,TimedDoor对象需要和另一个名为Timer的对象交互,如代码片段2所示。如果一个对象希望得到超时通知,可以调用Timer的Register函数。该函数有两个参数,一个超时时间和一个指向TimerClient的对象的指针,该对象的TimeOut函数会在超时到达时被调用。怎样将TimerClient类和TimedDoor类联系起来,才能在超时时通知到TimedDoor中相应的处理代码呢span style=”font-size:13px”>
class Door
{
public:
virtual void Lock() = 0;
virtual void Unlock() = 0;
virtual bool IsDoorOpen() = 0;
};
代码片段1class Timer
{
public:
void Register(int timeout, TimerClient* client);
};class TimerClient
{
public:
virtual void Timeout() = 0;
};
代码片段2
- 方案3:使用多重继承分离接口。优先选用的方案。
图3
- 方案3:使用多重继承分离接口。优先选用的方案。
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!