好的设计模式来使程序更具扩展性

6
我有一个关于如何为我的程序设计一个好的架构的问题。我的程序相当简单,但我想要一个良好的架构,使我的程序在未来容易扩展。
我的程序需要从外部数据源(XML)获取数据,提取这些数据的信息,并最终准备SQL语句以将信息导入数据库。因此,对于现在和将来可能存在的所有外部数据源,我的应用程序都有一个简单的“流程”:获取、提取和加载。
我考虑创建通用类,称为DataFetcher、DataExtractor和DataLoader,然后编写从它们继承的特定类。我想我会需要一些工厂设计模式,但是使用哪种?FactoryMethod还是Abstract Factory?
我还希望不要使用这样的代码:
if data_source == 'X':
     fetcher = XDataFetcher()
elif data_source == 'Y':
     fetcher = YDataFetcher()
....

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

如何利用设计模式来实现我的目标?如果您能提供一些python示例,那就太好了。

5个回答

10

如果所有的fetchers都有相同的接口,您可以使用一个字典:

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

就保持代码的灵活性而言,只需编写干净的惯用代码即可。我倾向于喜欢“你不会需要它”(YAGNI)哲学。如果你花太多时间试图展望未来并找出你将需要什么,你的代码最终会变得过于臃肿和复杂,以至于当你发现实际需要什么时,无法做出简单的调整。如果代码一开始就很清晰,那么重构以适应您的需求应该很容易。


1
地图就是伪装成switch语句的东西 ;) - atamanroman
1
@martineau -- 关于“所有映射都是dict的变体”的说法--这并不正确--用户可以定义不是dict的映射。至于另一点,这当然是个人喜好问题。但我已经看到有些人过分地使用了几次:def square(x): return x*x。那是一个非常无用的函数,“只是为了以防万一我想改变平方”。将本应该是普通函数的东西制作成类也是同样的道理。当然,我并不是说“内联一切”...只是试图做到实际。 - mgilson
1
@martineau -- 当然,设计策略也需要与最终目标相匹配。如果你想创建一个超级强大、直观的库来执行某些任务(例如hdf5文件格式)-- 那么花费额外的精力使事情更加灵活就更有意义了。 - mgilson
我的陈述与具体实现无关。在几乎任何语言中,映射的概念应该是相似的。当涉及到不同的实现时,我经常看到这种“如此多的if else代码,让我们使用映射代替”的说法。 - atamanroman
也许我有点喜欢把事情复杂化,但我只想写出好的代码。我知道在两个月内会有新的数据源需要处理,所以我想提前准备。我找到了这样一个东西:http://code.activestate.com/recipes/576687-maintenance-free-factory-design-pattern/ 它好吗?我忘了提到每个获取器将被作为单独的脚本调用,因为每个数据源在不同的时间刷新其信息。所以考虑到这一点,我认为你的代码示例不好。我必须更改每个脚本以更新fetcher_dict... - sebast26
显示剩余8条评论

1
您忽略了最重要的部分,即数据的形状。这真的是最重要的事情。 "设计模式" 是一个干扰因素 - 许多这些模式存在是因为 Python 没有的语言限制,并引入了不必要的严格性。
1. 首先看看你的数据形状。例如: - 首先你有 XML - 然后你有从 XML 中提取的一些数据集合(一个简单的字典?一个嵌套的字典?你需要什么数据?它是同质还是异质的?这是最重要的事情,但你没有谈论它!) - 然后将这些数据序列化 / 持久化在 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,你将编写一个“Spider”,它会接收一个字符串并返回 Item 序列(即你的数据)或 Request 序列(即请求更多字符串,例如要获取的 URL)。在可配置的 Item Pipeline 中,Item 会被发送到它接收到的任何 Item 上执行的操作中(例如在数据库中持久化),然后将它们传递下去。

即使你不使用 Scrapy,你也应该采用以数据为中心的流水线式设计,并优先考虑抽象的“可调用”和“可迭代”接口,而不是具体的“类”和“模式”。


0
你正在尝试动态导入一个基于某个基类的模块,就像C++中动态dll加载的用例一样。
请尝试以下SO问题。以及Python文档中关于importlib.import_module(它只是__import__的包装器)的说明。
import importlib
moduleToImport = importlib.import_module("moduleName")

将此与获取器结合,再加上一个数据库(例如非代码解决方案)用于配置,您就拥有了一个不错的插件系统。无需重写代码。再次强调:很可能是过度设计。 - RickyA
或者只需将要加载的模块名称(基本上是插件)作为命令行参数传递(就像链接的SO问题中一样),然后您就完成了。这样,唯一需要更改的代码就是插件中的代码,而不是您的“调用方”(除非接口发生更改,但这是另一个问题)。 - g19fanatic

0

我会将抽象添加到外部资源的上方而不是在个别外部源上。抽象化如何与外部源交互。例如,SoapClient、HttpClient、XmlServiceCLient、NativeOBjectCLient 等。这样,当您必须使用新的方法调用外部源时,只需添加一个新类即可。这样您就不必经常编写新的提取器类。(注:我不是 Python 开发者)

使用 ServiceRegistry 模式调用外部资源。您的 service registry.xml 中的一个条目将是

    <service registry>
      <externaldatasource>
         <dsname> xmlservice</dsname>
         <endpointurl> some url </endpointurl>
         <invocationtype> Soap </invocationtype>
      </externaldatasource>
    </service registry> 

当一个类需要访问外部资源时,该类只需将数据源名称传递给服务注册类。SR读取xml文件并调用外部资源以获取数据。现在,单个类将处理所有外部调用,并且没有太多的代码开销。
一旦您从客户端获得原始数据,您希望将其转换为您的数据模型。我假设您有自己的Xsd。使用xslt将传入的XML转换为您的xsd格式,并进行验证。我建议使用工厂模式来处理非XML数据格式,如Json。不必实现它们...只是为未来扩展打开一个口子。
这整套类可以位于Gateway包中。任何外部客户端相关的代码都将在此包中,并且不会渗透到任何其他包中。这被称为网关模式。此包中公共类的输入/输出将是您的域模型。
然后,您只需要一个单独的逻辑来加载到数据库中,这与任何外部数据源无关。

0

XML是结构化的,SQL-Inserts是表格形式的。听起来很简单,不要过度设计。你的基本方法可能是:

  1. 在文件系统中找到一堆XML文件
  2. 调用解析器对XML进行解析,得到一些树
  3. 查询树或递归树以填充一堆表格数据结构
  4. 序列化数据结构生成一些INSERTs

你的“业务逻辑”是第3点,这将因情况而异。一个写得好的例子会比几层抽象更有帮助。整个东西可能太小了,不值得拥有自己的领域特定语言。此外,XSLT已经存在。

其他点是可重用的候选项,但对我来说更像是编写良好且文档完备的函数,而不是工厂模式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接