learn - 软件设计原则与模式
应当知道的
设计模式与设计原则
7大设计原则
- (1/7)单一职责原则(Single Responsibility Principle, SRP)
一个类应该只负责一个功能,降低类的复杂度,
提高类的可读性和可维护性,减少变更带来的风险。
应当有且仅有一个原因引起类的变更。
1 2 3 4 5 6
// userDAO user Data Access Object type userDAO struct {} func (*userDAO) GetOne(data *model.User) (*model.User, error) {} func (*userDAO) Insert(data *model.User) (*model.User, error) {}
例如上述的单独的对
user对象进行数据库的读和写的交互, 不存在其他业务干涉。其实跟大多项目自己弄的util包的函数很像,具体的。
缺点:增加类的数量;增加初期开发成本;维护成本增加。
优点:降低类的复杂度;减少了耦合;变更的影响范围变小,降低了因变更带来的风险,提高系统的可扩展性、可读性和可维护性。
(2/7)里氏替换原则(Liskov Substitution Principle, LSP)
子类可以扩展父类的功能,但不能改变父类的功能,
保证父类与子类之间的基本契约关系不受破坏。
只要父类出现的地方子类就可以出现,而且替换为子类不会出现任何错误,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
package handler ... var UserHdl = &userHandler{} type userHandler struct { base.BaseHandler } // Detail 用户详情 func (this *userHandler) Detail(ctx *gin.Context) { userId, _: =this.GetParamInt(ctx, "userId", 0) data, err := user_app.UserSrv.GetDetailById(userId) this.SendJson(ctx, data, err) }
例如
this.GetParamInt(ctx, "userId", 0)和this.SendJson(ctx, data, err)都直接使用父类的,并不改变父类的功能。缺点:增加了代码的耦合性,降低了代码的灵活性。
优点:代码共享,父类共享给子类;提高代码的可扩展性。
用好了,利大于弊。
- (3/7)依赖倒置原则(Dependency Inversion Principle, DIP)
高层模块不应该依赖低层模块,二者都应该依赖于抽象;
抽象不应该依赖于细节,细节应该依赖于抽象。
1 2 3 4 5 6 7 8 9 10 11
type RewardFactory interface { // CheckReward 检测是否可以领取,返回error为校验失败错误信息 CheckReward(param *reward_def.CheckRewardParam) error // GetTaskReward 任务奖励,返回本次可以加的奖励和失败信息 GetTaskReward(param *reward_def.CheckRewardParam) (*reward_def.AddFinanceDto, error) // SaveRewardState 保存奖励信息标记 SaveRewardState(param *reward_def.CheckRewardParam, financeDto *reward_def.AddFinanceDto) error //GetTaskType 返回当前任务类型 GetTaskType() int }
例如上述:做任务领取奖励,任何类型不关心底层结构定义。
缺点:增加设计复杂度;调试难度增加,可能需要更多的时间和精力来定位问题。
优点:降低模块间的耦合度,抽象类不用去继承具体类;提高代码的可维护性和可扩展性,都是通过**依赖**抽象接口。
- (4/7)接口隔离原则(Interface Segregation Principle, ISP)
客户端不应该被迫依赖于它不需要的方法,
一个类对另一个类的依赖应该建立在最小接口上。
接口隔离原则主要应用于大型接口拆分、客户端定制化和预防胖接口等场景。当一个接口过于庞大,包含了许多不相关的方法时,应当考虑将其拆分成更小的、更具体的接口,以提高系统的灵活性和可扩展性。
缺点:增加设计复杂度;可能增加代码冗余;可能影响性能。
优点:减少冗余;提高灵活性;降低耦合;提高了系统的内聚性,增强系统的可扩展性,提高代码的可读性和可维护性。
这个要跟单一职责原则做一下对比:
共同:
都是为了提高类的内聚性、降低类之间的耦合性,体现了封装的思想。
它们都致力于将接口约束到最小功能,确保系统的灵活性和可维护性。
区别:
- 关注点不同:单一职责原则注重的是职责的划分,即一个类应该只有一个改变的原因;而接口隔离原则注重的是对接口依赖的隔离,即一个类不应该依赖于它不需要的接口。
- 应用范围不同:单一职责原则主要是约束类,针对程序中的实现和细节;接口隔离原则主要约束接口,针对抽象和程序整体框架的构建。
- 目的不同:单一职责原则是为了减少代码重复,提高代码的可读性和可维护性;接口隔离原则是为了减少系统间的耦合,提高系统的灵活性和可扩展性。
- (5/7)迪米特法则(Law of Demeter, LoD)
最少知识原则(Least Knowledge Principle)
一个对象应该对其他对象有尽可能少的了解,
即一个对象对另一个对象知道的越少越好。
迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
//package a type AdvertUserA struct {} func (*AdvertUserA) Price() float64 { return 0.5 } //package b type DSPSrv struct {} func (*DSPSrv) GetPriceByAID(aID int) float64 { ..... return 0.6*0.8 } //package c type Report struct {} func (*DSPSrv) PrintAllPrice() { dspSrv:=&DSPSrv{} ... for _,aID:=range aIDList { price :=dspSrv.GetPriceByAID(aID) fmt.Println(aid,price) } }
例如上述:对象Report通过产生的aIDList记录打印出对应的出价,不需要了解每个AdvertUser,增加了一个中介类DSPSrv来获取。enn,还可以通过在(7/7)的例子,目录dml的文件作为目录dal的中介类,不需要关注各自实体的DAO是如何交互的。
有些地方可能会引入一个抽象类,让其对抽象依赖。这样就比较类似依赖倒置原则。
缺点:引入过多中阶层;性能可能会下降;增加复杂度。
优点:降低耦合度;增强模块独立;简化接口。
- (6/7)开闭原则(开放封闭原则)(Open-Closed Principle, OCP)
软件实体应对扩展开放,对修改关闭, 即当需求变化时,可以通过添加新的代码进行扩展来满足新的需求,而不需要修改现有的代码。
一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// Style 样式配置数据表字段 type Style struct { ... Extra string `zh:"扩展字段信息" form:"extra" json:"extra"` } type PopBox struct { Title string `zh:"标题" form:"title" json:"title"` Img string `zh:"图片" form:"img" json:"img"` Desc string `zh:"描述" form:"desc" json:"desc"` } func (this *Style) ToPopBox() *PopBox { extra := new(PopBox) _ = json.Unmarshal([]byte(this.Extra), &extra) return extra }
如果继续增加新样式,对
Style的增、删、改、查都不需要改动,可以增加新代码来扩展结构体:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// ADLine 广告文字链 type ADLine struct { Intro string `json:"intro"` //用于后台查看说明 List []*ADItem `json:"list"` //样式列表 } type ADItem struct { Desc string `json:"desc"` Img string ` json:"img"` ClickUrl string ` json:"click_url"` } func (this *Style) ToADLine() *ADLine { extra := new(ADLine) _ = json.Unmarshal([]byte(this.Extra), &extra) return extra }
缺点: 面向对象的抽象难度大。
优点: 提高代码的可维护性;增强代码的可扩展性;促进代码的复用性;便于团队写作。
- (7/7)合成、聚合、复用原则(Composition/Aggregation Reuse Principle, CARP)
尽量使用对象的组合/聚合来达到复用的目的, 而不是通过继承关系达到复用的目的。
1 2 3 4 5 6 7 8 9 10
——user |——assemble // 聚合user、user_ext、user_group功能,对外复用 |——def |——internal |——dal └──dml |——user.go // 合成复用dml中对user单一职责访问操作的结果 |——user_ext.go └──user_group.go
聚合:表示“拥有”、整体与部分的关系。
部分对象可以被多个整体对象共享,且部分对象的生命周期可以独立于整体对象。例如,一个班级可以包含多个学生,删除班级不会影响学生的存在。
合成:表示一中更强的“拥有”关系。
整体与部分的生命周期相同,部分对象完全属于整体对象,且不能被其他整体对象共享。例如,一个人的头和四肢属于这个人,删除这个人也会删除其头和四肢。
缺点:增加管理的复杂度;可能会影响性能。
优点:灵活性、降低代码复杂度;黑箱复用。
设计模式
| 创建型模式 | 结构型模式 | 行为型模式 |
|---|---|---|
| 单例模式 | 适配器模式 | 责任链模式 |
| 抽象工厂模式 | 桥接模式 | 命令模式 |
| 工厂模式 | 过滤器模式 | 解释器模式 |
| 建造者模式 | 组合模式 | 迭代器模式 |
| 原型模式 | 装饰器模式 | 中介者模式 |
| 对象池模式 | 外观模式 | 备忘录模式 |
| 多例模式 | 享元模式 | 观察者模式 |
| 静态工厂模式 | 代理模式 | 状态模式 |
| 数据映射模式 | 空对象模式 | |
| 依赖注入模式 | 策略模式 | |
| 门面模式 | 模板模式 | |
| 流接口模式 | 访问者模式 | |
| 注册模式 | 规格模式 | |
| 规格模式 |
创建型模式
创建型模式(Creational Patterns)- 这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
结构型模式
结构型模式(Structural Patterns)- 这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
行为型模式
行为型模式(Behavioral Patterns)- 这些设计模式特别关注对象之间的通信。