可扩展程序的优秀设计模式

6 投票
5 回答
2571 浏览
提问于 2025-04-17 15:15

我有个问题,想知道怎么为我的程序设计一个好的架构。我的程序其实很简单,但我希望它的结构能很好,以后也能方便扩展。

我的程序需要从外部数据源(比如XML文件)获取数据,提取出有用的信息,最后准备SQL语句把这些信息导入数据库。所以,不论现在还是将来,所有外部数据源的处理流程都是:获取数据、提取信息和加载数据。

我在考虑创建一些通用的类,比如叫:DataFetcher(数据获取器)、DataExtractor(数据提取器)和DataLoader(数据加载器),然后再写一些具体的类来继承它们。我想我可能需要用到某种工厂设计模式,但具体用哪种呢?是工厂方法模式还是抽象工厂模式?

我还希望避免写这样的代码:

if data_source == 'X':
     fetcher = XDataFetcher()
elif data_source == 'Y':
     fetcher = YDataFetcher()
....

理想情况下(我不确定这是否容易实现),我希望能写一个新的“数据源处理器”,只需在现有代码中添加一两行,我的程序就能从新的数据源加载数据。

我该如何利用设计模式来实现我的目标呢?如果能给我一些Python的例子,那就太好了。

5 个回答

0

你想做的事情是动态导入一个模块(这个模块是基于某个基类的)。这有点像C++中动态加载DLL的用法。

可以看看这个StackOverflow的问题。还有Python文档中关于importlib.import_module的内容(这个其实是对__import__的一个封装)

import importlib
moduleToImport = importlib.import_module("moduleName")
1

你没有提到最重要的部分,也就是你数据的形状。这真的是最关键的地方。“设计模式”其实是个干扰因素——很多设计模式的出现是因为某些语言的限制,而Python并没有这些限制,所以引入了不必要的复杂性。

  1. 首先要关注你数据的形状。例如:
    1. 首先你有XML格式的数据
    2. 然后你从XML中提取出一些数据(是简单的字典吗?还是嵌套的字典?你需要什么数据?这些数据是同类的还是不同类的?这才是最重要的,但你没有提到!)
    3. 接着你把这些数据存储到SQL数据库中。
  2. 然后设计“接口”(就是对方法、属性,或者字典或元组中的项目的描述),以便对这些数据进行操作。如果你保持简单,使用Python的原生类型,可能根本不需要类,只需要函数和字典/元组就可以了。
  3. 不断重复这个过程,直到你得到应用所需的抽象层次。

例如,一个“提取器”的接口可以是“一个可迭代的对象,返回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,你也应该采用以数据为中心的管道式设计,倾向于用抽象的“可调用”和“可迭代”接口来思考,而不是具体的“类”和“模式”。

10

如果所有的获取器(fetchers)都有相同的接口,你可以使用字典来管理它们:

fetcher_dict = {'X':XDataFetcher,'Y':YDataFetcher}
data_source = ...
fetcher = fetcher_dict[data_source]()

关于保持灵活性这一点——只需编写干净、符合习惯的代码。我个人比较喜欢“你不需要它”(YAGNI)的理念。如果你花太多时间去预测未来,想知道自己会需要什么,你的代码就会变得臃肿复杂,等到真正需要修改的时候就很难简单调整了。如果一开始代码写得干净,后续再进行调整就会容易得多。

撰写回答