可扩展程序的优秀设计模式
我有个问题,想知道怎么为我的程序设计一个好的架构。我的程序其实很简单,但我希望它的结构能很好,以后也能方便扩展。
我的程序需要从外部数据源(比如XML文件)获取数据,提取出有用的信息,最后准备SQL语句把这些信息导入数据库。所以,不论现在还是将来,所有外部数据源的处理流程都是:获取数据、提取信息和加载数据。
我在考虑创建一些通用的类,比如叫:DataFetcher(数据获取器)、DataExtractor(数据提取器)和DataLoader(数据加载器),然后再写一些具体的类来继承它们。我想我可能需要用到某种工厂设计模式,但具体用哪种呢?是工厂方法模式还是抽象工厂模式?
我还希望避免写这样的代码:
if data_source == 'X':
fetcher = XDataFetcher()
elif data_source == 'Y':
fetcher = YDataFetcher()
....
理想情况下(我不确定这是否容易实现),我希望能写一个新的“数据源处理器”,只需在现有代码中添加一两行,我的程序就能从新的数据源加载数据。
我该如何利用设计模式来实现我的目标呢?如果能给我一些Python的例子,那就太好了。
5 个回答
你想做的事情是动态导入一个模块(这个模块是基于某个基类的)。这有点像C++中动态加载DLL的用法。
可以看看这个StackOverflow的问题。还有Python文档中关于importlib.import_module
的内容(这个其实是对__import__
的一个封装)
import importlib
moduleToImport = importlib.import_module("moduleName")
你没有提到最重要的部分,也就是你数据的形状。这真的是最关键的地方。“设计模式”其实是个干扰因素——很多设计模式的出现是因为某些语言的限制,而Python并没有这些限制,所以引入了不必要的复杂性。
- 首先要关注你数据的形状。例如:
- 首先你有XML格式的数据
- 然后你从XML中提取出一些数据(是简单的字典吗?还是嵌套的字典?你需要什么数据?这些数据是同类的还是不同类的?这才是最重要的,但你没有提到!)
- 接着你把这些数据存储到SQL数据库中。
- 然后设计“接口”(就是对方法、属性,或者字典或元组中的项目的描述),以便对这些数据进行操作。如果你保持简单,使用Python的原生类型,可能根本不需要类,只需要函数和字典/元组就可以了。
- 不断重复这个过程,直到你得到应用所需的抽象层次。
例如,一个“提取器”的接口可以是“一个可迭代的对象,返回XML字符串”。注意,这可以是一个生成器,也可以是一个有__iter__
和next()
方法的类!没有必要定义一个抽象类再去继承它!
你为数据添加的可配置多态性取决于数据的具体形状。例如,你可以使用约定:
# persisters.py
def persist_foo(data):
pass
# main.py
import persisters
data = {'type':'foo', 'values':{'field1':'a','field2':[1,2]}}
try:
foo_persister = getitem(persisters, 'persist_'+data['type'])
except AttributeError:
# no 'foo' persister is available!
或者如果你需要进一步的抽象(也许你需要添加一些你无法控制的新模块),你可以使用注册表(其实就是一个字典)和模块约定:
# registry.py
def register(registry, method, type_):
"""Returns a decorator that registers a callable in a registry for the method and type"""
def register_decorator(callable_):
registry.setdefault(method, {})[type_] = callable_
return callable_
return register_decorator
def merge_registries(r1, r2):
for method, type_ in r2.iteritems():
r1.setdefault(method, {}).update(r2[method])
def get_callable(registry, method, type_):
try:
callable_ = registry[method][type]
except KeyError, e:
e.message = 'No {} method for type {} in registry'.format(method, type)
raise e
return callable_
def retrieve_registry(module):
try:
return module.get_registry()
except AttributeError:
return {}
def add_module_registry(yourregistry, *modules)
for module in modules:
merge_registries(yourregistry, module)
# extractors.py
from registry import register
_REGISTRY = {}
def get_registry():
return _REGISTRY
@register(_REGISTRY, 'extract', 'foo')
def foo_extractor(abc):
print 'extracting_foo'
# main.py
import extractors, registry
my_registry = {}
registry.add_module_registry(my_registry, extractors)
foo_extracter = registry.get_callable(my_registry, 'extract', 'foo')
如果你想的话,可以在这个结构上轻松建立一个全局注册表(尽管即使这样做稍微不方便,你也应该尽量避免全局状态)。
如果你正在构建一个公共框架,并且需要最大程度的可扩展性和形式化,同时愿意为复杂性付出代价,你可以看看像zope.interface
这样的东西。(Pyramid框架就使用了它。)
与其自己开发一个提取-转换-加载的应用程序,不如考虑一下scrapy?使用scrapy,你可以编写一个“爬虫”,它接收一个字符串并返回一系列的项目(你的数据)或请求(请求更多字符串,比如要抓取的URL)。这些项目会被送入一个可配置的项目管道,管道可以对接收到的项目进行任意处理(例如,存入数据库),然后再传递下去。
即使你不使用Scrapy,你也应该采用以数据为中心的管道式设计,倾向于用抽象的“可调用”和“可迭代”接口来思考,而不是具体的“类”和“模式”。
如果所有的获取器(fetchers)都有相同的接口,你可以使用字典来管理它们:
fetcher_dict = {'X':XDataFetcher,'Y':YDataFetcher}
data_source = ...
fetcher = fetcher_dict[data_source]()
关于保持灵活性这一点——只需编写干净、符合习惯的代码。我个人比较喜欢“你不需要它”(YAGNI)的理念。如果你花太多时间去预测未来,想知道自己会需要什么,你的代码就会变得臃肿复杂,等到真正需要修改的时候就很难简单调整了。如果一开始代码写得干净,后续再进行调整就会容易得多。