-
Notifications
You must be signed in to change notification settings - Fork 3
组件设计
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框架包括:
- veil_component:以python包的形式提供veil component的实现,最基础的依赖
- veil_installer:以veil组件的形式提供veil应用/组件安装的接口及实现,依赖veil_component包
- 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首先要创建一个普通的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被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框架自动调用。
组件除了对外提供可被调用的接口之外,往往还需要向框架注册一些成员。比如说@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:注册后台任务
常规的接口就如上面所述的那样定义在__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等形式对外暴露。