软件开发是一件很痛苦的事情,大量新的需求与现有软件结构之间的冲突是造成软件难以维护的一个主要原因。很
多软件在刚开始时可能结构很好,便于扩充,但随着软件功能的丰富,代码的增加,可能会造成结构也变得不再适应
新需求的发展。对于编辑器软件来说,这样的问题更为明显。那么NewEdit就是想在如何构造一种灵活的结构方面进
行的一种有益地探索,同时它会发展成为一个专业、稳定地编辑器软件。因此,NewEdit的设计核心就是,如何构造一
种灵活的架构,使得新功能的增加更为方便和易于维护。
NewEdit使用的软件基础是Python+wxPython。因此,NewEdit可以充分地享受Python的动态特性所带来的好处,同
时由于使用了wxPython,还可以实现跨平台的运行。
这样,NewEdit的设计思想就是充分利用Python语言的动态特性,构造易于扩充而又灵活的软件体系。具体的实现技术
就是全部采用Mixin和Plugin的设计与开发方法,更为通俗地说法就是分布式类编程技术,或狭义地理解为插件编程
(但分布式类编程与插件编程是不同的,分布类编程包括Mixin和Plugin,而插件编程一般只包括Plugin,因此插件编程
的功能扩展是有局限的)。有时为了简单,我会叫分布类编程为Mixin,具体到技术细节时,会细分Mixin和Plugin。
比如,软件刚开始还是一个雏形,可能只有少数的功能。因此,会有一些实现了基础功能的类。当这些基础功能稳定,同
时有新的需求时,这时需要对这些类进行扩展。扩展基本分为两种:
- 新的方法、属性的实现
- 增加对新方法、属性的调用入口
对于无法通过新的方法、属性实现的可能还需要增加新的类。此时可以对现有的类采用第二种扩展从而实现原有类与新
类之间的联系。
知道了扩展方式,那么在哪里去实现呢?对于第1种扩展,我的方法是,基本不动原来的类所在的文件,而是创建一个
新的文件,在这个新文件中实现新的方法、属性。然后通过一种机制把新的方法和属性与目标类进行融合,同时这种融
合是在运行时才实现的。这样,一个类的功能是随着开发的进行,不停地进行扩展。由于新的扩展并不是直接在原来的
文件中进行地修改,并且在一个文件中可以同时对多个类进行扩展,因此相对容易地知道本次扩展都做了哪些工作,对
于软件的维护非常方便。这些扩展会通过一种机制,在软件运行时自动与目标类进行融合,不需要做额外的工作。对于
这种扩展,我叫它为Mixin扩展。对于第2种扩展,我的方法是,在原来的类中增加相应的调用接口,同时在Mixin扩展
中增加对调用接口的具体实现,从而通过调用接口将新增的功能与原类联系起来。对于这种扩展我叫它为Plugin扩展,
也可以叫做插件扩展。
因此Mixin扩展的开发主要是对目标类进行功能扩展,如增加新的类方法,修改类方法,增加新的属性等。对于Plugin
扩展主要是在目标类中需要寻找合适的调用位置,定义相应的调用接口。同时实现具体的Plugin实现代码。
这里还存在的一个要考虑的问题是,Plugin扩展要根据具体情况考虑是否需要实现。首先,实现Plugin是因为新增的方法、
属性的使用不会自动完成,需要一个调用点。如果你扩展的方法属合某种事件机制的调用规则,那么可以不用实现Plugin接
口,否则就要实现相应的Plugin接口。
下面举一个例子,来看一下两种扩展的实现。其中有些具体的细节还没有涉及,后面会讲到:
class MainFrame(wx.Frame):
def __init__(self, parent, title='Test'):
wx.Frame.__init__(self, parent, -1, title=title)
上面是一个窗体,这可能就是我们的雏形。现在我们想在上面增加一个按钮,并对按钮事件进行响应。我们可以直接在
MainFrame上修改。如果这个类很简单当然可以,但如果这个类很复杂,并且一次改动不止要改动一个地方,就不是一件
容易的事情了。现在我们使用Mixin的方法来做。
如果想使一个类可以实现分布类编程,那么需要使它成为Mixin类,我定义它为Slot类(槽类),那么它就是一个目标
类,将被进行Mixin扩展。改造MainFrame类如下:
import Mixin
import wx
class MainFrame(wx.Frame, Mixin):
__mixinname__ = 'mainframe'
def __init__(self, parent, title='Test'):
self.initmixin()
wx.Frame.__init__(self, parent, -1, title=title)
改造很简单,使得MainFrame有一个Mixin的基类,定义一个类属性__mixinname__,如: mainframe,在__init__中调
用self.initmixin()。这样,我们就把一个MainFame改造成了一个槽类。
创建一个新文件,将用于实现具体的扩展。内容如下:
import Mixin
import wx
def init(win):
win.ID_CLICK = wx.NewId()
win.btnClick = wx.Button(win, win.ID_CLICK, 'Click Me')
wx.EVT_BUTTON(win.btnClick, win.ID_CLICK, win.OnClick)
def OnClick(win, event):
print 'Click Me'
win.Close()
可以看出我们在新文件中实现了两个方法,一个用来创建一个按钮,并且设置当单击按钮时,调用OnClick事件处理函数。另一个就是
OnClick事件处理函数。如果我们不使用Mixin扩展,那么这两个函数可能是这样:
import wx
class MainFrame(wx.Frame):
def __init__(self, parent, title='Test'):
wx.Frame.__init__(self, parent, -1, title=title)
self.ID_CLICK = wx.NewId()
self.btnClick = wx.Button(self, self.ID_CLICK, 'Click Me')
wx.EVT_BUTTON(self.btnClick, self.ID_CLICK, self.OnClick)
def OnClick(self, event):
print 'Click Me'
self.Close()
可以看出,OnClick其实是MainFrame的成员方法,而init应该位于__init__中,是__init__中的一段处理。如果使用Mixin应该如何写呢?
结果是:
import Mixin
import wx
def init(win):
win.ID_CLICK = wx.NewId()
win.btnClick = wx.Button(win, win.ID_CLICK, 'Click Me')
wx.EVT_BUTTON(win.btnClick, win.ID_CLICK, win.OnClick)
Mixin.setPlugin('mainframe', 'init', init)
def OnClick(win, event):
print 'Click Me'
win.Close()
Mixin.setMixin('mainframe', 'OnClick', OnClick)
setPlugin是Plugin设置方法,它将把init与槽类('mainframe')的某个调用点相关联,由槽类的实例进行调用。而setMixin是Mixin
设置方法,它将把OnClick插入到槽类中,成为槽类的一个成员函数。
同时我们还需要修改MainFrame,增加一个调用点:
import Mixin
import wx
class MainFrame(wx.Frame, Mixin):
__mixinname__ = 'mainframe'
def __init__(self, parent, title='Test'):
self.initmixin()
wx.Frame.__init__(self, parent, -1, title=title)
self.callplugin('init', self)
这样,槽类与Mixin文件都准备好了。如何才能将它们真正关联起来呢?很简单:在启动程序中将Mixin文件作为一个模块导入即可。这样真
正的关联会等到运行时,首先通过导入先将所有Plugin和Mixin搜集起来,然后再等到创建实现时,通过self.initmixin()方法真正实现
关联。并且这种操作只会执行一次。
因此Mixin与Plugin的区别就是:Mixin实现类的新方法和属性,而Plugin实现调用点的具体处理。更简单地理解:Mixin是对未知的扩展,而
Plugin是对已知的扩展。
这里所讲的Mixin部件包括:将成为槽类的类成员方法的方法,和成为槽类的类属性的属性。每一个Mixin部件都要在实现完成后,调用Mixin
模块的setMixin方法来声明此Mixin部件将与哪一个槽类名进行融合、融合后的名称、及部件的对象实例。方法声明为:
setMixin(slot_class_name, binding_name, instance)
- slot_class_name
- 为槽类名称,即对应的槽类的__mixinname__的值
- binding_name
- 为融合后的名称
- instance
- 为Mixin部件的对象
举例如:
def OnClick(win, event):
print 'Click Me'
win.Close()
Mixin.setMixin('mainframe', 'OnClick', OnClick)
槽类名为:'mainframe'
绑定后的名字为:'OnClick',这样,当实现融合后,在槽类的类方法为:OnClick。你也可以改名,这里没有改动。如果有两个Mixin部件的融合
名是相同的,对于:
类方法
后融合的方法会替换前面融合的方法
属性
根据属性的不同有不同的处理:
列表,tuple
将新的内容增加到已经存在的内容之后
字典
将新的内容与已经存在的内容进地合并,如果已经存在某个键,则新的值会替换旧的值
其它
新值将替换旧值
Mixin部件实例:即为此模块中的OnClick对象
在实现Plugin部件之前,应该在相应的类中加入调用点,即增加:self.callplugin()或self.execplugin()的调用。然后再在Mixin文件中定
义相应的Plugin部件。callplugin和execplugin是Mixin模块的方法,两者很象,区别就是callplugin不处理返回值,而execplugin会处理返
回值。Plugin部件均为方法或函数,而且一个调用点可以对应多个Plugin部件。
方法声明为:
callplugin('plugin_call_name', *args, **kwargs)
execplugin('plugin_call_name', *args, **kwargs)
可以看出,每一个调用点都有一个字符串定义的名字,然后是相应的参数。
这里先强调一点,所有plugin部件会自动根据槽类、plugin_call_name进行分类,同时还会根据执行setPlugin时指定的优先级进行排序。因此,
plugin部件最终会组织为一棵森林,每棵对就是一个槽类,每个槽类有若干个调用点,每个调用点对应一个按优先级排序的列表,列表中的每一个
元素就是一个plugin部件--方法。
对于callplugin,它会把调用点对应的方法列表中的所有方法排优先级顺序自动执行一遍,同时传入*args, **kwargs参数。如例子中为:
self.callplugin('init', self)
plugin_call_name为'init',传入参数为自身的实例对象: self。
对于execplugin,它也会按方法优先级进行处理:
- 取出一个方法,调用它,同时传入相应的参数
- 判断返回值,如果为假,则跳到第一步;如果为真,则结束,同时返回结果
因此,这时函数的返回值就非常重要了。如果某个处理对其它处理没有影响,那么你不用返回任何值(这时Python会返回None),或返回假值。如果
某个处理执行后,不再允许其它的处理继续,则要返回一个真值。在它后面的所有函数都不会再进行处理了。这样优先级和返回值就要仔细地考虑与
调整。
首先定义一个函数,它的参数与相应的调用点的参数一致。然后定义完毕后,调用setPlugin来声明此函数将与哪个槽类的,哪个调用点相绑定。
setPlugin的函数声明为:
setPlugin(slot_class_name, binding_name, instance, priority, nice)
- slot_class_name
- 槽类名称。即为对应的槽类的__mixinname__的值
- binding_name
- 绑定后的名称
- instance
- Plugin部件的对象实例
- priority
- 在函数列表中的顺序值。因为相同槽类的相同调入点对应的Plugin部件会组织为一个函数的列表,因此这个值代表函数的排列顺序。但它是一个
大概的级别,一共有三个级别:HIGH, MIDDLE, LOW。相同的级别排列序列是由执行setPlugin的顺序来决定的。缺省为MIDDLE。
- nice
- 定义最终优先级数。priority定义了三级分类,它们会被处理为某个确定的优先级数:HIGH=100, MIDDL=500, LOW=900。而nice则是直接设定
这个优先级数。此参数缺省为-1,表示未定义,如果它的值 >= 0,则一个Plugin部件的优先级将使用此nice值来确认,而不是使用priority来确定。
因此,此参数与priority是互斥的,只能有一个生效。
为了定义一个Plugin部件的执行顺序,一种是使用优先级参数来设定,还有一种方法是设定多个调用点,将Plugin部件分派到不同的调用点上。由于调用
点是有执行顺序的,因此也决定了Plugin部件在大的范围上的执行顺序。