我们为什么要用IOC和AOP
作为一名java开发,对Spring框架是再熟悉不过了。Spring支持的控制反转(Inversion of Control,缩写为IOC)和面向切面编程(Aspect-oriented programming,缩写为AOP),早已成为我们的开发习惯,仿佛Java开发天生就是如此。
人总是会忽略习以为常的东西,所有人都熟悉IOC和AOP,却鲜有人说得清楚到底为什么用IOC和AOP。
技术肯定是为了解决某个问题而诞生的,要弄清楚为什么要使用IOC和AOP,就得先弄清楚不用他们会碰到什么问题。
IOC
我们现在假设回到了没有IOC的时代,用传统的Servlet开发。
传统开发模式的弊端
三层架构是经典的开发模型,我们一般将视图控制、业务逻辑和数据操作分别抽离出来单独形成一个类,这样各个的职责就非常清晰且易于复用和维护,大致代码如下:
1 |
|
1 | public class UserServiceImpl implements UserService{ |
1 | public class UserDaoImpl implements UserDao{ |
上层依赖下层的抽象,代码就分为了三层:
业界普遍按照这种方式组织代码,其核心思想是职责分离。层次越低复用性越高,比如一个DAO对象往往会被多个Service对象使用,一个Service对象往往也被多个Controller对象使用。
条理分明,井然有序。这些被复用的对象就像一个个的组件,供多方使用
虽然这个倒三角看上去非常漂亮,然而我们目前的代码有一个比较大的问题,那就是我们只做了逻辑复用,并没有做到资源复用
上层调用下层时,必然会持有下一层对象的引用,即成员变量。目前我们每一个成员变量都会实例化一个对象,如下图所示:
每一个链路都创建了同样的对象,造成了极大的资源浪费。本应多个Controller复用同一个Service,多个Service复用一个DAO。现在编程了一个Controller创建了多个重复的Service,多个Service又创建了多个重复的DAO,从倒三角编程了正三角。
许多组件只需要实例化一个对象就足够了,创建多个没有任何的意义。针对对象重复创建的问题,我们自然而然想到了单例模式。只要编写类的时候都将其写为单例,这样就避免了资源浪费。但是,引入设计模式必然会带来复杂性,况且还是每一个类都是单例,每一个类都会有相似的代码,其弊端不言而喻。
有人可能会说,我不在意资源浪费,我服务器大内存大没关系,我只求开发便捷痛快不想写额外的代码。
确实,三层架构达到逻辑复用已经很方便了,但是还是会存在一个致命的缺陷,那就是变化的代价太大
假设有10个Controller依赖了UserService,最开始实例化的是UserServiceImpl,后面需要换一个实现类OtherUserServiceImpl,我就得逐个修改那10个Controller,非常麻烦,但是对于更换实现类的需求不会可能太多,没有说服力,那就换一个情况。
之前演示的组件创建过程非常简单,new一下就完了,可很多时候创建一个组件没那么简单,比如DAO对象要依赖这样的一个数据源组件:
1 | public class UserDaoImpl implements UserDao{ |
该数据源组件想要真正生效的时候需要对其进行许多的配置,这个创建和配置过程是非常麻烦的,而且配置可能会随着业务需求的变化而经常更改,这个时候就需要修改每一个依赖该组件的地方,牵一发而动全身,这还只是演示了一个数据源的创建和配置过程,真是开发中可有太多的组件和太多的配置需要设置了,其麻烦程度堪称恐怖。
当然这些都可以引入设计模式来解决,不过这样又绕回去了,设计模式本身也会带来一定的复杂性,这样就进入了死循环,传统开发模式编码复杂,要解决这种复杂又得陷入另一种复杂中,难道没有其他方式吗?当然不是,在讲解决方案之前,我们先来梳理一下目前出现的问题。
- 创建了许多重复的对象,造成大量资源浪费
- 更换实现类需要改动很多的地方
- 创建和配置工作繁杂,给组件调用带来了极大的不便
通过现象来看本质,这些问题出现都是一个原因:组件的调用方式参数了组件的创建和配置工作
其实,调用方只需要关注组件如何调用,至于这个组件是如何创建和配置又与调用方有什么关系呢,就好比我去饭店吃饭,饭菜并不需要我亲自做,饭店自然会做好给我送过来。如果我们在编码时,有一个东西能帮助我们创建和配置好这些组件,我们只需负责调用就好了,这个东西就是容器
容器这个概念我们已经接触过,Tomcat就是一个Servlet容器,他帮我们创建并配置好了Servlet,我们只需要编写业务逻辑即可。试想一下,如果Servlet要我们自己创建,HttpRequest、HttpResponse对象也需要我们自己配置,那代码量就有点恐怖了。
Tomcat是Servlet容器,值负责管理Servlet,我们平常使用的组件则需要另外一中容器来管理,这种容器我们称之为IOC容器。
控制反转和依赖注入
控制反转,是指对象的创建和配置的控制权从调用方转移给了容器。好比在家做菜,菜的味道全部由自己掌控,而去餐馆吃饭,菜的味道由餐馆方掌控,IOC就相当于餐馆的角色
有个IOC容器,我们可以将对象的创建交由容器管理,交由容器管理后的对象称为Bean,调用方不再负责组件的创建,要使用组件时直接获取Bean即可。
1 |
|
调用方只需按照约定声明依赖项,所需要的的Bean就自动配置完毕了,就好像在调用方外部注入了一个依赖项给你使用,所以这种方式称之为依赖注入(Dependency Injection,缩写为DI),就相当于你给钱,就可以点各种菜一样,给你做好送过来。控制反转和依赖注入是一体两面,都是同一种开发模式的表现形式。
IOC轻而易举的解决了我们刚刚总结的问题:
- 对象交由容器管理后,默认是单例的,这就解决了资源浪费的问题。
- 如要更换实现类,只需要更改Bean的声明即可,即可达到无感知更换。
1 | public class UserServiceImpl implements UserService{ |
现在组件的使用和组件的创建与配置完全分离开来,调用方只需调用组件而无需关心其他工作,这极大提高了我们的开发效率,也让整个应用充满了灵活性,拓展性。
这样看来,我们中意IOC不是没有道理的。
AOP
我们再来看没有AOP会怎样。
面向对象的局限性
对象对象编程(Object-oriented programming,缩写OOP)三大特性:集成,封装,多态,我们已经使用的炉火纯青,OOP的好处无需累赘,大家都深有体会,我们在来看下OOP的局限性
当有重复代码时,可以将其封装出来然后使用,我们通过分层,分包,分类来规划不同的逻辑和职责,就像前面讲的三层架构,但这里的复用都是核心业务逻辑,并不能复用一些辅助逻辑,比如:日志记录,性能统计,安全校验,事务管理等等,这些边缘性逻辑往往贯穿你的整个核心业务,传统OOP很难将其封装。
1 | public class UserServiceImpl implements UserService { |
为了方便演示,我们只打印语句,就算如此这样的代码看着也很难受,而且这些逻辑是所有的业务方法都要加上,想想就很恐怖
OOP是至上而下的编程方式,犹如一个树状图,A调用B,B调用C,或者A继承B,B继承C,这种方式对于业务逻辑来说是合适的,通过调用或者继承以复用。而辅助逻辑就像一把闸刀横向贯穿了所有方法,如图所示:
这一条条横线切开了OOP的树状结构,犹如一个大蛋糕被切开了多层,每一层都有相同的执行逻辑,所以大家将这些辅助逻辑称之为切面
代理模式用来增加或者增强原有功能在合适不过了,但是切面逻辑的难点不是不修改原有的业务,而是对所有业务生效,对一个业务类增强就得新建一个代理类,对所有业务增强就得新建所有代理类,这无疑是一种灾难,而且这里只是演示了一个日志打印的切面逻辑,如果我在加一个性能统计的切面,就得新建一个切面类来代理性能统计的代理类,一旦切面多起来这个代理嵌套就非常深。
面向切面编程(Aspect-oriented programming,缩写为AOP),正式为了解决这一问题而诞生的。
AOP
AOP不是OOP的对立面,而是对OOP的一种补充,OOP是纵向的,AOP是横向的,连着相结合能够构建良好的程序结构,AOP技术,让我们不修改原有的代码,便能让切面逻辑在所有的业务逻辑中生效
我们只需要声明一个切面,写上切面逻辑:
1 | // 声明一个切面 |
无论你有一个业务方法,还是一万个业务方法,对我们开发者来说只需编写一次切面逻辑,就能让所有业务方法生效,极大提高了我们的开发效率。
总结
IOC解决的问题:
- 创建许多重复的对象,造成了大量资源浪费
- 更换实现类需要改动多个地方
- 创建和配置组件工作复杂,给组件调用方带来了极大的不便
AOP解决的问题:
- 切面逻辑编写繁琐,有多个业务就需要编写多少次。
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
扫描二维码,分享此文章