探讨 Go语言之面向对象特性:多态 设计思路和实际编程

1,822 阅读4分钟

开篇先抛出问题,Go语言算不算是一门面向对象编程语言?

要回答这个问题,我查阅了许多的外文资料,发现对于面向对象编程语言,并不存在一个严格的定义。

但从实际的角度出发,只要一门语言拥有类,对象的概念,以及提供了对应的语法,就可以用来实现面向对象编程了,所以从这个角度来看,Go语言是可以被认为是一门面向对象编程语言的。

那么抛出第二个问题,Go语言有没有实现面向对象的四大特性? 封装,抽象,继承,多态?

这个问题我不准备全部回答,但关于「多态」,Go语言很明确地提供了非常灵活的语法支持。

本篇文章通过Go的多态实现思路,来探讨如何理解「多态」,以及如何真正地在项目中使用,而不是随处可见的「小狗,动物,吠」这种虽然浅显易懂但不够实际落地的例子。

一,多态是什么?

多态的定义比较抽象,这里我贴一下百度词条的

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。

还是用具体的例子来理解吧,Go的fmt包相信大家都用过,其中fmt.Printf()更是常用的函数,我们先跳到fmt包里,去看一眼这个方法

// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

可见到,Printf基于Fprintf实现,在Printf中,显式调用了Fprintf(), 并传入了 os.Stdout

我们看一下Fprintf对os.Stdout的使用方式,可见到Fprintf认为os.Stdout实现了io.Writer,所以认为可以调用io.Writer身上应该具有的Write()方法。

这里有两个比较重要的信息,一个是Go语言的接口语法,一个是Duck Typing。

Go语言的接口语法我不深入介绍,就贴一下源码

type Writer interface {
	Write(p []byte) (n int, err error)
}

可见Writer接口只具有一个方法,Write(..),实现它很简单,因为接口是面对功能,不面对实现,所以你完全可以写一个类似下面的例子来实现Write()

func Write(p []byte) (n int, err error) {
    return len(p), nil
}

好了,这就一个常见的接口语法,只不过在Go语言中,实现一个接口,并不用显式说明,什么是显式说明,举个例子

比如在PHP中,如果某个类实现了一个接口,必须显式做出如下声明

class UriResolver implements UriResolverInterface {...}

但是在Go语言中,是采用Duck Typing来实现接口实现,什么是Duck Typing?

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

具体到上面Fprintf的例子,也就是说:只要传进来的参数对象实现了Write()方法,就可以认为它是一个io.Writer。

这对Go实现多态带来了极大的方便,下面我们实现一个简单的例子,写一个Sprintf()

func Sprintf(format string, args ...interface{}) string {
    var buf bytes.Buffer
    Fprintf(&buf, format, args...)
    return buf.String()
}

在Sprintf中,我们显示调用Fprintf,并且传入的io.Writer实现者是一个bytes.Buffer对象,这个对象不出意外,也实现了Write()。

所以对于多态,我个人的片面理解就是,对于一个通俗功能,有着「统一的调用方式」和「多种形态的实现」。

比如对于图片的存储功能,有着本地存,云端存等不同形态的实现,但它们被调用的方式统一是 local.Save()和Cloud.Save()

二,多态的意义

从上面的例子可以解读出两个现实意义:

1,因为有了Fprintf,在屏幕上打印(Stdout)和字符串生成(bytes.Buffer)可以不用分别实现「打印」这个功能,不用写StdoutPrintf和BufferPrintf,这就有利于代码复用;

2,以后如果想把数据发送到某个文件里,就可以给Fprintf传入一个file.file之类的对象,只要file.file实现了Write(),这就有利于代码拓展;

三, 多态的实现方式

其实在第一点的例子里已经可以看到,上述的接口(php显式声明),Duck Typing(隐式实现)都是多态的实现方式。

如果你是Java,PHP开发,你也可以通过「继承」->「重写方法」来实现多态。

比如 local和cloud都是继承自storage.

local和cloud都分别重写了storage中的save()方法,那么在就可以在如下的调用中实现多态:

<?php 



 Class Storage {

    public  save(string $pictureUrl) {
        //...
    }
}



Class Local extends Storage {

    public  save(string $pictureUrl) {
    
        $this->saveToLocal($pictureUrl);

    }
}



Class Cloud extends Storage {
    
    public  save(string $pictureUrl) {
        $this->saveToOss($pictureUrl);
    }
}



function savePicture(Storage $saver, string $url) {
    $saver->save($url);
}



$url = '../xxx.jpg';

$local = new Local();

$cloud = new Cloud();



savePicture($local, $url);

savePicture($cloud, $url);

到此,本篇就结束了。

参考