我在用 golang 的 interface 时候,总共写了3篇文章,大家可以关联着看,希望可以解决大家开发中遇到的一些问题

1. 单一职责原则(SRP)

1.1 什么是单一职责原则:

  • 单一职责原则对象应该仅具有一种单一功能
  • 为什么需要遵守单一职责原则?
    • 如果我们不遵守:同一个接口里面各个方法是会耦合的,所以当你一个接口含有多个职能的时候。可能当你改动一个其中一个方法的时候,说不定就会对其他职能有影响
    • 如果我们遵守:比如以下示例代码,当我们负责需要Send方法进行修改的时候难道分成了两个接口就不会有影响了吗?是的,依旧会有影响,要是你把Send()改崩了,推送消息照样挂。不过可以思考一下以下两个场景
      • 第一点:可是有没有想过接口当中可能耦合某个其他的方法,或者其他一些共同的变量,如果是分开两个接口,可能这些方法就是冗余两份,这样就不会互相影响
      • 第二点:对于你是开发者来说,要你和同事两个人维护推送消息模块,你是觉着大家都看所有的代码还是说大家就看自己负责的模块简单?或者说看可能都要看,那么平时改动后,调试和测试呢你是想就测试一个接口三个方法还是一个接口六个方法?

1.2 代码示例一

// 错误的
type Pusher interface {
    // 推送消息到华为,小米,苹果
    PushToHoner()
    PUshToXiaomi()
    PushToApple()


    Connect() // 建立和终端的连接
    Send() // 发送信息流
    Close() // 关闭终端之间的连接
}

// 正确的
type Pusher interface {
    // 推送消息到华为,小米,苹果
    PushToHoner()
    PUshToXiaomi()
    PushToApple()
}

type HttpClient interface {
    Connect() // 建立和终端的连接
    Send()    // 发送信息流
    Close()   // 关闭终端之间的连接
}

2. 开放封闭原则(OCP)

2.1 什么是开放封闭原则:

  • 开放封闭原则模块可扩展,而不可修改。也就是说,模块对扩展开放,而对修改封闭
  • 为什么要遵守开放封闭原则?
    • 如果我们不遵守(代码示例一)
      • 那么我们就需要改动 Connect 或者 Send,这样的话可能就对其他使用的人造成影响了。别人就想没有cookie进行连接,你非要整个cookie,万一别人网站对于这块有限制呢?
      • 有人说,那么就根据传参数决定是否需要设置cookie就好了,那这里就有一个问题,你就需要修改传入参数,这样所有调用了这个函数的都进行修改?
    • 如果我们遵守(代码示例一)
      • 那么我们如果要对http请求做一些修改,就调用SetCookies方法,如果我们还要做其他方法,就继续加其他操作

2.2 代码示例一

// 错误的
type HttpClient interface {
    // 修改 Connect or Send 方法达到cookie的目的
    Connect() // 建立和终端的连接 
    Send()    // 发送信息流
    Close()   // 关闭终端之间的连接
}

// 正确的
type HttpClient interface {
    Connect() // 建立和终端的连接
    Send()    // 发送信息流
    Close()   // 关闭终端之间的连接

    SetCookies() // 设置cookie
    SetUserAgent() // 设置请求头
}

3. 里氏替换原则(LSP)

3.1 什么是里氏替换原则:

  • 里氏替换原则
    • 其他语言(比如Java):一个对象在其出现的任何地方,都可以用子类实例做替换,并且不会导致程序的错误
    • go语言:golang中没有说类的概念,而是用interface。或者我们可以把interface就相当于抽象类,接口中的方法相当于抽象方法
  • 为什么需要里氏替换原则
    • 如果我们不遵守:
      • 则父类和子类的耦合性就会增强,比如下面的华为手机,重写父类userClient的PushMsg方法,当用HuaweiClient去替换userClient的时候就会发现可能影响原来的业务逻辑
    • 如果我们遵守:
      • 那么就可以将父类和子类的耦合性进行降低,你想想子类如果重写了父类的方法,则同一个方法实现细节不一致,那么当用子类去替换父类的时候,就会报错(备注:其实我目前没有想到类似的场景=-=)
      • 示例如下:这里苹果手机没有重写父类userClient的PushMsg方法,也就是说任何一个继承了userClient 的struct,都是可以去对 userClient进行替换,都不会影响系统的运行

3.2 代码示例一

package service

import "fmt"

// 用户终端
type IUserClient interface {
    PushMsg()
}
type UserClient struct {}
func (UserClient) PushMsg() {
    fmt.Println("push msg")
}

// 华为手机(没有遵守里氏替换原则)
type IHuaweiClient interface {
    IUserClient
    GetHuaweiVersion()
}
type HuaweiClient struct {
    UserClient
}
func (HuaweiClient) PushMsg(){
    fmt.Println("push msg to other svc.....")
}
func (HuaweiClient) GetHuaweiVersion() {}


// 苹果手机(遵守里氏替换原则)
type IAppleClient interface {
    IUserClient
    GetIosVersion()
}
type AppleClient struct {
    UserClient
}
func (AppleClient) GetIosVersion() {}

4. 接口隔离原则(ISP)

4.1 什么是接口隔离原则:

  • 一个类对另一个类的依赖应该建立在最小接口上
  • 建立单一接口,不要建立庞大臃肿的接口,接口中的方法尽量少

4.2 代码示例一

// Save writes the contents of doc to the file f.
func Save(f *os.File, doc *Document) error {
    return nil
}
  • 这里可以看到一个save方法,将一个文档写入到本地文件,这个方法有两个问题
  • 一:这里我们只要数据到本地文件,但是f这个参数os.File接口其实是实现了很多无关的方法,这里就导致save方法对于参数的依赖并不是最小接口


  • 二:这里我们目的是存储,而目前的储存方案是写入到本地文件,但是不排除后面我们会直接写入数据到网络,然后通过网络进行传输,所以这里也没有考虑到 向后兼容的问题

4.3 代码示例二

// Save_1 writes the contents of doc to the supplied ReadWriterCloser.
func Save_1(rwc io.ReadWriteCloser, doc *Document) error {
    return nil
}

// Save_2 writes the contents of doc to the supplied Writer.
func Save_2(rwc io.Writer, doc *Document) error {
    return nil
}
  • 这里一步步进行改进,可以看到save_1,我们进行把原来的File改为了io.readWriteCloser接口,这样就解决了3个问题
    • 第一个就是我们现在传入的是接口,而非结构体,所以我们就相当于对依赖进行了结偶,只要实现了io.readWriteCloser 这个接口的都可以当作参数进行传入
    • 第二个就是我们现在是没有说限定一定是本地文件,可以是网络数据
    • 第三个就是我们现在是io.ReadWriteCloser,这个接口的方法就比File这个struct实现的接口要少很多(File实现接口数量:23, io.ReadWriteCloser实现接口数量:3)
    • 最后我们还可以进行一个优化,也就是下面的Save_2是不用关心read和close的,所以我们可以用io.Writer对io.ReadWriteCloser进行替换

5. 依赖倒置原则(DIP)

5.1 什么是依赖倒置原则:

  • 程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了业务逻辑与实现模块间的耦合

5.2 代码示例一

  • 这里可以看到 PushService 方法就相当于我们的业务层,他只依赖 UserClient 这个interface,而不直接依赖下面的Huawei,Xiaomi,Apple
  • 可能有人会问这里这样不直接依赖有什么好处?
    • 如果我们要再增加机型会比较好增加,而是只需要增加相应UserClient的实现即可,不用去关心push service这块的业务逻辑
package service

import (
    "fmt"
    "testing"
)

type UserClient interface {
    PushMsg()
}

type Xiaomi struct{}

func (m Xiaomi) PushMsg() {
    fmt.Println("push msg to Xiaomi")
}

type Apple struct{}

func (m Apple) PushMsg() {
    fmt.Println("push msg to Apple")
}

func PushService(client UserClient) {
    // do something
    client.PushMsg()
    // do something eg: record logger
}

func TestSolid(t *testing.T) {
    clients := []UserClient{
        Xiaomi{}, Apple{},
    }
    for _, client := range clients {
        PushService(client)
    }
}

6. 参考引用