定义
对扩展开放,对修改关闭 详细说就是新增一个业务功能的时候,在已有代码的基础上去扩展(新增模块、类、方法),不要去修改已有代码(修改模块、类、方法)
什么是合格的修改
只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。 添加一个新功能,不可能任何模块、类、方法的代码都不“修改”,这个是做不到的。哪怕在已有代码基础上扩展一个新的类,类需要创建、组装、并且做一些初始化操作,才能构建成可运行的的程序,这部分代码是无法避免的。 我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂、最底层的那部分逻辑代码保持不变。
如何做到"对扩展开放,修改关闭"
指导思想
为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。 写代码的时候,多花时间往前思考一下未来这段代码的需求会发生什么变化,如何设计代码结构,预留扩展点,在需求发生变更的时候,不改动代码整体结构,以最小的代码改动代价将新功能实现代码插到扩展点上。 还有,识别出可变部分和不可变部分,将可变部分封装起来,隔离变化,提供抽象的稳定接口给上层使用,这样具体实现发生变化的时候,只需要新增一个接口的实现,然后替换掉原来的实现即可,上游不会感知下游的变化。
具体方法论
常用来提高可扩展性的方法有:多态、面向接口而非实现编程、依赖注入,以及一些设计模式(策略、模板方法、状态、装饰器、责任链等等)
如何在项目中灵活运用开闭原则?
运用好开闭原则的关键是预留扩展点
如何识别出扩展点
如果开发的是业务系统,那么就需要我们对业务足够了解,知道未来可能会扩展哪些需求;如果开发的底层框架、类库、组件,那么就需要知道框架会被如何使用,以后打算扩展哪些功能。 当然,我们不一定能识别出所有的扩展点,即使识别出所有的扩展点,预留这些扩展点的成本也很大,没有必要。 合适的做法是,对于一些短期内确定会扩展的需求,或者需求变动时对代码结构的影响较大,或者扩展点实现成本不高的地方我们去预留扩展点。 提高扩展性也是有代价的,有时会降低代码的可读性,所以我们要权衡,某些场景下扩展性很重要那么可以牺牲一点可读性,某些场景下可读性很重要那么可以牺牲一点扩展性。
最佳实践
这是一段 API 接口监控告警的代码。其中,AlertRule 存储告警规则,可以自由设置。Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。NotificationEmergencyLevel 表示通知的紧急程度,包括 SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要),不同的紧急程度对应不同的发送渠道。关于 API 接口监控告警这部分,更加详细的业务需求分析和设计,我们会在后面的设计模式模块再拿出来进一步讲解,这里你只要简单知道这些,就够我们今天用了。 设计实现这段代码,在新增一个报警需求时做到尽量小的代码改动
版本1
type AlterRule struct{}
func (r *AlterRule) GetMatchedMaxTps(api string) int {
return 1000
}
func (r *AlterRule) GetMatchedMaxErrorCount(api string) int {
return 1000
}
type Notification struct{}
func (n *Notification) Notify(level string, msg string) {
fmt.Printf("level:%v, send msg: %v", level, msg)
}
type Alter struct {
AlterRule *AlterRule
Notification *Notification
}
func NewAlter(rule *AlterRule, notification *Notification) *Alter {
return &Alter{
AlterRule: rule,
Notification: notification,
}
}
func (a *Alter) Check(api string, reqCount int, errCount int, durationSecond int) {
tps := reqCount / durationSecond
if tps > a.AlterRule.GetMatchedMaxTps(api) {
a.Notification.Notify("URGENCY", "Tps More Than 1000")
}
if errCount > a.AlterRule.GetMatchedMaxErrorCount(api) {
a.Notification.Notify("ERROR", "Error More Than 1000")
}
}
现在我们要扩展一个新的报警处理功能,比如超时的请求个数超过100个就报警,那么就需要修改Check方法的定义和代码实现,这样就会影响代码的正常运行,也会影响Check方法的单测,不是一个合适的改动
怎么样遵循开闭原则呢? 我们可以看出这里的可变部分就是不同的报警处理功能,我们可以将报警功能封装起来,对外暴露一个AlterHandler接口,新增报警处理功能时只需要新增一个接口的实现即可
版本2
type AlterRuleV2 struct{}
func (r *AlterRuleV2) GetMatchedMaxTps(api string) int {
return 1000
}
func (r *AlterRuleV2) GetMatchedMaxErrorCount(api string) int {
return 1000
}
func (r *AlterRuleV2) GetMatchedMaxTimeoutCount(api string) int {
return 100
}
type NotificationV2 struct{}
func (n *NotificationV2) Notify(level string, msg string) {
fmt.Printf("level:%v, send msg: %v", level, msg)
}
type ApiStatInfo struct {
Api string
ReqCount int
ErrCount int
TimeoutCount int
DurationSecond int
}
type IAlterHandler interface {
Check(info *ApiStatInfo)
}
type AbstractAlterHandler struct {
AlterRule *AlterRuleV2
Notification *NotificationV2
}
type TpsAlterHandler struct {
AbstractAlterHandler
}
func (h *TpsAlterHandler) Check(info *ApiStatInfo) {
tps := info.ReqCount / info.DurationSecond
if tps > h.AlterRule.GetMatchedMaxTps(info.Api) {
h.Notification.Notify("URGENCY", "Tps More Than 1000")
}
}
type ErrorAlterHandler struct {
AbstractAlterHandler
}
func (h *ErrorAlterHandler) Check(info *ApiStatInfo) {
if info.ErrCount > h.AlterRule.GetMatchedMaxErrorCount(info.Api) {
h.Notification.Notify("ERROR", "Error More Than 1000")
}
}
type TimeoutAlterHandler struct {
AbstractAlterHandler
}
func (h *TimeoutAlterHandler) Check(info *ApiStatInfo) {
if info.TimeoutCount > h.AlterRule.GetMatchedMaxTimeoutCount(info.Api) {
h.Notification.Notify("ERROR", "Timeout More Than 100")
}
}
type AlterV2 struct {
AlterHandlerList []IAlterHandler
}
func (a *AlterV2) AddAlterHandler(alterHandlerList []IAlterHandler) {
a.AlterHandlerList = append(a.AlterHandlerList, alterHandlerList...)
}
func (a *AlterV2) Check(info *ApiStatInfo) {
for i := range a.AlterHandlerList {
a.AlterHandlerList[i].Check(info)
}
}
|