单一职责原则(SRP)
为什么要把两个职责分离到单独的类呢?因为每个职责都是变化的一个轴线,如果一个类承担的职责过多,就等于把这些职责耦合在一起了,一个职责的变化可能会削弱这个类满足其它职责的能力,这种耦合会导致脆弱的设计,如果发生变化,设计就会遭到意想不到的破坏。
上面类图中显示 Rectangle 类有两个方法,一个方法把矩形绘制在屏幕上,另一个方法计算矩形的面积。有两个不同的应用程序使用 Rectangle 类,一个是计算几何程序,另一个是图形界面程序,它们分别主要使用 Rectangle 类总两个不同的方法。
这个设计就侵犯了 SRP 原则,Rectangle 类具备两个职责,一个职责是提供矩形几何计算的数学模型,另一个职责是在 GUI 上渲染矩形。
对 SRP 原则的侵犯会导致了一些难以解决的问题。首先,我们必须在计算几何应用中包含对 GUI 代码的引用,如果这是一个 C++ 程序,就必须把 GUI 代码链接进来,这导致应用程序无谓的消耗了链接时间、编译时间、内存空间和存储空间等。
再者,如果因为某些原因对 GraphicalApplication 的一个更改导致 Rectangle 类也相应做了更改,这将强制我们对 ComputationalGeometryApplication 进行重新构建、测试和部署等。如果我们忘了做这些事情,那么应用程序可能以无法预测的方式而失败。
一个较好的设计是将这两个职责完全地隔离到不同的类当中,如下图所示。将 Rectangle 中关于几何计算的职责移到了 GeometricRectangle 类中,而 Rectangle 类中仅保留矩形渲染职责。
在 SRP 中,我们将职责定义为“变化的原因”,如果你能想出多于一种动机来更改一个类,则这个类就包含多于一个职责。SRP 是所有原则中最简单的之一,也是最难运用正确的之一,因为我们习惯于将多个职责一起来考虑,职责的耦合有时很难被发现。
借用《UNIX编程艺术》的一句话:Do one thing, and do it well.
开放-封闭原则(OCP)
遵循开放-封闭原则的模块有两个特性:1)对扩展是开放的;2)对修改是封闭的。
乍看起来这两个特性好像是冲突的,因为通常扩展模块行为的常规方式就是修改该模块,那么如何使这两个相反的特性共存呢?关键就是抽象。
我们有如下程序,其功能是在标准 GUI 窗口上绘制圆形(Circle)和方形(Square),很明显DrawAllShapes
不满足 OCP,使用 C 语言的过程化技术是无法满足开放-封闭原则的。
enum ShapeType {circle, square};
struct Shape
{
ShapeType itsType;
};
struct Square
{
ShapeType itsType;
double itsSide;
Point itsTopLeft;
};
struct Circle
{
ShapeType itsType;
double itsRadius;
Point itsCenter;
};
void DrawSquare(struct Square*);
void DrawCircle(struct Circle*);
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:
DrawSquare((struct Square*)s);
break;
case circle:
DrawCircle((struct Circle*)s);
break;
}
}
}
我们将上面的代码进行适当修改,使其满足开放封闭原则。
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){
vetor<Shape*>::iterator I;
for (i = list.begin(); i != list.end(); i++) {
(*i)->Draw();
}
}
经过上面的改进,我们如果要增加一个新的图形,只需要创建一个衍生类并实现 Shape 类 中包含的方法就可以了,不再需要去修改DrawAllShapes()
函数。
但是要知道程序不可能达到完全封闭,如果对上述程序提出新的要求:所有的 Circle 都应该在 Square 之前先进行绘制,DrawAllShapes()
函数无法对这样的变化保持封闭,一般来说无论模块设计的有多么封闭,总会有变化来打破这种封闭的。
因此程序设计者必须考虑要对哪些变化封闭,需要讲究策略,也需要基于一定的经验去预测,了解用户及行业的的情况,可以更加准确的判断各种变化的可能性。
当然,我们可以在 Shape 中定义一个名为 Precedes 的方法,它可以接受另一个 Shape 作为参数并返回一个 bool 类型的结果,如果结果为 true 则表示接收调用的 Shape 对象应排在被作为参数的 Shape 对象的前面。(限于篇幅太长,下面不贴所有代码了)
bool Precedes(Shape another){
return another is Circle;
}
另外也可以使用数据驱动的方法获取封闭性,这里使用一个表格来存储绘制的顺序,如下所示。需要注意的是我们仍然无法对多种 Shape 的顺序保持封闭的就是表本身,可以将这个表定义放置在单独的模块中,使表与其他模块隔离,这样对表的更改则不再会对任何其他模块产生影响。
private Dictionary<Type, int> _typeOrderTable = new Dictionary<Type, int>();
private void Initialize()
{
_typeOrderTable.Add(typeof(Circle), 2);
_typeOrderTable.Add(typeof(Square), 1);
}
public bool Precedes(Shape another)
{
return _typeOrderTable[this.GetType()] > _typeOrderTable[another.GetType()];
}
参考资料:《敏捷软件开发 原则、模式与实践》