1. 概览

学习设计模式的前提需要先了解设计原则,下面是学习设计模式过程中的学习记录和理解。

对于面向对象软件系统的设计而言,在支持可维护性的同时,提高系统的可复用性是一个至关重要的问题,如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之一。在面向对象设计中,可维护性的复用是以设计原则为基础的。每一个原则都蕴含一些面向对象设计的思想,可以从不同的角度提升一个软件结构的设计水平。

面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。面向对象设计原则也是我们用于评价一个设计模式的使用效果的重要指标之一。

原则的目的: 高内聚,低耦合

image-20230830001846615

2. 单一职责原则 SRP

一个类对外只提供一种功能,引起类的变化的原因只有一个。

type IUserSession interface {
    Login()
    Logout()
}
type IUserGrade interface {
    Upgrade()
    Downgrade()
}

type User struct {
    Name    string
    Grade   int
    Session bool
}

func (u *User) Login() {
    fmt.Println(u.Name + " login")
    u.Session = true
}

func (u *User) Logout() {
    fmt.Println(u.Name + " logout")
    u.Session = false
}

func (u *User) Upgrade() {
    if u.Session {
        fmt.Println(u.Name + " upgrade")
        u.Grade++
    } else {
        fmt.Println(u.Name + " please login")
    }
}

func (u *User) Downgrade() {
    if u.Session {
        fmt.Println(u.Name + " downgrade")
        u.Grade--
    } else {
        fmt.Println(u.Name + " please login")
    }
}

func TestSingle(t *testing.T) {
    u := User{Name: "小米", Grade: 10}
    u.Login()
    u.Upgrade()
    u.Downgrade()
    u.Logout()
    u.Downgrade()
}
小米 login
小米 upgrade
小米 downgrade
小米 logout
小米 please login

3. 开闭原则 OCP

对扩展开放,对修改封闭,用接口构建框架,后面就不能修改了,然后用实现扩展细节。类的改动通过增加代码实现,不是修改代码。

type BusinessWorker interface {
    Work(*User)
}

type TransferWorker struct {
    ID int
}

func (w *TransferWorker) Work(u *User) {
    fmt.Println(fmt.Sprintf("%d业务员为%s转账", w.ID, u.Name))
}

type SaveWorker struct {
    ID int
}

func (w *SaveWorker) Work(u *User) {
    fmt.Println(fmt.Sprintf("%d业务员为%s存钱", w.ID, u.Name))
}

type User struct {
    Name string
}

func TestOCP(t *testing.T) {
    u := &User{Name: "小米"}
    w1 := TransferWorker{ID: 1}
    w2 := SaveWorker{ID: 2}
    w1.Work(u)
    w2.Work(u)
}
1业务员为小米转账
2业务员为小米存钱   

4. 里氏替换原则 LSP

子类可以扩展父类的功能,但是不会改变原有的功能。

将其拓展来理解,可以包含以下含义:

  • 子类可以覆盖父类抽象的方法,但是不能覆盖非抽象的方法;
  • 子类可以增加非抽象的方法;
  • 子类覆盖的抽象方法前置条件要宽松于父类,后置条件可以严格于父类;
type Math interface {
    Add(a, b int) int
}

type Parent struct {
}

func (p Parent) Add(a, b int) int {
    return a + b
}

type Son struct {
    Parent
}

// Add 这里父类原有的功能被修改了 破坏了里氏替换原则
func (s Son) Add(a, b int) int {
    return a * b
}
func (s Son) Sub(a, b int) int {
    return a - b
}

func TestLSP(t *testing.T) {
    var p Math = Parent{}
    var s Math = Son{}
    fmt.Println(p.Add(7, 8))
    fmt.Println(s.Add(7, 8))
    ss := s.(Son)
    fmt.Println(ss.Sub(7, 8))
}
15
56
-1

5. 依赖倒转原则 DIP

高层模块不依赖低层模块,两种都应该依赖抽象,降低耦合度,定义时类型为抽象接口类型,方法接收抽象接口方法。

type BusinessWorker interface {
    Work(*User)
}

type TransferWorker struct {
    ID int
}

func (w *TransferWorker) Work(u *User) {
    fmt.Println(fmt.Sprintf("%d业务员为%s转账", w.ID, u.Name))
}

type SaveWorker struct {
    ID int
}

func (w *SaveWorker) Work(u *User) {
    fmt.Println(fmt.Sprintf("%d业务员为%s存钱", w.ID, u.Name))
}

func HandlerWork(w BusinessWorker, u *User) {
    w.Work(u)
}

type User struct {
    Name string
}

func TestDIP(t *testing.T) {
    u := &User{Name: "小米"}
    var w1 BusinessWorker = &TransferWorker{ID: 1}
    var w2 BusinessWorker = &SaveWorker{ID: 2}
    HandlerWork(w1, u)
    HandlerWork(w2, u)
}
1业务员为小米转账
2业务员为小米存钱

6. 接口隔离原则 ISP

接口应该轻量化,不要定义实现类不需要的方法,把臃肿复杂的接口分离开来,不要全部封装到一个接口上。

type IUser interface {
    Run()

    Walk()

    Read()
}

// 分离为3个

type IRun interface {
    Run()
}
type IWalk interface {
    Walk()
}
type IRead interface {
    Read()
}

7. 合成复用原则 CRP

image-20230830000743602

尽量优先使用传入参数组合的方式来达到复用目的,而不是通过继承。因为如果使用继承的化,父类有100个方法,原本只是想继承1个方法的,结果把100个方法都继承了,增加代码复杂度。

type Cat struct {
}

func (c *Cat) Eat() {
    fmt.Println("猫在吃饭")
}

type CatA struct {
    Cat
}

type CatB struct {
    C *Cat
}

func (c *CatB) Eat() {
    c.C.Eat()
}

type CatC struct {
}

func (c *CatC) Eat(cc *Cat) {
    cc.Eat()
}

func TestCRP(t *testing.T) {
    ca := CatA{}
    ca.Eat()
    cb := CatB{}
    cb.C.Eat()
    cc := CatC{}
    cc.Eat(&Cat{})
}
猫在吃饭
猫在吃饭
猫在吃饭

8. 迪米特法则 LoD

一个对象应该降低对其他对象的了解,互相调用时使用提供的统一的接口,来减低相互之间的耦合度(黑盒)

9. 总结

用抽象构建框架,用实现扩展细节

  1. 单一职责原则强调一个实现类只负责一个职责;
  2. 开闭原则的核心是对扩展开放,对修改封闭;
  3. 里氏替换原则强调子类不要修改父类方法实现的功能;
  4. 依赖倒装原则主张面向抽象接口提供方法服务;
  5. 接口隔离原则告诉我们接口设计要精简单一;
  6. 合成复用原则强调优先使用组合的方法通过服务;
  7. 迪米特法则核心是降低耦合;

设计原则主要重在理解,其实学完一遍之后,对其具体理解感觉还是一知半解,领悟还得靠后面的项目的实践。

10. 参考链接

文章目录