GEF 框架中的设计模式

来源:互联网 发布:什么是电子数据交换 编辑:程序博客网 时间:2024/05/22 13:05

邵 兵, 研究员, IBM 中国研究院
石 立川, 软件工程师, IBM 中国研究院
王 晗, IBM 实习生, IBM 中国研究院

简介: 本文从设计模式的角度出发,通过解析关键应用场景,深层次地介绍了图形编辑框架 (Graphical Editing Framework, GEF) 涉及的大量概念和技术。本文主要涉及 MVC、命令、工厂、观察者、职责链、状态等模式。通过本文,希望能够帮助 Eclipse RCP 开发者更好地理解和应用 GEF 这一框架。



前言

图形编辑框架 (Graphical Editing Framework, GEF) ,是 Eclipse 平台下一个重要的框架,用来从应用模型开发富图形化的编辑器,是 Eclipse RCP 开发者的神兵利器。 GEF 框架涉及大量的概念和技术,有着非常陡峭的学习曲线。本文从设计模式的角度出发,解析 GEF 框架中的关键应用场景,希望能够帮助 Eclipse RCP 开发者更好地理解和应用这一框架。

GEF 通过大量使用设计模式来获取它的灵活性。除了 MVC 模式,GEF 最经常用到的设计模式是命令、工厂、观察者、职责链和状态。

  • 模型-视图-控制器 (Model-View-Controller):MVC 模式被 GEF 用来解除用户界面,行为和表示之间的耦合。模型可以用任意 Java 对象表示,EMF (Eclipse Modeling Framework,Eclipse 建模框架 ) 被普遍使用来构造 GEF 的模型。视图必须实现 IFigure 接口,控制器则必须是 EditPart 类型。
  • 命令 (Command):用来封装对模型的修改,支持 redo、undo 操作。
  • 工厂 (Factory): GEF 框架应用工厂方法模式创建 Figure 应用抽象工厂模式创建 EditPart。
  • 观察者 (Observer):通过观察 EditPart 的激活状态,ConnectionCreationTool 修改待创建连接的连接源。
  • 职责链 (Chain of Responsibility):用来决定请求 (Request) 如何被编辑策略 (EditPolicy) 处理。
  • 状态 (State):对于键盘、鼠标输入,GEF 编辑器通过 Tool 的改变来改变自己的行为。

本文示例代码来自于 GEF 的 3.4.1 版本。

模型-视图-控制器 (Model-view-controller, MVC)

GEF 框架严格遵循模型-视图-控制器模式 (MVC) 。


图 1. GEF 中的模型-视图-控制器 (images/gef_mvc_pattern.jpg)
图 1. GEF 中的模型-视图-控制器 (images/gef_mvc_pattern.jpg) 

GEF 中的模型可以是任意的数据。模型使用一种能在模型改变时通知控制器处理的事件通知机制。这种模型可以由手工来实现,也可以通过 EMF(Eclipse Modeling Framework) 自动生成。而对模型的修改一般由 Command 来完成。

EditParViewer 是 GEF 中的展现视图的地方。常见的 EditParViewer 有两种: GraphicalViewer 和 TreeViewer。GraphicalViewer 主要依靠 Draw2d 中的 Figure 来完成的。开发人员可以通过实现 IFigure 接口来完成复杂图形的设计。对于 TreeViewer 而言,则由 SWT 中的 Tree 和 TreeItem 来完成视图的绘制。

EditPart 对应 MVC 模式中的控制器,它维护着视图与模型的对应关系。在 AbstractGraphicalEditPart 中,createFigure 方法负责创建 Figure 图形,refreshVisuals 方法负责对 Figure 图形进行更新。一般情况下,模型与 EditPart 是一一对应的。模型数据的更新由 EditPart 所安装的编辑策略产生的 Command 来完成。GEF 框架中的常见的 EditPart 实现有三种,分别是 GraphicalEditPart,ConnecitonEditPart 和 TreeEditPart。

命令 (Command)

GEF 不会直接修改模型,而是要求使用命令来做实际的修改。通过命令,实现对模型或模型属性的修改和撤销。这样,GEF 编辑器就自动支持了模型修改的 undo/redo。

Command 类是 GEF 中的一个抽象类,主要实现如下几个方法:

  • execute:这是命令的执行方法,当请求结束并获得 Command 后,GEF 框架 ( 通过 CommandStack) 负责执行此方法。
  • undo:对模型修改后,可以通过 undo 进行撤销。
  • redo:当用户撤销后,能通过 redo 重复上一次的操作。

图 2. Command 相关类图 (images/command.jpg)
图 2. Command 相关类图 (images/command.jpg) 

每个编辑策略都会为请求返回一个命令,不希望处理请求的策略将返回一个 null。GEF 通过一个命令堆栈 (CommandStack) 执行和保存 Command 对象。用户通过命令堆栈可以轻松撤销或重复对模型所做的操作。

工厂 (Factory)

工厂模式是用于将生成对象的步骤进行封装的创建型模式。常见的形态有以下几种:

  1. 简单工厂 (Simple Factory):又叫做静态工厂方法 (Static Factory Method) 模式,但不属于 23 种 GOF 设计模式。简单工厂模式由一个工厂对象决定创建出哪一种产品类的实例。
  2. 工厂方法模式 (Factory Method):定义一个用于创建对象的接口,让子类决定实例化哪一个类。将创建工作推迟到工厂角色的子类去完成。
  3. 抽象工厂模式 (Abstract Factory):声明一个抽象的工厂接口,工厂的多个子类分别创建某一系列的产品。

在 GEF 中,Figure 的创建应用了工厂方法模式。抽象类 AbstractGraphicalEditPart 担当抽象工厂角色,定义了生成 Figure 的抽象方法 createFigure()。具体工厂角色则有 AbstractGraphicalEditPart 的子类担当,负责生成具体的编辑器图形。


图 3. AbstractGraphicalEditPart 类图 (images/factory_method.jpg)
图 3. AbstractGraphicalEditPart 类图 (images/factory_method.jpg) 

EditPart 实例对象的创建则运用了抽象工厂模式。所有的 EditPart 均由 EditPartFactory 的子类负责创建,GEF 自身就提供了 RulerEditPartFactory 和 PaletteEditPartFactory 两个工厂实现。如果用户自定义 EditPart,必须提供相应的 EditPartFactory 类型才能正确创建用户的 EditPart 对象。


图 4. EditPartFactory 相关类图 (images/EditPartFactory.jpg)
图 4. EditPartFactory 相关类图 (images/EditPartFactory.jpg) 

工厂方法和抽象工厂之间的区别在于,工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。

观察者 (Observer)

观察者模式是一种对象行为型模式,它可以定义对象间的一种一对多的依赖关系,当被依赖对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

通过选项栏创建连接时,ConnectionCreationTool 工具被生成用来创建连接,它通过监听鼠标的按下动作来设置连接的连接源,在设置连接源时,通过 addEditPartListener 方法为源 EditPart 添加 deactivationListener 监听。


清单 1. AbstractConnectionCreationTool 类中部分代码
 package org.eclipse.gef.tools;  public class AbstractConnectionCreationTool  extends TargetingTool{     ......     private EditPartListener.Stub deactivationListener = new EditPartListener.Stub() {  public void partDeactivated(EditPart editpart) {     handleSourceDeactivated();  }     };     protected boolean handleButtonDown(int button) {  if (isInState(STATE_INITIAL) && button == 1) {  updateTargetRequest();  updateTargetUnderMouse();  setConnectionSource(getTargetEditPart());  Command command = getCommand();  ((CreateConnectionRequest)getTargetRequest()).setSourceEditPart(  getTargetEditPart());  if (command != null) {     setState(STATE_CONNECTION_STARTED);     setCurrentCommand(command);     viewer = getCurrentViewer();  }  }  if (isInState(STATE_INITIAL) && button != 1) {  setState(STATE_INVALID);  handleInvalidInput();  }  return true;     }     protected void setConnectionSource(EditPart source) {  if (connectionSource != null)     connectionSource.removeEditPartListener(deactivationListener);  connectionSource = source;  if (connectionSource != null)     connectionSource.addEditPartListener(deactivationListener);     }     protected void handleSourceDeactivated() {  setState(STATE_INVALID);  handleInvalidInput();  handleFinished();     }  } 

deactivationListener 是 EditPartListener.Stub 类型的实例,用来观察源 EditPart 的状态。当源 EditPart 的状态由激活变为非激活时,及时通知工具 ConnectionCreationTool 做出停止创建连接的工作。


清单 2. AbstractEditPart 类中部分代码
 package org.eclipse.gef.editparts;  public abstract class AbstractEditPart  implements EditPart, RequestConstants, IAdaptable {     ... ...     protected void fireDeactivated() {  Iterator listeners = getEventListeners(EditPartListener.class);  while (listeners.hasNext())     ((EditPartListener)listeners.next()).  partDeactivated(this);     }     /**      * Adds an EditPartListener.      * @param listener the listener      */     public void addEditPartListener(EditPartListener listener) {  eventListeners.addListener(EditPartListener.class, listener);     }     public void removeEditPartListener(EditPartListener listener) {  eventListeners.removeListener(EditPartListener.class, listener);     }  } 

在这个过程中,EditPart 成为被观察的目标,提供了注册和删除观察者对象的接口。EditPartListener.Stub 类型的实例 deactivationListener 扮演了观察者的角色,在目标 EditPart 的状态变成非激活时,获取更新并通知 ConnectionCreationTool 取消连接的创建。

职责链 (Chain of Responsibility)

职责链是一种对象行为型模式。请求发出后,将在候选对象链 ( 职责链 ) 中进行传递,并有满足条件的对象进行处理。职责链模式降低了请求的发送者和接收者之间的耦合度,允许在运行时对职责链进行动态的增加或修改以增加或改变处理请求的职责。关于职责链模式更详细的描述,请参考 GOF 《设计模式》一书。

在 GEF 中,Tools 或者其他的 UI 解释程序将用户的编辑操作转换为一系列的请求 (Request),比如,用户在选项板 (Palette) 里选择了创建节点工具 (CreationTool),然后在画布区域按下鼠标左键,这时产生在画布上的鼠标单击事件将被 CreationTool 转换为一个 CreateRequest,它里面包含了要创建的对象,坐标位置等信息。

GEF 已经为我们提供了很多种类的 Request,其中最常用的是 CreateRequest 及其子类 CreateConnectionRequest,下图列出了 GEF 中已经实现的 Request.


图 5. Request 子类 (images/request_type_hierarchy.jpg)
图 5. Request 子类 (images/request_type_hierarchy.jpg) 

Editparts 不能直接处理编辑操作产生的 Request,而是通过安装的对应 EditPolicy 来处理。EditPolicy 的主要功能是根据请求创建相应的命令 (Command),而后者会直接操作模型对象。每个 EditPolicy 专注于一个单一的编辑任务或相关任务组,这使得一些编辑操作可以在不同 EditPart 实现共享。EditPolicies 决定了一个 EditPart 的编辑能力。

EditPart 在创建时,调用方法 createEditPolicies() 来安装一些适用的编辑策略。在示例代码中,ConnectionEditPart 安装了两个 EditPolicy。第一个是 ConnectionComponentPolicy,它给 Delete 菜单项所需要的 action 提供删除命令。第二个是 ConnectionEndpointEditPolicy,用来提供连接 (Connection) 转移的策略。


清单 3. ConnectionEditPart 类中部分代码
 class ConnectionEditPart extends AbstractConnectionEditPart         implements PropertyChangeListener {     ...     protected void createEditPolicies() {         installEditPolicy(EditPolicy.CONNECTION_ROLE, new ConnectionEditPolicy() {             protected Command getDeleteCommand(GroupRequest request) {                 return new ConnectionDeleteCommand(getCastedModel());             }         });         installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE,                           new ConnectionEndpointEditPolicy());     }    ...  } 

请求提交到选定的 editpart 后,通过 EditPolicy 的职责链进行处理。从第一个 EditPolicy 开始,链中收到请求的 EditPolicy 确定是否可以处理它,否则转发给链中的下一个 editpolicy。EditPolicy 的声明顺序决定了请求被传递的顺序。多个编辑策略可以收到请求,返回 Commands 作为响应,这些 Commands 以链的方式组织在一起。示例代码描述了 AbstractEditPart 中的职责链工作方式。


清单 4. AbstractEditPart 类中部分代码
 package org.eclipse.gef.editparts;  public abstract class AbstractEditPart  implements EditPart, RequestConstants, IAdaptable  {     ......     public Command getCommand(Request request) {  Command command = null;  EditPolicyIterator i = getEditPolicyIterator();  while (i.hasNext()) {             if (command != null)  command = command.chain(i.next().getCommand(request));     else  command = i.next().getCommand(request);  }  return command;     }  } 

状态 (State)

同职责链模式一样,状态 (State) 也是一种对象行为型模式。状态模式允许一个对象在其内部状态改变时改变它的行为。上下文 (context) 把状态相关的行为委托到状态对象上。对象通过上下文引用不同的状态对象,在运行时根据状态改变它的行为。关于状态模式更详细的描述,请参考 GOF 《设计模式》一书。

在 GEF 的编辑器中,用户在选项板 (Palette) 切换工具可以改变编辑器的状态,从而修改编辑器的行为。例如,对于鼠标按下事件,编辑器在激活选区工具和激活创建工具下的行为是截然不同的。现在,我们就来看一下 GEF 编辑器是如何根据当前选中的 Tool 来改变行为的。

在每个 GEF 的 Editor 里,都需要有一个 EditDomain 的存在。EditDomain 类似于 GraphicalEditor 的执行上下文环境,维护着 GEF 中的命令栈、负责事件通知等。在 EditDomain 中,通过 setActiveTool 可以设置当前处于 Active 状态的 Tool。

EditDomain 类维护一个表示鼠标和键盘输入的工具对象 ( 一个 Tool 接口实现类的实例 )。EditDomain 类将所有与视图输入相关的请求委托给这个工具对象。EditDomain 类使用 Tool 接口实现类的实例来执行特定于视图输入的操作。在状态模式中,EditDomain 对应上下文环境,工具 (Tool) 对应状态。一旦 Active Tool 改变,EditDomain 对象就会改变它所使用的工具对象。


图 6. Tool 继承层次图 (images/tool_hierarchy.jpg)
图 6. Tool 继承层次图 (images/tool_hierarchy.jpg) 

需要注意的是,上图关于 Tool 的继承层次部分并不是严格按照 GEF 框架进行描述,本文作者为了描述方便做了某种程度的简化。具体层次请参考 GEF 框架代码。

示例代码描述了 EditDomain 是如何将与视图输入相关的请求委托给它的 Tool 实例 activeTool。


清单 5. EditDomain 类中部分代码
 package org.eclipse.gef;  public class EditDomain {     ......     private Tool activeTool;     private void handlePaletteToolChanged() {            PaletteViewer paletteViewer = getPaletteViewer();                if (paletteViewer != null) {                ToolEntry entry = paletteViewer.getActiveTool();                if (entry != null)  setActiveTool(entry.createTool());     else  setActiveTool(getDefaultTool());  }     }     public void setActiveTool(Tool tool) {  if (activeTool != null)  activeTool.deactivate();  activeTool = tool;  if (activeTool != null) {     activeTool.setEditDomain(this);     activeTool.activate();  }     }  } 

工具会执行某些操作,这些操作可能包括:

  • 要求 editparts 显示或隐藏反馈
  • 从 editparts 获得命令
  • 在命令栈执行命令
  • 更新鼠标光标

后记

GEF 出现的模式远不止我们列出来的这么多。我们只是列出了对 GEF 框架理解有帮助的一些模式。本文作者从事 Eclipse RCP 开发多年,通过这篇文章,希望能将在 GEF 中体会到的一些设计思想与大家分享。

原创粉丝点击