编程思想讨论:面向接口 vs 面向对象

19 阅读5分钟

从复杂度说开去

“软件设计的复杂度在于如何降低软件的复杂度。”

这句话不是什么名人名言,是我说的。

通常来说,软件设计的难点有三:

  1. 确定需求。其中主要的难点是开发人员与提需求的人之间的沟通问题,即如何挖掘出对方的真实需求,并准确表述需求。

  2. 高性能。不用多解释。

  3. **抽象与建模。**软件的本质就是对现实的抽象。软件设计就是把需求中需要实现的功能抽象为一个业务模型,并用代码实现这个模型。但如何抽象出一个好的模型来,是非常难的事情。因为这个模型需要:

    1. 能准确表达业务形态
    2. 灵活,能快速迭代

本文讨论其中难点3。

既然前文说“软件的本质就是对现实的抽象”,那么如何进行抽象呢?

一个很自然的想法就是,把现实中所有的“实体”抽象为代码中的“对象”。看到这两个词你的脑海中应该自动冒出了这个名词:“面向对象编程”。

面向对象

面向对象的思想就是把一切都抽象为有属性和方法的“类”以及各“类”之间的关系。

例如,我们想使用面向对象的思想表示“鸭子”这个概念,就会先抽象出一个名叫“鸭子”的类,这个类的父类是“禽类”。鸭子类又有羽毛、眼睛、血液等属性,而这些属性各自也都是“类”,也都有自己的属性...

而它拥有哪些方法呢: 比如“叫”, "游泳", “走路”, “下蛋” 等。

最终,将现实中的鸭子所拥有的全部属性和功能都抽象出来,就实现了一只鸭子。

面向对象中,在抽象各类之间的关系时,有一类关系就叫作继承。

“鸭子”的父类是“禽类”, 那么鸭子将继承禽类所有的属性和方法,同时将也有自己的特有属性和方法,也会将父类的一些方法进行“重载”。

也就说,实现了属性和方法的复用。

到这里,看起来一切都很美好。但在实践中,就不是那个味道了。

面向对象的局限

设计这些xxx编程思想的本质是为了应对“软件复杂度”。因为如果只是一款小品级软件,可能不需要任何思想,甚至一两个函数就写完了。

而面向对象思想所针对的软件复杂度,更多的是那种多层级、实体之间关系复杂的业务场景。传统的大型软件多数就是这样的。

但到了现在的互联网时代,时代变了...

互联网时代的服务端系统往往是什么样的呢?业务功能多、迭代快,模块之间的关系比较扁平。这时,面向对象的思想还能不能用呢? 能用。 但这时的面向对象已经不能明显降低软件复杂度了。相反的,由于它的代码冗余,还带来了新的、额外的软件复杂度。

因此,这时就需要一种新的能降低软件复杂度的编程思想。“面向接口思想”

面向接口

面向接口的思想并不是什么新产物。Java里也有接口。但很多人第一次接触面向接口的思想应该是在初学Golang的时候。后来的新秀语言Rust也跟着使用了面向接口思想。

但是,个人认为,并不是Golang带火了面向接口,而是行业发展的趋势使得面向接口更具优势,从而导致更多新语言开始更优先支持面向接口的思想。

那么什么是面向接口呢? 如果说面向对象是对属性和方法的复用以及对类的组合,那么面向接口就是只对方法进行复用和组合。它摒弃了类的概念和继承的概念,因为在现代服务器系统里,真正需要实现复杂的类与类之间的层级关系,并且通过抽象类与类之间关系能明显降低软件复杂度的场景已经不多了。

用一个比较互联网黑话的现实例子来解释,也可以称之为“能力复用”。它需要关心的是这个人能给我的团队提供哪些能力,而不是这人叫什么、是哪个大学的、他爸是谁。

我们从Golang标准库的一些设计里也可以体会出这种设计的灵活性。

例如,Golang提供了接口 io.Writer​和io.Reader

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

非常简单的两个接口,所有需要实现向某个数据源进行字节的读写操作的场景,都只需要实现这两个接口即可,无论这个数据源是文本文件、Socket、程序中的buffer还是其它自定义的结构。

这时,代码里无需在意具体这个接口将如何被实现,它只要对这个接口中的方法进行操作即可。

总结

面向接口与面向对象思想,不存在孰强孰弱。两者都是为了解决软件复杂度而生,因此,在适合的场景下,选择最能解决自己软件复杂度的用法是最好的。