Skip to content

组件设计

Eric Du edited this page Sep 8, 2017 · 11 revisions

Veil框架自身,以及使用Veil框架的应用程序都是由组件(veil component,目录)、包(python package,目录)和模块(python module,文件)构成的。

Python的namespace上有两类对象,一类是module,一类是package。两者的区别在于module是普通的.py文件。而package是一个目录,同时这个目录有一个名字叫__init__.py的文件。Veil在python的namespace上添加了第三类的对象——“component”,从python的角度来看,也就是一个package。但是从veil的角度来看,这个package是特殊的package,它对于外部来说不是透明的,而是黑色的。以前可以通过a.b.c引用到package内部的module,以及module上的方法和类。如果a是component,那么a.b.c就是一个非法的引用,在调用的时候会出错。在package/module的模型上引入component/package/module的体系就是Veil框架模块化的核心特色。component把彼此透明的python代码分成了彼此封装的小块块。组件要么被依赖、要么不被依赖,不能被部分依赖。组件之间不能有循环依赖。

从代码结构上,Veil框架包括:

  1. veil_component:以python包的形式提供veil component的实现,最基础的依赖
  2. veil_installer:以veil组件的形式提供veil应用/组件安装的接口及实现,依赖veil_component包
  3. veil:以veil组件的形式提供基础功能和服务,依赖veil_component包、veil_installer组件、其他组件和python包

设计组件的考量涉及对外暴露的接口、依赖(配置、其他组件、python包)、安装(os包、python包、生成配置)和测试(数据和实现)。

安装组件时,先安装依赖的其他组件,安装声明在INSTALLER文件中依赖的os包和python包,然后执行组件的初始化方法init()。安装应用时,应用的配置包括其各个组件需要的配置,组件的配置文件必须在安装应用(application_resource)期间生成。在一个veil应用的上下文中,组件的配置有全局配置和动态配置两种,全局配置在组件的init方法中生成,动态配置则在安装过程中加载依赖组件的过程中执行相关的方法,示例如下:

    def init():
        add_application_sub_resource('hash', lambda config: hash_resource(**config))  # 全局配置在组件初始化方法生成
def register_database(purpose, verify_db=False):
    ...
    add_application_sub_resource('{}_database_client'.format(purpose), lambda config: database_client_resource(purpose=purpose, config=config)) # 动态配置在组件初始化方法生成
    ...

component的创建

创建一个component首先要创建一个普通的package,也就是建立一个目录,然后在目录里创建一个空的__init__.py文件。然后要把package变成组件,就需要在__init__.py按照规范编写如下代码:

import veil_component

with veil_component.init_component(__name__):
    pass

有了init_component这么一段,这个package就变成了组件。外部不再能够直接引用内部的module了。

组件对外提供接口

绝对的黑盒是没有多大用处的。组件还必须对外提供接口,让外部的代码可以通过接口使用组件的内部实现。这就需要在组件的__init__.py上定义一个__all__变量,示意如下:

import veil.component

with veil.component.init_component(__name__):
    from .shell import shell_execute
    from .shell import pass_control_to
    from .shell import ShellExecutionError

    __all__ = [
        # from shell
        shell_execute.__name__,
        pass_control_to.__name__,
        ShellExecutionError.__name__
    ]

只有通过__all__暴露出来的接口,才能够被外部使用。值得注意的是,__name__是一种方便写法,把上面改写成下面这样也是可以的:

import veil.component

with veil.component.init_component(__name__):
    from .shell import shell_execute
    from .shell import pass_control_to
    from .shell import ShellExecutionError

    __all__ = [
        # from shell
        'shell_execute',
        'pass_control_to',
        'ShellExecutionError'
    ]

这样写的问题在于,pycharm的编辑器会认为shell_execute等被import的方法没有地方使用,而标记为灰色,所以一般使用__name__的写法。对于常量的export,就必须使用字符串的形式了,因为常量是没有__name__的,例如:

import veil.component

with veil.component.init_component(__name__):
    from .some_impl imoprt SOME_CONST

    __all__ = [
        # from some_impl
        'SOME_CONST'
    ]

component的初始化

component自身往往也有一些初始化工作要做。在component被import的时候,其__init__.py文件以及,目录种包含的所有module都会被加载。值得注意的是,该行为并不是python的默认行为。普通的package并不会自动加载其内部的module,但component自动加载内部的module,体现的是component的整体重用的意义,不存在部分使用一个component的说法,用了就是全用了。理论上来说,component的初始化可以写到上述的任何地方,但是为了让模块的阅读者能够一目了然的知道component是怎么被初始化的,那么就需要有一个统一的地方放置component初始化代码。示例如下:

import veil.component

with veil.component.init_component(__name__):
    def init():
        from veil.model.event import subscribe_event
        from .web_setting import register_website
        from ..routing import EVENT_NEW_WEBSITE

        subscribe_event(EVENT_NEW_WEBSITE, register_website)

定义的init方法会在init_component的时候被Veil框架自动调用。

component内部成员注册

组件除了对外提供可被调用的接口之外,往往还需要向框架注册一些成员。比如说@script就是向Veil框架注册了一个script handler。这样从命令行执行veil命令的时候,就能够调用到@script注册的函数。从某种意义上说,这些组件内部成员,是以向框架注册的形式,对外暴露了出去。本质上来说,也是组件对外提供的接口一种。示例如下:

demo/demo.py

from veil.frontend.cli import *

@script('hello')
def say_hello():
    print('hello!')

常见的框架注册方式有:

  • @script:注册命令行
  • @route:注册WEB路由规则
  • @installation_script:注册安装程序
  • @widget:注册WEB界面上的控件
  • @job:注册后台任务

component对外提供特殊接口

常规的接口就如上面所述的那样定义在__all__变量里。但是一个component除了对外提供常规的接口之外,有时还需要对外提供特殊接口。特殊与常规的使用区别在于使用场合。比如说__all__里面export的方法与类都是在生产环境会使用的接口,而有的时候component还要对外提供测试的时候需要用到的一些东西,比如测试数据和测试用的实现。这样的接口如果与生产环境使用的接口混杂在一个文件内对外export,从效果上来说是没问题的(本质上来说都是对外暴露的接口),但是从接口定义的阅读者的角度来说就未免有些不够清晰。所以,Veil框架还允许component以如下的方式对外提供特殊接口:

some_component/__fixture__.py

some_fixture = '测试数据'

外部可以用from some_component.__fixture__ import some_fixture的方式使用到这个component提供的名字为some_fixture的接口。其实也就时说,所有以'__'开头和结尾的模块,会被当作特殊的模块,不被封装起来,允许外部直接引用。

总结

两个要点

  • 初始化:component内部的所有模块都会在组件被import的时候自动加载。component自身的初始化放置在__init__.py的init_component内部的init方法内。
  • 暴露接口:对外提供的API用__all__暴露,@script等通过向框架注册暴露内部成员,特殊的如测试使用的接口用__fixture__.py等形式对外暴露。