让程序支持脚本

rel="File-List" href="file:///C:%5CDOCUME%7E1%5Csech%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">

1.  问题提出

在公司经历过几个GUI项目,总结一下,发现GUI程序有以下通用功能。

n         撤销(Undo)/重做(Redo)

n         复制/粘贴

这两个问题基本上是每个GUI程序都要具备的功能,但是由于它们与程序内部的模型对象集(每个程序都会由一个主要的模型集合,各功能都是围绕它进行的)紧密相关,如果不注意,这里也是最容易出现问题的地方。

例如,复制时,有两种选择:保存被复制对象的引用;使用原型模式得到对象的副本,但是由于模型对象之间存在依赖关系,所以这个模型常常也保存了其它对象的引用。如果用户按照下面的操作:

     拷贝对象

     删除对象

     其它操作

     粘贴对象

删除对象时,由于要实现Undo/Redo功能,根据命令模式,将它实现为一个命令对象,并将该命令对象放入Undo列表中。这样,在粘贴对象时,剪切板中即使保存的是对象的引用也没有什么问题。但是,为了限制内存的使用量,Undo/Redo命令列表的长度一般是加了限制的;另外,按照下面这个序列操作:

     添加对象   à添加对象命令添加到Undo列表中

     拷贝对象   à拷贝动作不是撤销动作范围内

     Undo       à模型对象会从模型对象集合中移除,同时,将添加对象命令放入Redo列表中

     其他操作   àUndo列表中添加新的命令,同时清空Redo列表,此时①添加的对象被释放。

这两中情况都导致一个结果:拷贝的对象被删除了。拷贝时再访问时出现异常(也许你说捕捉一下异常,然后忽略这个操作。但是这样实现并不大好,因为不知道这个对象到底是由于什么原因被删除的)。

前面提出的问题,如果在C++程序中,会因为死引用出现异常(引用已删除的对象),它可以通过使用引用计数技术来避免,但是引用计数也有一个需要在设计上避免的问题,那就是循环引用问题。关于引用计数技术,这里不多阐述,有兴趣的朋友可以到网上查查学习学习。如果是Java/C#程序中,虽然不会出现死引用问题,但是会出现错误引用问题——Java/C#的自动垃圾回收机制虽然能够保证被引用的对象一定存在,但是它不能保证该对象和其它对象之间关系的正确性(例如,它引用了不该引用的对象)

 

本文以这个问题为引子,研究一下让程序支持脚本的方案。

2.  让程序支持脚本

我们常常感叹于Excel/Word的宏脚本功能的强大。但是我们自己的程序实现这种功能也未尝不可。这种宏脚本功能不仅仅是一种软件机能,如果结合前面的问题考虑,我们会发现这种设计方式能解决很多问题。宏脚本有一个很明显特点,就是它与程序是分离的,这样脚本里就无法直接访问到内存中的对象,如果程序能够支持脚本的话,上面提的问题也就迎刃而解了。

下面就要讨论一下,如果程序能够支持脚本的话,需要满足什么条件,设计的时候就要满足这些条件。

本章讨论操作对象的类应该具有的特点和原因。

说明相关问题时,以下面的软件实现为例。

     图形编辑软件

     图形对象可以是矩形、正方形、圆形等

     图形对象是图形对象管理器(集合)管理的

     有回退/重做功能,使用命令模式实现

     有复制粘贴功能。复制的时候,剪切板保存的是对象的引用,不是对象的副本。

 

2.1.      脚本引擎

如果是C/C++程序,并且可以使用COM的话,那么可以参照下面的网址:

http://msdn.microsoft.com/zh-cn/magazine/cc302278(en-us).aspx

http://support.microsoft.com/kb/221992/zh-cn

另外,也可以使用嵌入式脚本语言,例如LuaPythonRuby等等。

 

它是通过Com组件实现的,而且已经实现了VBScriptJavaScript语言的引擎。

 

如果是Java程序,可以使用BeanShell,它可以直接访问Java内部的类和对象。

如果是C#程序,可以参照一下下面的例子。

http://www.cnblogs.com/cuihongyu3503319/archive/2008/08/06/1262068.html

2.2.      确定脚本可访问的接口(类、函数)

脚本可访问的类和函数和脚本要实现的机能有关,不同的程序需要做不同的决定。

对于C++程序,如果使用Lua脚本的话,有很多工具能够将C++源代码的类转换为Lua中的数据结构。

如果是Java/C#程序,则免除了这个痛苦,因为这些脚本是通过Java/C#的反射机制实现的,天生就支持。

向脚本提供的类和函数根据功能可以分为以下几种:

l         关系对象的访问。例如从一个模型对象访问它的父对象系统设定对象等等。

l         操作对象。对应脚本中要操作的对象和属性和操作。

对于前者,根据实际情况进行设计,它的功能和一个简单的函数相当,不需要讨论。

对于后者,也是很容易确定的。大部分GUI程序都会把操作对象抽象抽象出来,也就是说,都包含一个操作对象集合。这个对象也就是脚本能够操作的对象。

2.3.      可永久化句柄

基本上,所有的GUI程序都会有永久化机能(文件的保存和读取)。文件里面,操作对象是主要的存储内容。这里对象关系永久化是一个常见并难于解决的问题。

如果不明白对象关系永久化的问题,请看下面的例子。

假如程序里面有ABC 3个对象,他们的关系如下:

     A包含C的引用

     B包含C的引用

按照面向对象思想,保存的文件内容可能如下。

<A>

   <property>…</property>

   <ref_obj>B</ref_obj>

</A>

<B>

   <property>…</property>

   <ref_obj>B</ref_obj>

</B>

<C>

   <property>…</property>

</C>

读入的时候,序列化A对象的时候,是不能序列化C对象的,所以A里面保存的C实际上是对象的标识。所以要实现对象关系永久化的话,需要为对象设定一个唯一的标识符(句柄)。这个句柄可能是在读入文件的时临时建立的,也可以是永久保存的。

对于临时建立的关系,只影响了系统的一个功能,对其他功能没有影响。

对于永久保存的关系,在程序内部保存的对象的引用也可以替换为对象的句柄,这样,当引用该对象的时候,就可以通过该句柄判断对象是否有效(在集合内有效,集合外无效)。这个功能很有用,例如,当一个矩形从图形对象管理器中删除的时候,如果执行图形拖动命令的重做,那么该命令中保存的对象的引用应该被认为是无效的;如果执行粘贴功能,那么该对象的引用是有效的。对象有了唯一的句柄,可以让很多问题变得简单。它的缺点是需要为句柄和对象引用间建立一个映射,可以使用Hash表来实现。因此访问该引用时,会降低一些性能。

对象的句柄可以简单的使用一个递增的整型值即可。如果考虑句柄的可读性(例如,添加对象的类型信息),可以添加一些额外的信息。

句柄的添加,需要向程序中添加一个对象工厂模块,它用来管理和创建所有的图形对象。它与图形对象管理器的不同之处在于,在对象工厂中,可以使用句柄来访问到所有在内存中存在的对象——即使它在对象管理器中不存在。

2.4.      不在命令中包含对象的直接引用

如果没有对象句柄机制,一个移动图形的命令可能会实现成下面这个样子。

class MoveCommand

{

   protected Object obj;

   protected int x;

   protected int y;

   public void Execute(Object obj, int x, int y)

   {

       this.obj = obj;

       this.x = x;

       this.y = y;

       Obj.Move(x, y);

   }

   public void Undo()

   {

      this.obj.Move(-x, -y);

   }

}

结合对象句柄机制,不包含对象直接引用的命令就变成这样。

class MoveCommand

{

   protected string objHandle;

   protected int x;

   protected int y;

   public void Execute(string objHandle, int x, int y)

   {

       this.objHandle = objHandle;

       this.x = x;

       this.y = y;

       Object obj = ObjectFactory.FromHandle(objHandle);

       if(obj)   Obj.Move(x, y);

   }

   public void Undo()

   {

      Object obj = ObjectFactory.FromHandle(objHandle);

      if(obj) this.obj.Move(-x, -y);

   }

}

这样实现,可以使代码变得更安全。又因为对象句柄是可以永久化的,那么在脚本中来执行这个命令也就变得可能。而且,对于打开保存过的文件,使用了对象句柄的脚本仍然可以正常工作。

2.5.      为用户提供的输入用命令来实现

为用户提供的输入指的是软件引导用户通过一组键盘和鼠标动作完成对对象的修改,例如拖动一个图形、在图形的属性对话框中修改图形的属性、甚至还可以包括像SDLScenarioEditor中的切换Tab页的操作等等。这样做的好处有。

l         明确用户的输入。这样做可以使设计人员清楚自己的设计是否有遗漏;还可以使编码人员清楚在该处理中应该做什么;另外,也有助于式样跟踪和ST

l         将界面中的输入处理与对模型的处理分离开。如果不强调用命令实现的话,很容易将界面消息处理和对模型的处理混合到一起,导致逻辑比较混乱、就容易出现bug

2.6.      使用MVC模式

也就是使用MVC模式设计对象。关于MVC模式,网上有很多解释,这里不再赘述。

用户操作是从界面开始进行的,即界面操作模型属性变更界面更新而脚本操作却是从模型开始的,即模型属性变更界面更新。所以如果让程序支持脚本,那么就必然要实现一个MVC模式的设计。而且为了能够支持脚本,可以很容易的分清楚什么处理应该放到VC(通常是放到一起的),什么处理应该放到M里。下面用例子说明。

例如设定一个图形的属性,是通过一个属性对话框来完成的。通常为了封装对象的功能,是要把属性对话框封装和对象关联起来,这样的封装就有可能放到一起。这样,就把VCM混合起来,代码有可能像下面这样。

Class Rectangle : public Shape

{

    Protected int height;

    Protected int width;

    Void DoProperty()

    {

        If(propertyDlg.show() == ID_OK)    //V(view)部分

        {

            height = propertyDlg.height;   //M(Model)部分

            width = propertyDlg.width;

        }

    }

}

这样做,虽然面向对象的封装要求满足了,但是独立性和扩展性却不好。

如果你想让你的程序支持脚本,那就逼迫你必须将VM分开,再加上一个C。代码就会变成下面这样。

Class RectangleView      //V

{

    Rectangle rect;

    Void DoProperty()

    {

        RectangleModel m = rect.GetModel();

        If(m && propertyDlg.show() == ID_OK)

        {

            RectPropCommand cmd = new RectPropCommand();

            cmd.height = propertyDlg.height;

            cmd.width  = propertyDlg.width;

            cmd.Execute();

        }

    }

}

 

Class RectangleModel    //M

{

    Int height;

    Int width;

}

 

Class Rectangle    //C

{

    RectangleView view;

    RectangleModel model;

    Model GetModel()

    {

        Return model;

    }

    Void DoProperty()

    {

        View.DoProperty();

    }

}

这样一来,脚本中属性设定的语句,例如Rectangle.SetBounds(100, 200)就可以直接对RectangleModel操作,不需要经过View的对话框了。

需要说明的是,能在脚本中访问的对象,必须用MVC模式设计来设计。但是反过来说,MVC模式的使用会越来越提高设计能力。

3.  总结

本文试图研究让软件支持脚本需要具备的特点。但是绝大部分的软件都不需要支持脚本,所以看起来并没有什么必要。但是如果程序是按照上面的规则来设计的话,却应该能够使软件的设计变得更好一些。我们还可以利用这个机能做一些额外的事情,例如:

l         将命令字符串化作为Log输出。

l         可以分别开发MVC模式中的MVC

l         可以将一些测试提前。例如M开发完后,可以使用这个脚本机制模拟一些用户操作的场景测试模型实现的是否正确;VC开发完后,也可以通过假的命令来测试一下VC的显示和控制流程是否正确。

我觉得,软件在系统设计的时候,一部分是业务相关的设计,需要根据软件的需求进行不同的设计;另一部分是纯粹的软件设计技术,它与业务是无关的,甚至有些技术与语言都是无关的。例如设计时是采用面向对象还是面向过程、是不是要使用设计模式等等。

设计的内容通常有以下几点。

l         保证机能(实现所有的式样)

l         模块的独立性和程序的扩展性

模块的独立性和程序的扩展性是非常难定义的,它是一个抽象概念,既然没有一个明确的定义,当一个设计完成后,这一条是很难评价的。但是我觉得,设计时出了要考虑以上两点以外,还要考虑以下问题。

l         是否避免了常见的技术问题。例如C++中内存是非常难处理的,如果在设计上能够让开发人员使用智能指针的话,这个问题就能变得简单得多。

l         是否建立了一种机制确保开发人员出现错误的几率尽可能少。

总之一句话,设计不仅要确保机能正确性,模块独立性,还要尽量避免问题。重构是一种好技术,但是在设计时却不能指望出现bug后进行重构。

也许这些想法会导致过分设计,但是我觉得,只要机制简单,设计过分一点也没有什么问题。软件的设计方法越来越丰富,我现在能说上来的有面向过程设计、面向对象设计、设计模式的融入、面向服务设计等等。每一个新的设计方法都要比旧的复杂得多,而且有些需要引入新的技术,例如面向对象设计引入了类的概念,所以必须要引入面向对象语言;面向服务设计则是在软件部署上做了文章。所以我觉得,提高设计水平需要方方面面的知识,而不能局限于语言本身,更需要一种勇气去尝试接纳新技术,更重要的,可能还是一种交流体制能促使大家不断的提出问题、讨论、试验、推广自己独有的技术。

最后,由于篇幅有限,本人的能力也有限,暂时对这个框架只想到了这些问题,还需要进一步研究。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值