继承 - 声明式组合

189 阅读3分钟

背景

一般在扩展时,我们有两大法宝:组合和继承。这是两个老生常谈的东西,但是在两者使用的边界上往往让人头晕,通常我们使用以下方式区分:

  • 组合:A has B
  • 继承:A is B

但这种分类往往让人混淆,没有具体的边界感,像在玩文字游戏。

在实际使用过程中,我们想构建一个易于扩展的系统,究竟应该怎样使用这两大利器呢?不如我们换个方式理解。

什么是声明式组合?

首先,我们回想一下我们的声明语句:

const a = 2;

当一个变量是你已知的,当你定义出来后,我们认为你“声明”了一个变量a,同时声明了语句a = 2;

相同的,当一个类的属性是你已知的,当你定义出来后,我们也可以认为你声明了一个类的属性;

而当你声明了多个类的属性后,我们是不是可以认为你对你声明的属性做了一次组合

因此,当你洋洋洒洒写完一个类的定义后,其实本质上是完成了一次声明式组合。

例如:类A本质上是属性a和属性b组合的结果

class A {
    public a = 2;
    public b: number;
}

什么是非声明式组合?

顾名思义,即我们无法提前声明的那部分内容,对于一个系统来说,这部分内容往往是需要扩展的内容,无法提前预知,也往往可有可无。

例如:A.c = 2 是一次非声明式组合,因为c是不存于与A上的,是在使用时添加的,这个可以代入插件理解一下,unity使用的ecs框架应该就是大量使用非声明式组合的模式,可以简单理解为非声明式组合发生在运行时

class A {
    public a = 2;
}

A.b = 2

组合和继承 与 声明式组合的关系?

对于继承来说,往往使用的是声明式组合的方式

例如:A继承B,我们是在类B的基础上,继续声明式组合了属性a

class B{
    public b = 3;
}
class A extends B{
    public a = 2;
}

对于组合来说,往往使用的是非声明式组合的方式

例如:pluginB是在运行时被添加到实例中的,是预先无法声明的

class A {
    addPlugin(){}
}

const pluginB = A.addPlugin()

当然也有两种结合的方式

例如:A声明式组合了b这个属性,但没有声明式组合它的值,我们可以认为属性b是声明式组合的结果,它的值是非声明式组合的结果

class A {
    b: B
}

A.b = B

利弊

声明式组合:

  • 优势:属性与类直接关联,实例化时即可获得,使用更方便,比如可以直接通过 A.a 拿到对应的属性
  • 劣势:需要提前声明,也就是说需要知道这个类包含的所有内容,而这个往往在设计类时是无法完全预知的,因此扩展性差,扩展时要么修改原类,要么使用继承继续声明式组合,也就会出现多重继承相关的问题

非声明式组合

  • 优势:扩展性好,以插件为例,只要满足定义好接口规范,可以添加任意想扩展的内容
  • 劣势:未提前声明,导致需要运行时添加,如果有多个实例,则每个实例创建后都需要添加,使用更复杂一些,且读取属性也相对复杂,以插件为例,A.getPlugin(B).b会比A.b使用嵌套更深

为何使用声明式组合理解?

声明式组合是更原子化的操作,以属性为粒度,能更好确定哪些属性需要声明,哪些属性需要扩展,以及扩展时是继续使用声明式还是改用非声明式,可以更好理解组合与继承,对类的设计会有很好的帮助。