我们总会听见如下词语:

面向过程编程(opp)
面向对象编程(oop)
面向切面编程(aop)

上面几个词语,都是编程范式。

编程范式是指:编程时所秉承的思想和风格,不同的编程语言对各大编程范式会有不同程度的支持。

不同的编程范式各自会有自己的优点,它们适用在各种不同的情况下:面向过程性能很高,面向对象比较易于管理和维护,面向切面使软件变得更灵活。

在理解编程范式之前,我们一定要知道:

新的编程范式,并不一定完全各方面都优于旧的编程范式,它们只是在某一特定领域或特殊场景下有着独到的优势。

编程范式只有适合不适合项目特性,没有绝对的好坏。

OPP和OOP

OPP是较贴近计算机执行的编程范式。

它主要关注“怎么做”,即完成任务的具体细节。

OOP则是比较贴近人类思维的一种编程范式,它主要关注的是“谁来做”,也就是完成任务的对象,而非细节。

面向过程会将一个任务拆分成各个小的步骤,然后用一个个函数来实现每一个小步骤,再在主函数中依次调用这些函数,以此来完成任务。

面向对象则是从一个任务中抽取出各个角色(对象),这些角色分别提供了一些能力,然后,这些拥有能力的对象互相配合,完成任务,这样的好处是:各个对象可以通过不同的组合,完成不同的任务,这样可以提升代码重用,对代码做好分类和分级

举例对比

比如完成吃饭这个任务。

面向过程的写法,需要封装一个eat()函数:

  • 如果是狗吃饭,则eat(狗,饭)
  • 如果是人吃肉,则eat(人,肉)

如上,eat是人和狗共用的吃饭能力。

那么,如果狗吃饭要嚼10下,人吃肉要嚼20下。

这时候,我们就需要在eat函数中,判断传入的第一个参数是人还是狗,然后再执行嚼的次数。

那如果之后要处理猫吃鱼、老虎吃人、猫走路、老虎走路…

eat函数中就会存在大量的if…else的判断,这段代码,无疑是很恶心的,另外,因为存在走路的功能,还需要定义一个恶心的walk函数,这不易于维护、升级、扩展。

但如果是面向对象,如何来解决这个问题呢?

这里涉及到“抽象”这个思维。

我们可以发现,狗、猫、人、老虎,它们都有吃的能力,所以,我们需要抽象出它们的祖先:会吃东西的动物

然后,再抽象出狗、猫、人、老虎,继承会出东西的动物,以此来决定它们都具备吃东西的能力。

接着,解决上面的问题。

狗、猫、人、老虎各自有自己的eat方法,也各自有自己的walk方法。

当我们想要进行狗吃肉,那就“狗->eat(肉)”,这样,我们从面向过程维护eat的焦点,转移到了面向对象维护角色的焦点上来。我们只需要维护好不同的角色(类)就好了,并且狗的eat不会影响到猫的eat,猫的eat也不会影响到人的eat。

所以,oop思想非常贴近软件工程高内聚的思想:自己管好自己的东西,自己做好自己的事情。

上面,主要是从思想上讲述了opp和oop之间的差异,如果深入oop,不同的语言也会提供不同程度的支持,比如继承、多态、封装这些,比较深入的内容,不是本文讨论的主题。

大多数支持面向对象的语言,同时也支持面向过程,不论是java、php,还是javascript,它们都还无法完全面向对象,因为面向过程是必然的,面向过程代表着必要的程序流程,调动对象进行组合或对象内部能力的实现,都一定会存在“过程”,它最终还是需要通过拆分步骤来指导最具体的执行细节。

在此,我们也能得到一些感悟,许多事情并非完全非黑即白,非oop就必然是opp,特别是思想层面的东西,它们呈现出互相结合的形态,从opp到oop,这是一个思想进步的过程,也是人们遇到“管理问题”、“可持续发展问题”后对“管理”和“发展”提出的讨论和求证。

AOP

面向切面编程,AOP也是一种编程范式,是对oop的延续。

即:基于oop延伸出来的编程思想,它进一步降低项目合作、维护、扩展、多人协作的成本,提高程序的内聚性,降低程序的耦合度。

那么,AOP如何体现?

这里,可以联想一下laravel的中间件、javaweb的拦截器、vue的Decorator…它们都是AOP思想的实践。

也可以联想一下,装饰器模式、代理模式,它们也是基于AOP思想的设计模式。

这里以laravel的中间件(middleware)作为详细例子:

middleware所运行的地方,就是一个切面,这个切面上承路由,下接controller。

想象一下,laravel从接收到请求一路运行下来,到中间件这里,就好像是被切开了一个平整面,这就是面向切面。

而AOP思想,指导我们通过找到平整切面的形式,插入新的代码,使新插入的代码对切面上下原有流程的伤害降到最低。

举更具体的例子:

我们拿laravel中间件做什么?

权限、日志、请求过滤、请求频率限制、csrf过滤……

我们知道,中间件对于controller的业务逻辑,不会有任何伤害。

如果没有这个切面,我们想要记录请求日志,可能需要在每个controller的具体方法中写日志记录的代码,或者调用日志记录的函数、方法。

这会使一段记录日志的代码,或调用记录日志的调用语句出现在许多controller中,这与controller原本要关注的逻辑无关,使controller职责不单一,提高维护成本。

当然,我们可能会写一个父类,让许多controller来继承这个父类,然后统一在父类的__construct方法中记录日志,以此来解决耦合问题。

但实际上,这不父类的construct方法,不正是一个切面吗?它在原有流程中截取了一个切面,在切面中植入代码,以达到承上启下的作用,并且不对上下文产生伤害。

从这个例子中,我们也能得出另外一个思考:AOP指导我们寻找切面,但找到合适的切面,也尤为重要。就像上文,父类构造函数的切面和中间件的切面比起来,显然中间件这个切面更利于维护,你可以灵活选择中间件,但你无法灵活选择父类,因为决定你的controller继承什么父类的,不是切面中的代码,而是controller本身处理什么逻辑。

另外,TP常会提到的钩子和行为,也是AOP思想的一种实践。

aop的思想,是对oop的补充。

而oop又是对opp的升级。

许多项目,opp、oop、aop是同时存在的,它们是编程范式,是一种指导编程的思想,并非不能互相配合。