文章

learn - 软件设计原则与模式

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)

    客户端不应该被迫依赖于它不需要的方法,

    一个类对另一个类的依赖应该建立在最小接口上。

    接口隔离原则主要应用于大型接口拆分、客户端定制化和预防胖接口等场景。当一个接口过于庞大,包含了许多不相关的方法时,应当考虑将其拆分成更小的、更具体的接口,以提高系统的灵活性和可扩展性。

    缺点:增加设计复杂度;可能增加代码冗余;可能影响性能。

    优点:减少冗余;提高灵活性;降低耦合;提高了系统的内聚性,增强系统的可扩展性,提高代码的可读性和可维护性。

    这个要跟单一职责原则做一下对比:

    共同:

    都是为了提高类的内聚性、降低类之间的耦合性,体现了封装的思想。

    它们都致力于将接口约束到最小功能,确保系统的灵活性和可维护性。

    区别:

    1. 关注点不同:单一职责原则注重的是职责的划分,即一个类应该只有一个改变的原因;而接口隔离原则注重的是对接口依赖的隔离,即一个类不应该依赖于它不需要的接口。
    2. 应用范围不同:单一职责原则主要是约束类,针对程序中的实现和细节;接口隔离原则主要约束接口,针对抽象和程序整体框架的构建。
    3. 目的不同:单一职责原则是为了减少代码重复,提高代码的可读性和可维护性;接口隔离原则是为了减少系统间的耦合,提高系统的灵活性和可扩展性。
  • (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)- 这些设计模式特别关注对象之间的通信。

本文由作者按照 CC BY 4.0 进行授权