• Go中的面向接口编程
  • 发布于 2个月前
  • 182 热度
    0 评论
  • LoveC
  • 1 粉丝 35 篇博客
  •   
说起面向对象(OOP),很多人都听说过封装、继承、多态这些特性,从本质上说面向对象只是一种软件编程思想。但由此衍生出面向对象语言这个概念,其中Java是最典型的代表,是一种完全面向对象的语言,表现在语言层面就有类和对象的设计。严格来说Go不是一门完全面向对象的语言,但是在某种层面上实现了面向对象的部分特性,毕竟任何软件工程的主要目标都是为了实现重用性、灵活性和扩展性,Go也不例外。

举个例子:假设你需要把一个大象放到冰箱里面,需要几步?
第一步:打开冰箱
第二步:把大象放进冰箱
第三步:关上冰箱

这3个步骤用面向过程的方式去实现可能就是3个函数,比如openFridge、placeElephant、closeFridge,我们只需要依次调用即可。但是从面向对象的思维来看,冰箱作为一个对象,它应该有2个函数:open、close,而大象作为一个对象应该有一个函数:walk,我们只需要组合这2个对象的函数就完成这些步骤。

1.Go的面向对象
Go里面没有类这个概念,只有结构体struct,结构体可以有属性,如:
type Fridge struct {
    Name   string
    Status string
}
虽然结构体里面并不能定义函数,但是我们可以给这个结构体定义方法,通过这种形式:
func (i Fridge) Open()  {
    // open
}

func (i Fridge) Close()  {
    // close
}
通过这种方式我们认为Open和Close是属于Fridge这个结构体的方法,这些加在一起可以比作是面向对象语言里面类、类变量、类方法的概念。非常简单易懂,没有其它面向对象语言里面比如静态类、静态属性等等其它特性。

2.函数还是方法?
函数英文是function,方法英文是method,很多人对这2个词概念没有区分,往往都是混着叫,函数方法不分,虽然本质上都是一段代码块,但是在不同的环境下还是略有不同。严格来说,方法是面向对象语言里面的概念,它必须属于一个对象,比如Java是一门完全面向对象的语言,所以在Java里面只有方法,没有函数。而函数则是很传统的概念,比如在C语言里面函数是一等公民,所以C里面只有函数。

回到Go里面,其实也应该区分一下,一般我们说函数,指的是这种不属于任何结构体的函数,可以直接通过包名调用:
func Open()  {
    // open
}
而方法则是属于某个结构体的,不能直接调,你得先New一个对象出来,然后再调用这个对象的方法。很多语言,比如PHP,既有函数,也有方法,相对来说更加灵活,但是最好还是区分一下,虽然意思大家都懂。

3.Go的接口
这里说的接口不是指API接口,而是面向对象里面的接口,也叫interface,如上所说Go虽然不是一个完全面向对象的语言,但是依然提供了接口,虽然Go的接口和其它语言接口不太一样。Go的接口被称为是鸭子类型Duck Type,当你看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。在鸭子类型中,关注点在于对象的行为,能作什么,而不是关注对象所属的类型。

在很多面向对象的语言里面,如果你要实现一个接口你就必须实现其所有定义的抽象方法,这是强制要求,而Go则不是这样,Go甚至连implement这个关键字都没有,你不能“实现”接口!
type Duck interface {
    Walk()
    Swim()
}
但只要一个结构体实现了接口定义的所有方法,我们就认为实现了这个接口
type Dog struct {
}

func (i Dog) Walk()  {
}

func (i Dog) Swim()  {
}

4.为什么需要接口?
其实这个问题也困扰我很久,很多时候我们在写业务代码几乎用不到接口,大多数都是一些方法和函数的调用,但是在看一些底层库源码的时候却发现处处是接口。到底什么时候该用接口? 这是一个非常值得思考的问题。因为接口这种设计,本质上还是为了灵活性和扩展性,什么时候去用还是得看具体情况,比如配置文件库,往往需要支持json、yaml、ini等多种格式,而一个日志库需要支持console、file、api各种输出方法。这时候就需要利用接口去灵活设计结构,可以实现非常容易的扩展更多类型的目的。

而过多的使用interface也会导致代码过于冗余,阅读难度增加,变相增加了后续维护成本,实际工作中,公司开发人员水平层次不齐,最简单直白的代码反而更容易被其它人接手维护。

在我看来,在业务开发代码中,接口最实际的意义其实在于方便写单测,在依赖注入这种实现模式配合下,可以分割不同层之间的依赖,单独对每一层做单测,从而提高代码质量。

比如在开发中,一个模块依赖另一个模块去实现功能,如果不使用接口做隔离,就很难单独的去做测试:
type ArticleService struct{}

func NewArticleService() ArticleService {
    return ArticleService{}
}

func (i *ArticleService) GetArticles() ([]byte, error) {
    articles, err := NewApi().GetArticles()
    if err != nil {
        return nil, err
    }
    return articles, err
}
在这段代码里面,ArticleService是依赖Api去获取结果的,他们之间是完全依赖耦合的,这样写就很难去单独测试ArticleService的逻辑。
type Api struct{}
func NewApi() Api {
    return Api{}
}

func (i Api) GetArticles() ([]byte, error) {
    get, err := http.Get("https://www.duidaima.com")
    if err != nil {
        return nil, err
    }
    defer get.Body.Close()
    all, err := ioutil.ReadAll(get.Body)
    if err != nil {
        return nil, err
    }
    return all, nil
}
如果用依赖注入加上接口的方式去改造这些代码,可以这么写:
// 定义一个接口
type ApiInterface interface {
    GetArticles() ([]byte, error)
}

type ArticleService struct {
    api ApiInterface
}

func NewArticleService(api ApiInterface) ArticleService {
    return ArticleService{api}
}

func (i *ArticleService) GetArticles() ([]byte, error) {
    articles, err := i.api.GetArticles()
    if err != nil {
        return nil, err
    }
    return articles, err
}
我们定义一个接口,它有一个方法,然后ArticleService依赖这个接口,并且我们在New方法里面通过参数的方式注入这个依赖。在使用的时候区别并不大,我们只需要先初始化Api对象,而且作为参数传入ArticleService内部,然后调用就行了。
func main() {
    articleService := service.NewArticleService(service.NewApi())
    res, err := articleService.GetArticles()
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s\n", res)
}
但是其实际意义也不仅如此,一个是ArticleService依赖的是一个接口,不是一个具体的对象,这就是所谓的“面向接口编程,而不是实现”。另外,我们可以单独针对ArticleService做测试,可以Mock一个Api对象,实现解耦。
type mockApi struct {
}

func (mockApi) GetArticles() ([]byte, error) {
    return []byte(""), nil
}

func TestGetArticles(t *testing.T) {
    service := NewArticleService(mockApi{})
    articles, err := service.GetArticles()

    if err != nil {
        t.Fatal("should be nil")
    }
    if len(articles) > 0 {
        t.Fatal("should be 0")
    }
}
这种写法可以屏蔽外部依赖对测试结果的影响,专注于自身逻辑的测试,这里只是简单的展示这种用法,实际开发中可以使用一些mock库更加方便的测试各种情况。
用户评论