0
点赞
收藏
分享

微信扫一扫

《敏捷软件开发》— 设计原则(一)

夕阳孤草 2022-02-10 阅读 49

《敏捷软件开发》— 设计原则(一)

一、单一职责原则(SRP)

这项设计原则体现了模块的内聚性要求。当需求变化时,该变化会反映为类的功能的变化。如果一个类承担了多于一个的职责,那么就可能有多个原因导致其发生变化。一个职责的变化可能会抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,无法很好的响应变化。

1、职责

在 SRP 中,我们把职责定义为变化的原因。如果能想到多于一个的动机去改变一个类,那么这个类就是具有多于一个的职责。有时,我们很难注意到这一点。我们习惯于以组的形式去考虑职责。例如,考虑下面 Modem 接口:

interface Modem {
	public void dial(String pno);
	public void hangup();
	public void send(char c);
	public void recv();
}

初始看来,这个接口很合理。然而, 它实际承担了两个职责:连接和通信。那么这两个职责应该被分开吗?这依赖于需求的变化。如果二者的变化总是独立的,例如经常需要在保持通信方式的情况下,修改连接接口,那么这两种职责就应该被分离;另一方面,如果它们总是成对地发生变化,那么就不需要分离它们。

2、分离耦合的职责

在这里插入图片描述

二、开放-封闭原则(OCP)

如果程序中的一处改动会产生连锁反应,导致一系列相关模块的改动,那么设计就具有僵化性的臭味。OCP 建议我们应该对系统进行重构,这样以后对系统再进行那样的改动时,就不会导致更多的修改。如果正确使用 OCP, 那么以后再进行同样的改动时,就只需要添加新的代码,而不必改动已经正常运行的代码

1、抽象

实现 OCP 的基础在于抽象。如果模块 A 依赖于模块 B,而模块 B 的实现又经常发生变化,那么我们可以让 A 仅依赖于 B 的抽象接口类:
在这里插入图片描述
这样,当我们新增服务器类型时,不需要修改 Client 类(显然,配置类关系的地方需要修改)。

2、Shape 程序

一个很常见的违反OCP的例子如下:

// Shape.h
enum ShapeType {
	e_Circle,
	e_Square
};

struct Shape
{
	ShapeType shapeType;
};

// Circle.h
struct Circle : public Shape
{
	double radius;
	Point center;
};

// Square.h
struct Square : public Shape
{
	double side;
	Point topLeft;
};

// drawAllShapes.cpp
void drawAllShapes(Shape* shapes[], int num)
{
	for (int i = 0; i < num; ++i)
	{
		Shape* shape = shapes[i];
		switch (shape->shapeType)
		{
		case e_Circle:
		{
			DrawCircle((struct Square*)shape);
		}
		case e_Square:
		{
			DrawSquare((struct Square*)shape);
		}
		}
	}
}

上述代码的问题在于它对于类型的增加不是封闭的,我们需要修改实现类中的代码。此外,每增加一种类型,我们就需要修改枚举常量的数目,造成所有的文件都需要重新编译。

针对上述问题,只要我们使用面向对象的继承和多态两大特性就可以解决。

3、识别变化

使用继承和多态真的使上述代码完全封闭了吗?不见得。如果我们要求所有的圆必须在正方形之前绘制,那么程序无法很好地拥抱这种变化。

如果我们预测到了这种变化,那么就可以设计一个抽象来隔离它。我们使用的抽象 Shape 类对新的需求来说,并不是一个合适的抽象。因为新的需求更重视形状的顺序而非形状的类型。

这导致无论模块是多么封闭,总会有一些无法对之封闭的变化。没有对所有的情况都贴切的模型。因此,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。我们进行适当的调查,提出正确的问题,并且使用我们的经验和一般常识。最终,我们会一直等到变化发生时才采取行动。

4、刺激变化

我们选择在变化发生时才进行抽象。但是这种变化如果出现的很晚,一方面会造成抽象的困难(模块的耦合度可能已经很高了);另一方面将会造成项目的延期或是质量不过关。因此,我们希望能够刺激变化尽早地出现:

  • TDD。TDD 实际上是号召我们站在需求提出方的角度思考问题,这会让我们更好地识别变化。
  • 在加入基础结构前就开发特性,并经常性地把那些特性展示给涉众。
  • 使用较短的迭代和发布周期。
  • 根据特性优先级的顺序安排开发顺序。

5、使用抽象获得显式封闭

回到前面所说的图形顺序问题,我们怎样借助抽象封闭变化呢?显然,我们需要某种抽象的排序接口。该抽象接口能够根据对象的类型决定绘制顺序:

// Shape.h
class Shape
{
public:
	virtual void draw() = 0;
	virtual bool precede(Shape* shape) = 0;
};

// Circle.h
#include "Square.h"
class Circle : public Shape
{
	double radius;
	Point center;

public:
	virtual void draw() override
	{
		cout << "draw Circle" << endl;
	}

	virtual bool precede(Shape* shape) override
	{
		try
		{
			dynamic_cast<Square*>(shape);
			return true;
		}
		catch (bad_cast& e)
		{
			return false;
		}
	}
};

// Square.h
class Square : public Shape
{
	double side;
	Point topLeft;

public:
	virtual void draw() override
	{
		cout << "draw Square" << endl;
	}

	virtual bool precede(Shape* shape) override
	{
		return false;
	}
};

// drawAllShapes.cpp
void drawAllShapes(Shape* shapes[], int num)
{
	sort(shapes, shapes + 5, [](Shape* left, Shape* right) {return left->precede(right); });

	for (int i = 0; i < num; ++i)
	{
		Shape* shape = shapes[i];
		shape->draw();
	}
}

虽然我们找到了一个抽象方法,但是显然派生类中实现的 precede 方法并不符合 OCP。它竟然需要知道 Square 类的存在。如果以后再进行类型的扩展和绘制顺序的变更,都将导致前面的具体类需要修改,这又违反了 SRP 原则。此外,Shape 接口承担了两个相关性比较差的责任,不符合接口隔离原则。

针对最后一个问题,我们可以使用类似 java 中的 Comparable 接口解决;针对前两个问题,我们很自然地想到使用表驱动模式来解决。

6、使用数据驱动的方式获得封闭性

首先,我们增加一个比较接口进行接口隔离,同时提供 Shape 的比较接口实现类。其中,shapePrecedes 保存了表驱动的必要数据(如果在 java 中,可以使用匿名内部类来实现 ComparableShape,因为我们已经把变化的部分隔离到 shapePrecedes 的初始化过程中了):

class Comparable
{
	virtual bool operator()(T* left, T* right) = 0;
};

multimap<string, string> shapePrecedes;

class ComparableShape : public Comparable<Shape>
{
public:
	virtual bool operator()(Shape* left, Shape* right) override
	{
		auto range = shapePrecedes.equal_range(typeid(*left).name());
		if (range.first != range.second)
		{
			auto iter = find_if(range.first, range.second, [right](auto strIter)
				{
					return strIter.second == typeid(*right).name();
				});

			return iter != range.second;
		}

		return false;
	}
};

表格的初始化如下:

void init_shapePrecedes()
{
	shapePrecedes.insert(make_pair(typeid(Circle).name(), typeid(Square).name()));
}

实际我们可以通过读取配置文件或数据库等方式来获取该表。

绘图函数的声明如下:

void drawAllShapes(Shape* shapes[], int num, const ComparableShape& comparableShape)
{
	sort(shapes, shapes + 5, comparableShape);

	for (int i = 0; i < num; ++i)
	{
		Shape* shape = shapes[i];
		shape->draw();
	}
}

其余各类和我们没有引入排序需求时一样。这样的实现能够满足我们前面提到的这些设计原则。

举报

相关推荐

0 条评论