wxPython: 如何在控制器中组织每个组件的数据?
我有一个小工具,它可以显示文件系统的层级结构,方便用户浏览(基本上就是一个树形控件,还有一些相关的工具栏按钮,比如“刷新”)。每个这样的工具都有一组基础目录供它显示(可以递归显示)。假设用户可以根据需要创建多个这样的工具。需要注意的是,这些工具和任何业务数据没有关系——它们是独立于模型的。
在好的MVC设计中,这些(每个工具独有的)基础目录应该放在哪里呢?
当用户点击刷新按钮时,控制器会捕捉到这个事件,并且这个事件包含了对应的文件浏览工具。控制器会为这个特定的工具确定基础目录(以某种方式),然后遍历这个目录路径,并把一些数据传递给工具进行渲染。
我想到两个可以存储基础目录的地方:
- 简单的解决方案:把基础目录作为工具的一个实例变量,然后让控制器来操作它,以保持这个工具的状态。不过,这里有个概念上的问题:因为这个工具从来不查看这个实例变量,所以你实际上是把控制器的一部分责任转移到了工具上。
- 更复杂(从技术上讲,可能从概念上讲也是)的解决方案:在控制器中保持一个
{widget: base_directory_set}
的映射,并使用弱引用作为键。
第二种方法可以让控制器的职责在以后更容易扩展,因为把东西放在控制器中通常会这样做——例如,如果我后来决定想要确定所有这些工具的基础目录集合。
可能我缺少一些MVC的知识,这种知识能够很好地解决这类问题。
3 个回答
我现在采用的解决方案是“每个小部件都有自己的控制器”。(也许这个概念已经有了现成的名称。)这个控制器会把一些全局的功能交给一个“父”控制器来处理,但它主要是用来控制每个小部件,并且管理与每个小部件相关的数据。
“每个小部件的控制器”这个概念可以避免把不相关的属性直接放到小部件上。你可以扩展这些控制器,让它们在创建或销毁小部件时注册或注销这些小部件,这样在需要对多个小部件进行操作时,就不需要用到复杂的弱引用技术了。
举个例子:
class FSBrowserController(wx.EvtHandler):
"""Widget-specific controller for filesystem browsers.
:ivar parent: Parent controller -- useful when UI-wide control is
necessary to respond to an event.
"""
def __init__(self, parent, frame, tree_ctrl, base_dirs):
self.parent = parent
self.frame = frame
self.tree_ctrl = tree_ctrl
self.base_dirs = base_dirs
frame.Bind(EVT_FS_REFRESH, self.refresh)
frame.Bind(wx.EVT_WINDOW_DESTROY, self._unregister)
self.refresh()
self._register()
def _register(self):
"""Register self with parent controller."""
self.parent._fsb_controllers.append(self)
def _unregister(self, event):
"""Unregister self with parent controller."""
if event.GetEventObject() == self.frame:
self.parent._fsb_controllers.remove(self)
def refresh(self, event=None):
"""Refresh the :ivar:`tree_ctrl` using :ivar:`base_dirs`."""
raise NotImplementedError
class Controller(wx.EvtHandler):
"""Main controller for the application.
Handles UI-wide behaviors.
"""
def __init__(self):
self._fsb_controllers = []
fsb_frame = FSBrowserFrame(parent=None)
FSBrowserController(self, fsb_frame, fsb_frame.tree_ctrl,
initial_base_dirs)
fsb_frame.Show()
这样,当FSBrowserFrame被销毁时,控制器和相关的数据也会自然地随之消失。
根据MVC方法的运作方式,我建议你对你列出的第一个解决方案进行一些修改:
把基础目录作为一个实例变量放在小部件上,然后让控制器来操作它,以保持这个小部件的状态。
为什么这么说呢?你提到小部件和模型是独立的,但它们真的没有在模型中被引用吗?如果你没有把小部件和模型绑定在一起,那就偏离了MVC的基本概念。
我对wxPython没有了解,所以无法评论它是否符合MVC。不过,我觉得你应该考虑把小部件整合进模型中,或者把它们当作模型本身来对待。
所以如果我们假设在这个上下文中,小部件实际上是你模型层次的一部分,那么这可能不仅是你说的简单解决方案,也是正确的解决方案。
因为MVC的一个核心原则是保持各个部分之间的松耦合,你总是想把业务逻辑和数据输入、展示分开。把小部件单独显示而不和模型关联,这样做就打破了这个原则,所以把任何信息处理的方法放到控制器里是不合适的。你希望模型包含所有它需要的信息,以便在展示数据时被控制器操作或显示。
你有没有考虑过为所有小部件创建一个超类,这样它们就会有一套共同的方法可以继承呢?
举个例子:
import os
WIDGET_PREFIX = '/tmp'
class Widget:
def __init__(self, name):
self.name = name
self.widget_prefix = WIDGET_PREFIX
self.dirs = os.walk(os.path.join(self.widget_prefix, name))
def _get_base_directory_set(self):
return self.dirs
base_directory_set = property(_get_base_directory_set)
希望这些能给你一些思考的方向。
从MVC的角度来看,这种设计难以符合MVC的原因在于,你想展示一些信息,而在你的理解中,这些信息“并不属于模型”。在MVC中,没有“信息不属于模型”这种说法:它的基本理念是“模型保存所有信息,视图只负责展示,控制器处理用户交互”。
你展示的信息可能和“业务数据”没有关系,但在MVC的视角下,这并不意味着这些信息“独立于模型”,因为根本没有这种情况——这只是意味着你需要一个额外的模型类(除了你用来保存“业务数据”的模型)来保存这些“非业务”数据!
所以,当用户“实例化一个小部件”(创建一个目录展示视图,可能是通过某个主视图上的用户操作,或者在另一个现有的小部件上“克隆”),控制器需要负责创建一个小部件对象和一个“目录展示模型类”的实例,并建立它们之间的连接(通常是通过在小部件上设置一个指向相关模型实例的引用),同时告诉模型去加载初始信息。当用户在小部件上的操作意味着要对模型进行某种操作时,控制器会从参与事件的小部件中获取模型实例的引用,并向该实例发送相应的请求(模型的职责是让对它感兴趣的视图知道信息的变化——通常是通过某种观察者模式;控制器并不负责向视图提供信息——这实际上是与MVC的理念非常不同的做法!)。
在你的情况下,MVC所需的架构投资是否值得,相比于一种信息流动不那么清晰、缺少必要模型的粗糙方法?我是一个务实主义者,我并不崇拜MVC,但我认为在这种情况下,投入相对较小的时间和精力去建立一个清晰的架构,可能会带来丰厚的回报。这是一个关于预见可能变化方向的问题——例如,如果你现在不需要的某些功能(但可能很快就会需要)采用正确的MVC方式会很容易添加,而如果不这样做,则可能会变成一场临时拼凑的噩梦(或者需要对整个架构进行痛苦的重构)?各种可能的情况,比如想在不同的小部件中展示相同的目录信息,或者有一个更智能的“目录信息监控”模型,可以在需要时自动刷新(并通过常见的观察者模式直接向感兴趣的视图提供新信息,而不需要控制器的参与),在MVC中都是自然且简单的(毕竟,这就是MVC的重点,所以这并不令人惊讶!),而在临时拼凑的架构中则显得笨拙且脆弱——小投资,大回报,值得尝试!
你可能会从上一段的语气中注意到,我也并不崇拜“极限编程”——作为一个务实主义者,我会进行一些“前期设计”(特别是在建立一个干净、灵活、可扩展的架构方面,从一开始就这样做,即使现在并不是绝对必要的)——正是因为根据我的经验,稍微考虑一下并进行非常适度的投资,尤其是在架构方面,会在项目的生命周期中多次获得回报(在可扩展性、灵活性、可扩展性、可维护性、安全性等方面,尽管并不是所有这些都适用于每个项目——例如,在你的情况下,安全性和可扩展性并不是主要问题……但其他方面可能会是!)。
为了更普遍地说明,我想指出,我这种务实的态度并不意味着可以在选择架构上花费过多的精力和时间(“过多”这个词的定义就是如此;-)——熟悉一些基本的架构模式(而MVC肯定是其中之一)通常会减少初期的时间和精力投入——一旦你意识到这种经典架构会很好地服务于你,比如在这种情况下,真的很容易看到如何实现它(例如,拒绝“没有M的MVC”这种想法!),而且与最笨拙、最临时的捷径相比,所需的代码并不会多多少!