Go设计模式学习——开闭原则

Go设计模式学习——开闭原则_第1张图片

1 开闭原则

对扩展开放、修改关闭 (Open Closed Principle)

software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification

直译: 软件实体(模块、类、方法等)应该 “ 对扩展开放、对修改关闭 ”

白话: 添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

重要性: 大部分设计模式是为了解决代码的扩展性问题而存在的,主要遵从的设计原则:开闭原则。

1.1 问题

  • 怎样的代码改动才被定义为‘扩展’
  • 怎样的代码改动才被定义为‘修改’
  • 怎么才算满足或违反‘开闭原则’
  • 修改代码就一定意味着违反‘开闭原则’吗

同样的代码改动,在粗代码粒度,可能被认定为“修改”;在细代码粒度,可能又被认定为“扩展”。

开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。

核心: 尽量让修改操作更集中、更少、更上层,让最核心、最复杂部分逻辑代码满足开闭原则。

1.2 指导思想

1.2.1 底层思维
  • 扩展意识
  • 抽象意识
  • 封装意识

思考: 代码未来可能有哪些需求变更、如何设计代码结构

预留: 事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上

识别: 识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用. 当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。

1.2.2 如何应用

可以根据每个人的开发工作所属领域进行区分: 偏业务还是偏技术?

  • 业务系统
    • 对业务有足够的了解
    • 知道当下以及未来可能要支持的业务需求
  • 通用的、偏底层的系统(框架、组件、类库)
    • 它们会被如何使用?
    • 今后你打算添加哪些功能?
    • 使用者未来会有哪些更多的功能需求?

可以通过业界成熟的手段践行开闭原则思想:

  • 多态
  • 依赖注入
  • 基于接口而非实现编程
  • 大部分的设计模式(比如,装饰、策略、模板、职责链、状态等)
1.2.3 多态栗子
/**
 * @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)
}

运行结果
Go设计模式学习——开闭原则_第2张图片

以上代码实现非常简单,但是当需要增加一个新功能,比如当每秒钟超时请求个数超过某个阈值,需要触发告警通知,针对以上代码设计需要修改两处:

  • 修改check接口增加timeout入参
  • check内部增加超时逻辑相关代码
//接口处增加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类图:

  • 蓝色是go的构造函数(直接写的函数实现)
  • 红色是新增超时告警时需要新增的代码

Go设计模式学习——开闭原则_第3张图片

抽象理解如图所示,红色为需要新增的部分代码

Go设计模式学习——开闭原则_第4张图片

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)
}

运行结果:

Go设计模式学习——开闭原则_第5张图片

1.3 总结

1.3.1 唯一不变的是变化本身
  • 即便我们对业务、对系统有足够的了解,那也不可能识别出所有的扩展点
  • 即便你能识别出所有的扩展点,为这些地方都预留扩展点,这样做的成本也是不可接受的。
  • 我们没必要为一些太遥远的、不一定发生的需求去提前买单,做过度设计。
1.3.2 最佳实践
  • 对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。
  • 对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。
1.3.3 扩展性与可读性

提高代码扩展性也有成本。代码的扩展性会跟可读性相冲突。

为了更好地支持扩展性,我们对代码进行了重构工作,重构之后的代码要比之前的代码复杂很多,理解起来也更加有难度。很多时候,我们都需要在扩展性和可读性之间做权衡。

在某些场景下,代码的扩展性很重要,我们就可以适当地牺牲一些代码的可读性;

在另一些场景下,代码的可读性更加重要,那我们就适当地牺牲一些代码的可扩展性。

1.4 结束语

没有一个放之四海而皆准的参考标准,全凭实际的应用场景来决定。

对拓展开放是为了应对变化(需求),对修改关闭是为了保证已有代码的稳定性

Go设计模式学习——开闭原则_第6张图片

你可能感兴趣的:(设计模式,Golang学习,golang,开闭原则,开发语言)