OOD&OOP-对扩展开放、对修改关闭

对扩展开放、对修改关闭

SOLID 中的第二个原则:开闭原则,即对扩展开放、对修改关闭,扩展性是代码质量最重要的衡量标准之一,那么,“怎样的代码改动才被定义为‘扩展’?怎样的代码改动才被定义为‘修改’?怎么才算满足或违反‘开闭原则’?修改代码就一定意味着违反‘开闭原则’吗?”。

如何理解“对扩展开放、修改关闭”?

开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension ,but closed for modification。我们把它翻译成中文就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。

这个描述比较简略,如果我们详细表述一下,那就是,添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

以一个例子来解释,如下为 API 接口监控告警的代码,其中,AlertRule 存储告警规则,可以自由设置。Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。NotificationEmergencyLevel 表示通知的紧急程度,包括 SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要),不同的紧急程度对应不同的发送渠道。

class AlertRule {/*...*/};
class Notification {/*...*/};
class Alert
{
public:
	Alert(AlertRule rule_, Notification notification_) :
		rule(rule_), notification(notification_) {}
	
	void check(std::string api, long requestCount, long errorCount, 
		long durationOfSeconds)
	{
		long tps = requestCount / durationOfSeconds;
		if (tps > rule.getMatchedRule(api).getMaxTps())
		{
			notification.notify(NotificationEmergencyLevel.URGENCY, "...");
		}
		if (errorCount > rule.getMatchedRule(api).getMaxErrorCount())
		{
			notification.notify(NotificationEmergencyLevel.SEVERE, "...");
		}
	}
private: 
	AlertRule rule;
	Notification notification;
};

上面这段代码非常简单,业务逻辑主要集中在 check() 函数中。当接口的 TPS 超过某个预先设置的最大值时,以及当接口请求出错数大于某个最大允许值时,就会触发告警,通知接口的相关负责人或者团队。

现在,如果我们需要添加一个功能,当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。这个时候,我们该如何改动代码呢?主要的改动有两处:第一处是修改 check() 函数的入参,添加一个新的统计数据 timeoutCount,表示超时接口请求数;第二处是在 check() 函数中添加新的告警逻辑。具体的代码改动如下所示:

class AlertRule {/*...*/};
class Notification {/*...*/};
class Alert
{
public:
	Alert(AlertRule rule_, Notification notification_) :
		rule(rule_), notification(notification_) {}
	//改动一:添加参数timeoutCount
	void check(std::string api, long requestCount, long errorCount, 
		long durationOfSeconds, long timeoutCount)
	{
		long tps = requestCount / durationOfSeconds;
		if (tps > rule.getMatchedRule(api).getMaxTps())
		{
			notification.notify(NotificationEmergencyLevel.URGENCY, "...");
		}
		if (errorCount > rule.getMatchedRule(api).getMaxErrorCount())
		{
			notification.notify(NotificationEmergencyLevel.SEVERE, "...");
		}
		//改动二:添加接口超时处理逻辑
		long timeoutTps = timeoutCount / durationOfSeconds;
		if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps())
		{
			notification.notify(NotificationEmergencyLevel.URGENCY, "...");
		}
	}
private: 
	AlertRule rule;
	Notification notification;
};

这样的代码修改实际上存在挺多问题的。一方面,我们对接口进行了修改,这就意味着调用这个接口的代码都要做相应的修改。另一方面,修改了 check() 函数,相应的单元测试都需要修改.

上面的代码改动是基于“修改”的方式来实现新功能的。如果我们遵循开闭原则,也就是“对扩展开放、对修改关闭”。那如何通过“扩展”的方式,来实现同样的功能呢?

我们先重构一下之前的 Alert 代码,让它的扩展性更好一些。重构的内容主要包含两部分:
第一部分是将 check() 函数的多个入参封装成 ApiStatInfo 类;
第二部分是引入 handler 的概念,将 if 判断逻辑分散在各个 handler 中。
具体代码如下:

class AlertRule {/*...*/};
class Notification {/*...*/};
class ApiStatInfo
{
public:
	std::string getApi()const
	{
		return api;
	}
	long getRequestCount()const
	{
		return requestCount;
	}
	long getErrorCount()const
	{
		return errorCount;
	}
	long getDurationOfSeconds()const
	{
		return durationOfSeconds;
	}
	long getTimeoutCount()const
	{
		return timeoutCount;
	}
private:
	std::string api;
	long requestCount;
	long errorCount;
	long durationOfSeconds;
	long timeoutCount;
};

class AlertHandler
{
public:
	AlertHandler(AlertRule rule, Notification notification) :
		rule(rule), notification(notification) {}
	virtual ~AlertHandler() {}
	virtual void check(ApiStatInfo apiStatInfo) = 0;
	
	AlertRule rule;
	Notification notification;
};

class TpsAlertHandler : public AlertHandler
{
	TpsAlertHandler(AlertRule rule, Notification notification) :
		AlertHandler(rule, notification) {}
	void check(ApiStatInfo apiStatInfo) override
	{
		long tps = apiStatInfo.getRequestCount() / apiStatInfo.getDurationOfSeconds();
		if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps())
		{
			notification.notify(NotificationEmergencyLevel.URGENCY, "...");
		}
	}
};
class ErrorAlertHandler : public AlertHandler
{
	ErrorAlertHandler(AlertRule rule, Notification notification) :
		AlertHandler(rule, notification) {}

	void check(ApiStatInfo apiStatInfo)override
	{
		if(apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi().getMaxErrorCount()))
		{
			notification.notify(NotificationEmergencyLevel.URGENCY, "...");
		}
	}
};

对Alert重构后,对其的使用需要再添加一个ApplicationContext单例类,负责Alert的创建、组装、初始化等

class ApplicationContext
{
private:
	static AlertRule* rule;
	static Notification* notification;
	static Alert* alert;
	static ApplicationContext* instance;
private:
	ApplicationContext() = default;
	~ApplicationContext() = default;
	ApplicationContext(const ApplicationContext&) = delete;
	ApplicationContext& operator=(const ApplicationContext&) = delete;
public:

	static ApplicationContext* Instance()
	{
		instance = new ApplicationContext();
		return instance;
	}
	
};
AlertRule* ApplicationContext::rule = new AlertRule();
Notification* ApplicationContext::notification = new Notification();
Alert* ApplicationContext::alert = new Alert();
ApplicationContext* instance = nullptr;
};
AlertRule ApplicationContext::rule;
Notification ApplicationContext::notification;
Alert ApplicationContext::alert;
ApplicationContext* instance = nullptr;

现在,我们再来看下,基于重构之后的代码,如果再添加上面讲到的那个新功能,每秒钟接口超时请求个数超过某个最大阈值就告警,我们又该如何改动代码呢?主要的改动有下面四处:

  1. 在 ApiStatInfo 类中添加新的属性 timeoutCount。
  2. 添加新的 TimeoutAlertHander 类。
  3. 在 ApplicationContext 类的 initializeBeans() 方法中,往 alert 对象中注册新的 timeoutAlertHandler。
  4. 在使用 Alert 类的时候,需要给 check() 函数的入参 apiStatInfo 对象设置 timeoutCount 的值

重构之后的代码更加灵活和易扩展。如果我们要想添加新的告警逻辑,只需要基于扩展的方式创建新的 handler 类即可,不需要改动原来的 check() 函数的逻辑。而且,我们只需要为新的 handler 类添加单元测试,老的单元测试都不会失败,也不用修改。

修改代码就意味着违背开闭原则吗?

在重构之后的 Alert 代码中,我们的核心逻辑集中在 Alert 类及其各个 handler 中,当我们在添加新的告警逻辑的时候,Alert 类完全不需要修改,而只需要扩展一个新 handler 类。如果我们把 Alert 类及各个 handler 类合起来看作一个“模块”,那模块本身在添加新的功能的时候,完全满足开闭原则。
而且,我们要认识到,添加一个新功能,不可能任何模块、类、方法的代码都不“修改”,这个是做不到的。类需要创建、组装、并且做一些初始化操作,才能构建成可运行的的程序,这部分代码的修改是在所难免的。我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。

如何做到“对扩展开放、修改关闭”?

实际上,开闭原则讲的就是代码的扩展性问题,是判断一段代码是否易扩展的“金标准”。如果某段代码在应对未来需求变化的时候,能够做到“对扩展开放、对修改关闭”,那就说明这段代码的扩展性比较好。所以,问如何才能做到“对扩展开放、对修改关闭”,也就粗略地等同于在问,如何才能写出扩展性好的代码。

为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识

在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。还有,在识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。

如何在项目中灵活应用开闭原则

前面我们提到,写出支持“对扩展开放、对修改关闭”的代码的关键是预留扩展点。那问题是如何才能识别出所有可能的扩展点呢?
如果你开发的是一个业务导向的系统,比如金融系统、电商系统、物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解,能够知道当下以及未来可能要支持的业务需求。如果你开发的是跟业务无关的、通用的、偏底层的系统,比如,框架、组件、类库,你需要了解“它们会被如何使用?今后你打算添加哪些功能?使用者未来会有哪些更多的功能需求?”等问题。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值