IOC 容器

什么叫 ioc 容器?

如果把在 Noba 中提供某方面功能的叫做服务1,那么把生产这些服务实体的地方叫做容器或则工厂。而服务有时候会依赖其他服务,如何能解决这些依赖关系?ioc 容器提供了一种很好的解决依赖关系的依赖注入工厂模式。

┌─────────────┐        ┌─────────────┐
│ Application │------->│IOC Container│
└─────────────┘        └─────────────┘
         ▲                     |
         |        ┌────────────┴───────────┐
         |        │                        │
         |   Register dependencies       Resolve objects
         |        │                        │
         |        ▼                        ▼
┌─────────────┐  ┌─────────────┐     ┌─────────────┐
│    Class    │  │ Interface   │     │    Object   │
├─────────────┤  ├─────────────┤     └─────────────┘
│ Dependencies│<-│ Dependencies│
└─────────────┘  └─────────────┘

接下来将从四个方面来阐述 Noba 的 ioc 容器。初识 ioc 容器介绍一套完整的 ioc 容器代码,从创建到注册到使用。创建服务介绍如何创建一个 Noba 服务。注册服务介绍如何把创建的服务注册到 ioc 容器中。创建服务实体介绍如何通过容器创建出服务实体,并使用服务实体实现该有的服务。

初识 ioc 容器

  1. 新建的 noba 项目
    mkdir noba_project
    cd noba_project
    
  2. services 下新建 backtest.py文件如下
    # services/backtest.py
    
    class Indicators(object):
        def __init__(self, *args):
            super(Indicators, self).__init__(*args)
    
        def simple_moving_average(self, datas, period=5):
            return sum(datas[0:period])/period
    
    class Backtest(object):
        def __init__(self, ind: Indicators, *args):
            self.ind = ind
    
        def run(self, datas):
            today_close = datas[4]
            if self.ind.simple_moving_average(datas) > today_close:
                print('Five days sma cross close')
            else:
                print('Five days sma not cross close')  
    
    
  3. 修改 main.py 如下:
    # main.py
    
    from noba import core
    
    if __name__ == '__main__':
        backtest = core.make('services.backtest.Backtest')
        close = [1.9, 1.3, 2.5, 3.1, 2.1]
        backtest.run(close)
    
  4. 运行 main.py
    python main.py
    

创建服务

* 以 初识 ioc 容器 中代码为例:假设有一个量化回测类依赖于某个指标类

# services/backtest.py

class Indicators(object):
    def __init__(self, *args):
        super(Indicators, self).__init__(*args)

    def simple_moving_average(self, datas, period=5):
        return sum(datas)/period

# Backtest 依赖于 Indicators类,所以在 __init__ 中进行依赖注入
class Backtest(object):
    def __init__(self, ind: Indicators, *args):
        self.ind = ind  # 容器会自动解决依赖关系,获得 Indicators 的实例

    def run(self, datas):
        today_close = datas[4]
        if self.ind.simple_moving_average(datas) > today_close:
            print('sma cross close')
        else:
            print('sma not cross close')
        
  1. 在服务(Backtest)的 __init__ 中通过 ind: Indicators 方式注入依赖(Indicators)。但容器不一定要解决依赖,也可以简单用作实例化的目的。比如:
    ind = core.make(Indicators)
    # 等同于
    ind = Indicators()
    
  2. 依赖和服务可以不在 main.py 文件中。甚至依赖和服务也不需要在一个文件里

注册服务

* 注册时可以用服务或接口的路径字符串,比如 'services.backtest.ThreeDayBacktest' 就是一个路径字符串。也可以通过 import 服务或则接口,然后用该服务或接口进行注册。推荐使用路径字符串。

  1. 通过服务别名注册2

    • 编辑 config/core_config.jsonaliases 值。举例:
      "aliases": {
          "backtest": "services.backtest.Backtest"
      }    
      
      *注意:服务别名注册默认在创建服务实体时是单例模式。可以通过如下方式取消单例模式
      "aliases": {
          "backtest": ["services.backtest.Backtest", false]
      }    
      
    • 编辑 main.py
      # backtest = core.make('services.backtest.Backtest')
      backtest = core.make('services') # 因为有了别名,所以直接可以通过别名创建服务实体
      
  2. 通过绑定注册3。你可以通过 bind, singleton, instance 绑定接口到服务或则别名到服务。在 bind, singleton 绑定时,还可以绑定一个函数

    • 修改 services/backtest.py 如下

      # services/backtest.py
      
      from abc import ABCMeta, abstractmethod
      
      class BacktestAbs(object, metaclass=ABCMeta):
          @abstractmethod
          def run(self):
              pass
      
      class Indicators(object):
          def __init__(self, *args):
              super(Indicators, self).__init__(*args)
      
          def simple_moving_average(self, datas, period):
              return sum(datas[0:period])/period
      
      class ThreeDayBacktest(BacktestAbs):
          def __init__(self, ind: Indicators, *args):
              self.ind = ind
      
          def run(self, datas):
              today_close = datas[2]
              if self.ind.simple_moving_average(datas, period=3) > today_close:
                  print('Three days sma cross close')
              else:
                  print('Three days sma not cross close')
      
      class FiveDayBacktest(BacktestAbs):
          def __init__(self, ind: Indicators, *args):
              self.ind = ind
      
          def run(self, datas):
              today_close = datas[4]
              if self.ind.simple_moving_average(datas, period=5) > today_close:
                  print('Five days sma cross close')
              else:
                  print('Five days sma not cross close')  
      
      
    • bindsingleton 的区别仅仅在于创建服务实体时是不是单例模式。示例:修改 main.py 如下

      # main.py
      
      from noba import core
      
      if __name__ == '__main__':
          # 绑定 BacktestAbs 接口到 ThreeDayBacktest 服务
          core.bind('services.backtest.BacktestAbs', 'services.backtest.ThreeDayBacktest')
          
          # 绑定 BacktestAbs 接口到 FiveDayBacktest 服务
          # core.bind('services.backtest.BacktestAbs', 'services.backtest.FiveDayBacktest')
      

      * tips: 觉得接口名或路径字符串太长?还可以通过绑定接口到别名的方式减少创建服务实体时的麻烦。例如:

      # main.py
      
      from noba import core
      
      if __name__ == '__main__':
          # 绑定 BacktestAbs 接口到 ThreeDayBacktest 服务
          core.bind('services.backtest.BacktestAbs', 'services.backtest.ThreeDayBacktest')
          core.bind('BacktestAbs', 'services.backtest.BacktestAbs')
      
          # make 的用法见创建服务实体
          # core.make('BacktestAbs') # 不再需要 core.make('services.backtest.BacktestAbs')
          
      
    • instance 绑定接口或则别名到一个实例

      # main.py
      
      from noba import core
      
      if __name__ == '__main__':
          three_day_backtest = core.make('services.backtest.ThreeDayBacktest')
          core.instance('services.backtest.BacktestAbs', three_day_backtest)
      
    • 绑定函数为创建服务实体时提供了更多操作空间

      # main.py
      
      from noba import core
      
      def ret_services(core):
          print('绑定接口或别名到函数')
          three_day_backtest = core.make('services.backtest.ThreeDayBacktest')
          return three_day_backtest
      
      if __name__ == '__main__':
          core.bind('services.backtest.BacktestAbs', ret_services)
      
      
  3. 通过服务提供者注册。服务提供者是绑定服务的更加工程化实现方式。register 用于服务绑定。绑定方法同第 2 点。boot 用于服务提供者的其他引导工作,会在所有服务提供者的注册工作完成后运行

    • 修改 services/backtest.py 同绑定环节

    • 新建 provider/backtest_provider.py 文件如下:

      # provider/backtest_provider.py
      
      class BacktestProvider():
          def __init__(self, core):
              self.__core = core
      
          def register(self):
              # 绑定 BacktestAbs 接口到 ThreeDayBacktest 服务
              self.__core.bind('services.backtest.BacktestAbs', 'services.backtest.ThreeDayBacktest') 
              
              # 绑定 BacktestAbs 接口到 FiveDayBacktest 服务
              # self.__core.bind('services.backtest.BacktestAbs', 'services.backtest.FiveDayBacktest')
      
              return self
      
          def boot(self):
              pass        
      
    • 修改 config/core_config.jsonproviders 值如下:

      {
          "providers": ["provider.backtest_provider.BacktestProvider"]
      }
      

创建服务实体

* 通过容器创建服务实体有三种等价方式:core.make()core()core[]。无论何种方式都能解决依赖注入问题

  1. 没有经过注册的服务,也可以直接通过容器创建服务实体。* 创建对象可以是一个服务的路径字符串,比如 'services.backtest.ThreeDayBacktest' 。也可以是通过 import 服务。推荐使用路径字符串。

    # main.py
    
    from noba import core
    
    if __name__ == '__main__':
        # 假设 ThreeDayBacktest 没有通过服务别名、绑定、服务提供者进行过注册,仍然可以直接通过容器创建出服务实体
        three_day_backtest = core.make('services.backtest.ThreeDayBacktest')
    
  2. 通过服务别名注册的服务。沿用注册服务第 1 点的例子

    # main.py
    
    from noba import core
    
    if __name__ == '__main__':
        backtest = core.make('backtest')
        close = [1.9, 1.3, 2.5, 3.1, 2.1]
        backtest.run(close)
    
        # 运行 main.py 会打印出下面信息
        # Five days sma cross close        
    
  3. 通过绑定注册的服务。沿用注册服务第 2 点的例子

    # main.py
    
    from noba import core
    
    if __name__ == '__main__':
        backtest = core.make('services.backtest.BacktestAbs')
        close = [1.9, 1.3, 2.5, 3.1, 2.1]
        backtest.run(close)
    
        # 运行 main.py 会打印出下面信息
        # Three days sma not cross close
    
  4. 通过服务提供者注册的服务。沿用注册服务第 3 点的例子

    # main.py
    
    from noba import core
    
    if __name__ == '__main__':
        backtest = core.make('services.backtest.BacktestAbs')
        close = [1.9, 1.3, 2.5, 3.1, 2.1]
        backtest.run(close)
    
        # 运行 main.py 会打印出下面信息
        # Three days sma not cross close
    

1

一个服务是一个类,也可以是一个函数或则一个 python 模块等

2

服务别名的作用是用别名全局代替服务。这样可以方便的在任何地方用较短的路径创建服务实体

3

绑定注册为面向接口编程提供一种便捷方式