• home > theory > engineering > model >

    前端AOP/面向切面编程、IoC/控制反转、DI/依赖注入

    Author:zhoulujun Date:

    面向切面编程(横切关注点)将我们对于业务逻辑无关的一些操作从业务逻辑中剥离出来,将它们放在一些独立的方法中。控制反转就是面向接口编程而不是面向实现编程,将所依赖的底层功能模块注入到高层模块中。依赖注入

    Java Spring,肯定悉知 Spring框架IOC和AOP的实现原理

    AOP(Aspect Oriented Programming)

    AOP面向方面编程基于IoC,是对OOP的有益补充;

    这里建议温习一下《java反射机制原理剖析》、《Java注解(批注)的基本原理 》,算安利吧。

    AOP利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了 多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的 逻辑或责任封装起来,比如日志记录,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性

    AOP代表的是一个横向的关 系,将“对象”比作一个空心的圆柱体,其中封装的是对象的属性和行为;则面向方面编程的方法,就是将这个圆柱体以切面形式剖开,选择性的提供业务逻辑。而 剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹,但完成了效果。它可以让编程人员在不修改对象代码的情况下,为这个对象添加额外的功能或者限制

    AOP (Aspect Oriented Programming),中文译为:面向切面编程(横切关注点),是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。

    AOP 主要的思想是将我们对于业务逻辑无关的一些操作,比如日志记录、性能统计、安全控制、异常处理、错误上报等,将这些操作从业务逻辑中剥离出来,将它们放在一些独立的方法中,然后如果我们对这些操作做修改的时候就可以不用影响到业务逻辑相关的代码。它主要体现了我们对代码的低耦合性的追求。

    但是前端开发都知道面向对象编程(OOP),却比较少了解 AOP(面向切面编程)这个概念。

    typescript出来,还不得大搞IOC与AOP

    AngularJS、NestJS(都是后端大拿整出来的),全面借鉴了后台开发的一些设计思想(当然是 spring),写法跟java神似

    @Injectable({
      providedIn: 'root'})export class BrowserStorageService {
      constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}
    
      get(key: string) {
        this.storage.getItem(key)
      }
    
      set(key: string, value: string) {
        this.storage.setItem(key, value)
      }
    
      remove(key: string) {
        this.storage.removeItem(key)
      }
    
      clear() {
        this.storage.clear()
      }}

    看NestJS,有回到了些java的快感里

    @Controller('users')
    export class UsersController {
      constructor(private readonly usersService: UsersService) {}
    
      @Post()
      create(@Body() createUserDto: CreateUserDto): Promise<User> {
        return this.usersService.create(createUserDto)
      }
    
      @Get()
      findAll(): Promise<User[]> {
        return this.usersService.findAll()
      }
    
      @Get(':id')
      findOne(@Param('id') id: string): Promise<User> {
        return this.usersService.findOne(id)
      }
    
      @Delete(':id')
      remove(@Param('id') id: string): Promise<void> {
        return this.usersService.remove(id)
      }
    }



    前端AOP

    在面向对象中,我们强调单一职责原则和封装,于是我们用不同的类来设计不同的方法,这样代码就分散到一个个类中,降低了复杂度,也提高了类的可重用。

    推荐阅读:前端开发中的 AOP 和 IoC https://github.com/yinguangyao/blog/issues/39

    • 经典的 before 和 after:修改函数的原型,增加 before 和 after 两个方法。

    • 代理模式:创建一个代理类,在这个类里面去执行 eat 方法就好了。

    • Proxy:通过 Proxy 来代理类上面的 eat 方法,在执行 eat 方法之后去插入执行 log 方法。

    • 装饰器:用装饰器来实现后就会更加优雅。

    • 中间件:中间件技术类似于可以自由组合、自由插拔的插件机制,你可以使用多个中间件去帮完成一些与主业务无关的任务。


    前端IoC

    IoC(Inversion of Control),即 “控制反转”是基于 依赖倒置 的设计模式。就是面向接口编程而不是面向实现编程,将所依赖的底层功能模块注入到高层模块中。

    软件设计专家 Martin Fowler 在 2004 年编写的一篇文章 Inversion of Control Containers and the Dependency Injection pattern 里对其进行了总结,对控制反转、依赖注入这些概念完全不了解的可以先阅读此文。

    在正常情况下,当前对象会自己负责创建其依赖的所有对象,也就是当前对象为控制方。而在控制反转情况下,当前对象会以某种方式自动获得其依赖的所有对象,就像是被控制了一样。这个控制方现在就是 IoC 容器,前提是被创建的对象允许以某种方式由外部注入其依赖对象。

    按照自动获得依赖对象的方式的不同,控制反转思想的实现可分为依赖注入和服务定位器(Service Locator)两种模式。

    依赖查找(Dependency Lookup):容器提供接口和上下文条件给组件,组件需要实现接口,该接口提供了一个对象,可以重用这个对象查找依赖。

    依赖注入(Dependency Injection):内置对象是通过注入的方式进行创建。依赖注入有两种实现方式:Setter方式(传值方式)和构造器方式(引用方式)。

    两者并不互斥,通常会结合起来使用。不过依赖注入的使用场景要远多于服务定位器,这也是通常只把它跟控制反转一起提及的原因。依赖注入能够解耦组件之间的关系,从而使得组件使用起来更简单,也变得更加通用,同时还能简化应用结构。

    在开发中, IoC 意味着你设计好的对象交给容器控制,而不是使用传统的方式,在对象内部直接控制。

    如何理解好 IoC 呢?理解好 IoC 的关键是要明确 “谁控制谁,控制什么,为何是反转,哪些方面反转了”,我们来深入分析一下。

    谁控制谁,控制什么:

    在传统的程序设计中,我们直接在对象内部通过 new 的方式创建对象,是程序主动创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 IoC 容器控制对象的创建;

    谁控制谁?

    当然是 IoC 容器控制了对象;

    控制什么?

    主要是控制外部资源(依赖对象)获取。

    为何是反转了,哪些方面反转了:

    有反转就有正转,传统应用程序是由我们自己在程序中主动控制去获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;

    为何是反转?

    因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转了;

    哪些方面反转了?

    依赖对象的获取被反转了。

    IoC 能做什么

    IoC 不是一种技术,只是一种思想,是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。

    传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器注入组合对象,所以对象之间是松散耦合。 这样也便于测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活

    其实 IoC 对编程带来的最大改变不是从代码上,而是思想上,发生了 “主从换位” 的变化。应用程序本来是老大,要获取什么资源都是主动出击,但在 IoC 思想中,应用程序就变成被动了,被动的等待 IoC 容器来创建并注入它所需的资源了



    依赖注入(Dependency Injection, DI)

    依赖倒置原则要求程序依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

    前端中的依赖注入

    在 AngularJS 中,依赖注入是其核心的特性之一。在 AngularJS 中声明依赖项有 3 种方式:

    // 方式一: 使用 $inject annotation 方式 
    let fn = function (a, b) {}; 
    fn.$inject = ['a', 'b']; 
     
    // 方式二: 使用 array-style annotations 方式 
    let fn = ['a', 'b', function (a, b) {}]; 
     
    // 方式三: 使用隐式声明方式  
    let fn = function (a, b) {}; // 不推荐

    在 mobx 和 redux 中都有注入的概念,通过 inject/connect 方法,将组件需要的状态和方法都注入进去。

    // redux
    @connect(mapStateToProps, mapDispatchToProps)
    class App extends React.Component {}
    
    // mobx
    @inject(({ store }) => ({
        // ...
    }))
    class App extends React.Component {}

    React的Context API、Vue的Provide/Inject其实也差不多。

    依赖注入的好处是对外部依赖比较清晰,便于维护

    在 nestjs 中到处都能看到依赖注入的实例。

    @Module({
        controllers: [ UsersController ],
        components: [ UsersService, ChatGateway ],
    })
    export class UsersModule implements NestModule {
        constructor(private usersService: UsersService) {}
    }

    依赖注入的好处就是将两个对象解耦,通过一个 IoC 容器来将依赖的数据注入到另一个对象之中,这个 IoC 容器就起了桥接的作用。

    服务定位器/依赖查找(Dependency Lookup)

    服务定位器模式则是一种不同的方式来处理依赖关系。在这种模式下,对象不是直接获取其依赖,而是通过一个全局的服务定位器对象来查找并获取它们。比如提供了一个中心化的注册表,客户端可以通过这个注册表来查找和获取所需的服务。客户端需要知道如何查询服务定位器以获取所需的服务。




    参考文章:

    TypeScript:从架构分层设计到IOC和AOP https://zyoncode.com/typescript-cong-jia-gou-fen-ceng-she-ji-dao-ioche-aop/

    前端开发中的 AOP 和 IoC  https://github.com/yinguangyao/blog/issues/39

    了不起的 IoC 与 DI https://www.51cto.com/article/623960.html

    Thinking--IOC思想在前端中的应用 https://cloud.tencent.com/developer/article/1487179






    转载本站文章《前端AOP/面向切面编程、IoC/控制反转、DI/依赖注入》,
    请注明出处:https://www.zhoulujun.cn/html/theory/engineering/model/9239.html