编程范式粗讲
编程范式(Programming paradigm)指计算机编程的基本风格或典型模式。
编程范型提供了(同时决定了)程序员对程序执行的看法。例如,在面向对象编程中,程序员认为程序是一系列相互作用的对象,而在函数式编程中一个程序会被看作是一个无状态的函数计算的序列。
着眼于解决问题的不同方式,编程范式现存许多种,其中如:面向过程、面向对象、函数式编程等范式,我们对此比较熟悉,他们也经常出现在我们的视野中。为了进一步加深对编程范式的认识,这里介绍几种常用的编程范式。
面向过程
面向过程编程,也被称之为命令式编程,是一种最原始,也是我们最熟悉,日常工作中使用较多的一种编程范式。从本质上讲,它是「冯.诺伊曼机」运行机制的抽象,它的编程思维方式源于计算机指令的顺序排列。
面向过程的核心是将解决问题的步骤分析出来,用函数将这些步骤实现,然后再被依次调用。
比如一个经典的例子:
把大象放在冰箱分为几步?
第一步:把冰箱打开
第二步:把大象装进去
第三步:把冰箱门关上
得益于面向过程的直观性,以及最接近于程序的实际运行,到现在,它仍然被大范围的使用。
但是它不适合某类问题的解决,例如那些非结构化的具有复杂算法的问题。它必须对一个算法加以详尽的说明,并且其中还要包括执行这些指令或语句的顺序。实际上,给那些非结构化的具有复杂算法的问题给出详尽的算法是极其困难的;并且它强调顺序至关重要,而这并不适合所有问题。
面向对象
面向对象编程(OOP:Object Oriented Programming)把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数,它最早由 Alan Kay 在 1966 年或 1967 年在研究生期间提出。
与面向过程编程不同,在面向过程编程中,数据和处理数据的函数彼此独立,我们需要先将数据处理成函数能接受的格式,然后调用相关函数。而在在面向对象中,数据和处理数据的函数都在一个类中,通过初始化实例传递数据。
现如今,当谈及面向对象时,下意识的就会联想出它的三个特性:封装、继承与多态。
class Animal {
constructor(
public name: string
) {}
eat() {
console.log('eat food')
}
}
class Cat extends Animal {
constructor() {
super('Cat')
}
}
class Bird extends Animal {
constructor() {
super('Dog')
}
}
复制代码
网上也有一张的图,形象的解释了面向对象中的一些常见概念:
用面向对象的方式来解释「把大象关进冰箱」:
冰箱.开门()
冰箱.放入(大象)
冰箱.关门()
面向切面
面向切面编程(AOP:Aspect Oriented Program)是对面向对象编程的一个补充。我们知道在面向对象编程时,无法做到重复使用与主业务无关的某些方法。比如 A、B 两个类,如果都要使用日志功能,按照面向对象的设计原则,你必须在每个类里面都写上这样一个方法,尽管他们几乎一摸一样。当然你也可以选择将这样一段代码放到一个独立的类里,然后再在A、B类中调用,但是这样就等于形成了一个强耦合的关系,它的改变会影响到这两个类。面向切面编程,正是解决的这样一个问题:把和主业务无关的事情,放到代码外面去做。
比如在前端,实现一个代码埋单时:
如果不使用 AOP 你的代码可能是这样:
class Test {
add(paras: Params) {
// 埋点代码
// 其他代码
}
}
复制代码
但是这样会造成耦合,且不能复用。
使用 AOP 时:
function log(): MethodDecorator {
return (target, key, descriptor) => {
// 在这里你可以加上你的埋点接口
// 或者做一些其他事情
return descriptor;
};
}
class Test {
@log()
add(paras: Params) {
//
}
}
复制代码
再比如后端接口的鉴权,你可能会在中间件中处理:
function guardsMiddle(req, res, next) {
// 这里可能会有各种判断
// 判断 url 是否是指定 url,然后再判断是否有权限
next()
}
复制代码
或者你会在某个具体的 controller/services 中处理:
class SomeController {
getUserInfo() {
// 现判断是否有对应的权限
// 再做具体的处理
}
}
复制代码
在 AOP 中,则可能是这样:
@Controller()
class SomeController {
@Get('userInfo')
@Roles('admin')
getUserInfo() {
//
}
}
复制代码
面向接口
同面向切面编程一样,面向接口也是对面向对象的一种补充。它规定了实现本接口的类必须拥有的一组规则。比如:
interface Animal {
eat(): void;
}
class Cat implements Animal {
eat() {}
}
class Dog implements Animal {
eat() {}
}
复制代码
在开发 vscode 插件时,会使用很多从 vscode 中暴露出来的接口:
class StatusBar implements vscode.Disposable {
dispose() {
// xxx
}
}
class TargetTreeProvider implements vscode.TreeDataProvider<{}> {
getChildren() {
// xxx
}
}
复制代码
你可能会有疑问,我也可以使用抽象类实现,那么区别是什么?
abstract class Animal {
abstract eat(): void;
}
class Cat extends Animal {
eat() {}
}
复制代码
在文中这两个例子里,使用接口或是抽象类,是没有多少区别的,唯一的区别仅仅是在 TypeScript 编译成 JavaScript 后,interface 不存在了(不会有额外使用额外空间)。除此之外,抽象类可以包含程序的实现细节,即是可以实现代码的复用。
abstract class Animal {
constructor(private name: string) {}
abstract eat(): void;
printName() {
console.log(this.name);
}
}
class Cat extends Animal {
constructor() {
super('cat');
}
eat() {}
}
class Dog extends Animal {
constructor() {
super('dog');
}
eat() {}
}
复制代码
另外,接口和抽象类的另外一个区别在于:抽象类和它的子类之间应该是一般和特殊的关系,而接口仅仅是类应该实现的一组规则。
总结
如今,编程范式现存许多种:
每个编程范式在自己所注重的场景里发挥着举足轻重的作用。比如 OOP 注重的是数据和行为的打包封装以及程序的接口和实现的解耦;而 FP 最大的特点是纯函数的无状态性质;面向过程则更贴近于实际工程中硬件的运行方式。
每个范式都有它的「灵魂」,只有在实际使用时,才能理解。
在实际项目中,更多的时候,我们是使用的多范式编程,正如范·罗伊信仰的一样:解决一个编程问题,需要选择正确的概念;解决多个问题,则需要组合分属不同部分的多个概念。
参考
更多文章,请关注公众号: