对扩展开放、修改关闭 (Open Closed Principle)
software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification
直译: 软件实体(模块、类、方法等)应该 “ 对扩展开放、对修改关闭 ”。
白话: 添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
重要性: 大部分设计模式是为了解决代码的扩展性问题而存在的,主要遵从的设计原则:开闭原则。
同样的代码改动,在粗代码粒度,可能被认定为“修改”;在细代码粒度,可能又被认定为“扩展”。
开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
核心: 尽量让修改操作更集中、更少、更上层,让最核心、最复杂部分逻辑代码满足开闭原则。
思考: 代码未来可能有哪些需求变更、如何设计代码结构
预留: 事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上
识别: 识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用. 当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。
可以根据每个人的开发工作所属领域进行区分: 偏业务还是偏技术?
可以通过业界成熟的手段践行开闭原则思想:
/**
* @Author wangshuai52
* @desc 当接口请求个数超过某个阈值发出告警
* @aim Go语言-开闭原则
*/
package main
import "fmt"
// 报警规则
type AlertRule struct{}
// tps阈值
func (a *AlertRule) GetTpsCount(api string) int64 {
return 10
}
// 错误数阈值
func (a *AlertRule) GetErrCount(api string) int64 {
return 10
}
// 告警通知
type Notification struct{}
func (n *Notification) Notify(level string) {
fmt.Println("APP Notify: ", level)
}
// 监控接口
type Alert struct {
rule *AlertRule
notification *Notification
}
// 构造函数创建实例,如果单实例参考之前文档单实例创建方式
func NewAlert(rule *AlertRule, notification *Notification) *Alert {
return &Alert{
rule: rule,
notification: notification,
}
}
// 检查
func (a *Alert) check(api string, requestCnt, errCnt, durationOfSeconds int64) {
// 每秒事务处理数
tps := requestCnt / durationOfSeconds
// tps告警
if tps > a.rule.GetTpsCount(api) {
a.notification.Notify("请求数太多了!")
}
// 错误数告警
if errCnt > a.rule.GetErrCount(api) {
a.notification.Notify("错误数太多了!")
}
}
// main主函数
func main() {
aa := NewAlert(&AlertRule{}, &Notification{})
aa.check("pay", 100, 100, 1)
}
以上代码实现非常简单,但是当需要增加一个新功能,比如当每秒钟超时请求个数超过某个阈值,需要触发告警通知,针对以上代码设计需要修改两处:
//接口处增加timeout入参
func (a *Alert) check(api string, requestCnt, errCnt, durationOfSeconds int64) {
// 每秒事务处理数
tps := requestCnt / durationOfSeconds
// tps告警
if tps > a.rule.GetTpsCount(api) {
a.notification.Notify("请求数太多了!")
}
// 错误数告警
if errCnt > a.rule.GetErrCount(api) {
a.notification.Notify("错误数太多了!")
}
//此处增加超时逻辑,并扩展rule和Notify
}
借鉴开闭原则进行优化:
UML类图:
抽象理解如图所示,红色为需要新增的部分代码
package main
import "fmt"
// 告警处理器群 实例
type Alertx struct {
AlertHandlers []AlertHandler // 注册存储不同告警功能模块
}
// 告警处理器
type AlertHandler interface {
Check(info ApiInfo)
}
// Api数据指标
type ApiInfo struct {
Api string
RequestCnt int64
ErrCnt int64
DurOfSecs int64
TimeoutCnt int64
}
// 处理器群 构造函数
func NewAlertx() *Alertx {
return &Alertx{
AlertHandlers: make([]AlertHandler, 0),
}
}
// 处理器群 添加 处理器
func (a *Alertx) AddHandler(aHandler AlertHandler) {
a.AlertHandlers = append(a.AlertHandlers, aHandler)
}
// 处理器群 轮询处理
func (a *Alertx) CheckAll(aInfo ApiInfo) {
for _, handler := range a.AlertHandlers {
handler.Check(aInfo)
}
}
// 报警规则
type AlertRule struct{}
// tps阈值
func (a *AlertRule) GetTpsCount(api string) int64 {
return 10
}
// 错误数阈值
func (a *AlertRule) GetErrCount(api string) int64 {
return 10
}
// 告警通知
type Notification struct{}
func (n *Notification) Notify(level string) {
fmt.Println("APP Notify: ", level)
}
// 实现AlertHandler接口,按照TPS规则
type TpsAlertHandler struct {
rule *AlertRule
notification *Notification
}
func NewTpsAlertHandler(rule *AlertRule, notification *Notification) *TpsAlertHandler {
return &TpsAlertHandler{
rule: rule,
notification: notification,
}
}
func (t *TpsAlertHandler) Check(info ApiInfo) {
tps := info.RequestCnt / info.DurOfSecs
if tps > t.rule.GetTpsCount(info.Api) {
t.notification.Notify("TPS 太高了!!")
}
fmt.Println("TPS处理完成")
}
// 实现AlertHandler接口,按照Err规则实现
type ErrAlertHandler struct {
rule *AlertRule
notification *Notification
}
func NewErrAlertHandler(rule *AlertRule, notification *Notification) *ErrAlertHandler {
return &ErrAlertHandler{
rule: rule,
notification: notification,
}
}
// 实现AlertHandler接口
func (e *ErrAlertHandler) Check(info ApiInfo) {
if info.ErrCnt > e.rule.GetErrCount(info.Api) {
e.notification.Notify("ERR数 太高了!!")
}
fmt.Println("Err处理完成")
}
//------------------------------------
// 新增超时处理,struct也要新增数据字段TimeoutCnt
func (a *AlertRule) GetTimeoutCount(api string) int64 {
return 10
}
type TimeoutAlertHandler struct {
rule *AlertRule
notification *Notification
}
func NewTimeoutAlertHandler(rule *AlertRule, notification *Notification) *TimeoutAlertHandler {
return &TimeoutAlertHandler{
rule: rule,
notification: notification,
}
}
func (t *TimeoutAlertHandler) Check(info ApiInfo) {
if info.TimeoutCnt > t.rule.GetTimeoutCount(info.Api) {
t.notification.Notify("Timeout数 太高了!!")
}
fmt.Println("Timeout处理完成")
}
//------------------------------------
func main() {
alerts := NewAlertx()
infos := ApiInfo{
Api: "pay",
RequestCnt: 100,
ErrCnt: 100,
DurOfSecs: 1,
TimeoutCnt: 100,
}
alerts.AddHandler(NewTpsAlertHandler(&AlertRule{}, &Notification{}))
alerts.AddHandler(NewErrAlertHandler(&AlertRule{}, &Notification{}))
alerts.AddHandler(NewTimeoutAlertHandler(&AlertRule{}, &Notification{}))
alerts.CheckAll(infos)
}
运行结果:
提高代码扩展性也有成本。代码的扩展性会跟可读性相冲突。
为了更好地支持扩展性,我们对代码进行了重构工作,重构之后的代码要比之前的代码复杂很多,理解起来也更加有难度。很多时候,我们都需要在扩展性和可读性之间做权衡。
在某些场景下,代码的扩展性很重要,我们就可以适当地牺牲一些代码的可读性;
在另一些场景下,代码的可读性更加重要,那我们就适当地牺牲一些代码的可扩展性。
没有一个放之四海而皆准的参考标准,全凭实际的应用场景来决定。
对拓展开放是为了应对变化(需求),对修改关闭是为了保证已有代码的稳定性