
2.5.1 接口和继承
需求 “诗酒趁年华”——我们网站同时支持两种特定商品:红酒和图书。红酒一定要有一个年份的字段,而图书一定要有一个书号(ISBN)字段。
这是常见的需求,两种商品有自己的特殊性,但是大多数操作和数据属性又是相同的。如果独立定义两种类型,很多Schema定义还有前后端实现都会有重复的地方,如何减少重复,增加代码的重用呢?熟悉面向对象设计的读者估计马上就会想到类似图2-3的结构。

图2-3 红酒和图书继承商品
在图2-3中,提供了两个具体的实现类Wine和Book,它们有一个共同的父类Product。父类Product可以定义为一个抽象类,承载Wine和Book都有的字段id和name。
在GraphQL中,一般用接口interface来表达Product这种抽象类。先看接口interface如何定义:

在GraphQL中接口是一个抽象数据类型,不可以直接为抽象数据类型创建一个实例。只能为两个具体类Wine和Book来创建实例。请注意下面implements关键字的使用:


和很多面向对象语言不同的是,GraphQL的子类必须重载接口里所有的字段。也就是要把接口里的字段都在子类里抄写一遍,这个设计来自GraphQL的官方标准,看起来有些累赘,但是也带来了一些好处,或者说需要注意的地方。
子类中的字段的类型可以和接口中的同名字段的类型不同,但必须是接口中同名字段类型的子类或非空类型。
可以在interface Product中增加字段:

在Product的子类Wine中,可以覆盖relatedProduct这个字段的类型:

有了多态和继承之后,现在就来享受它所带来的好处,只需要定义一套操作,就可以同时覆盖这两种商品了:

这两个查询操作一个返回商品列表,一个返回单一商品,结果中既可以有书,也可以有红酒。
这样的做法还有另外一个好处,就是如果以后要加入新的商品类型,比如电商网站突然开始卖茶叶了,那么这两个查询操作不用进行任何修改就可支持新加入的茶叶商品类型。
尽管多态和继承是面向对象程序设计最重要的特征,但是在项目中过度使用复杂的继承结构会给项目日后维护带来很多的困难。而且随着项目新功能的不断积累,原本非常简单的继承关系也会逐渐变得复杂。所以,要尽量少引入多态和继承,即便是下面所引入的“联合”例子——“书和新朋友”,虽然现在看起来人畜无害,但不能保证以后不会成长为一个大怪兽。