【程式设计】TypeScript 实作面向协定程式设计可行吗?
面向协定程式设计(Protocol Oriented Programming, POP)和面向对象设计(Object Oriented Programming, OOP)其实都是在经历一个抽象化的过程,将实作的细节先隐藏起来,透过语意来统筹管理规模日渐膨胀,复杂度不断指数提高的专案内容。因此,由语意上的意图来理解这两者的区别可能会更容易一些。
其实这是型态(Shape)与类型(Type)的区别
自从 Swift 语言成为 iOS/macOS 生态系内主流开发语言之一后,面向协定程式设计(Protocol Oriented Programming, POP)就一直不断被拿来跟大家耳熟的面向对象设计(Object Oriented Programming, OOP)比较。孰优孰劣这件事情已经被广泛地讨论过了,所以阿柴就不再赘述。倒是一直以来,阿柴觉得无论是 POP 或是 OOP,这两种设计模式其实并非冲突,彼此互斥的存在。身为开发者可以扪心自问,时日至今有任何人敢说经手过的专案不是同时采用 POP 和 OOP 并行吗? 事实上,说穿了其实也并不奇怪。因为 POP 定义的主要是对象的型态(Shape),而 OOP 描述的主要是对象的类型(Type)。
所以对象的型态与类型说的是什么样的一件事
单就词义的本身,型态与类型确实是很近似的两个概念。所以在更深入的说明之前,还是来先定义一下这两个字词意思。
- 型态:事物的面貌,也就是事物所能表现的动静态形式。
- 类型:事物的类,也就是有着共同特性的事物,可以归类为同一类型。
所以回到对象模型设计时,POP 和 OOP 其实都是在经历一个抽象化的过程。将实作的细节先隐藏起来,透过语意来统筹管理规模日渐膨胀,复杂度不断指数提高的专案内容。因此,由语意上的意图来理解这两者的区别可能会更容易一些。 POP 处理的是偏向“有嘴巴能够发出声音”,“有眼睛能够看东西”,或是“有脑袋能够思考”与此相当层面的语意规格。而 OOP 是在前述语意层面聚合后,进行归纳上的分类。例如将“有嘴巴能够发出声音”,“有眼睛能够看东西”,和“有脑袋能够思考”三个形态组合在一起,归纳得出灵长类这个类型。
介面(Interfaces)和抽象类(Abstract Classes)正好是型态与类型关系的实作
语意说得再多,终究是属于抽象层面的思考。将概念实际在代码体现时,到底是怎么样的一个结果。我们可以透过 TypeScript 的介面和抽象类,试着仿效 Swift 的 CustomStringConvertible Protocol,实际来感受实作 POP + OOP 设计模式。
介面定义对象的型态
首先,透过关键字 interface 来定义介面,并且命名为 ICustomStringConvertible
和 ICustomDebugStringPrintable
。相当于 Swift 的协定宣吿(Protocol Declaration)。
interface ICustomStringConvertible {
description: string
}
interface ICustomDebugStringConvertible {
debugDescription: string
}
上述的宣告,主要是告知了编译器将会有两个新增的介面。透过类型检查确保支持该介面的类,必然分别会拥有 description 和 debugDescription 两个属性。在这个介面约定的基础上,我们可以像这样设计 print() 功能,进而输出该有的类描述。
function print(arg: ICustomStringConvertible): void {
console.log(arg.description);
}
function debugPrint(arg: ICustomDebugStringConvertible): void {
console.log(arg.debugDescription);
}
抽象类决定对象的类型
接着下一步,设计一个命名为 AnyObject 的抽象类,并且支持 ICustomStringConvertible
和 ICustomDebugStringConvertible
介面。这样一来,意谓着任何继承 AnyObject 的类,都必然会有,而且同时具备实作细节的 description 和 debugDescription 属性。
abstract class AnyObject implements ICustomStringConvertible, ICustomDebugStringConvertible {
abstract description: string;
abstract debugDescription: string;
}
举例来说,AnObject 类就是一个继承 AnyObject 抽象类的实作例子。
class AnObject extends AnyObject {
description: string = `description for ${ this.constructor.name }`
debugDescription: string = `debugDescription for ${ this.constructor.name }`
}
而实际的执行结果就会如下。也就是基于抽象类 AnyObject 继承类所产生的实例,都能透过 print() 和 debugPrint() 列印出实作细节中所提供的对应内容。
const anObject = new AnObject()
print(anObject)
debugPrint(anObject)
---
[LOG]: "description for AnObject"
[LOG]: "debugDescription for AnObject"
最后的几点探讨
面向协定程式设计或许不是一个理想的修辞方式来描述 TypeScript 的介面。然而无论如何,实际上 TypeScript 和 Swift 确实在抽象化的设计模式上,有些相似而且彼此可以呼应借鉴的地方。所以对于 Swift 程式开发者来说,学习 TypeScript 或许会相对容易上手一些。特别是代码的样式,以及代码的构成经验上,都有些可以互换且对应的地方。更不用说,在个别程式语言上实作抽象化,更是有助于加深对面向对象程式设计的理解。