跳转到主要内容

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

所有提交的信息确保安全。

  • 关闭 [x]

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

所有提交的信息确保安全。

  • 关闭 [x]

Presentation Model 模式在 SWT 程序中的应用

对富客户端应用的单元测试

鲁 韵涛, 软件工程师, IBM
鲁韵涛,10 年以上的研发经验,目前就职于 IBM 中国研发中心。

简介: 单元测试作为开发过程中一个必要组成部分,已得到广大开发人员的认同。针对富客户端应用程序的单元测试,尤其是对 UI 状态及控制逻辑的测试,一直是一个难题。Presentation Model 模式正是一种解决富客户端应用耦合与代码可测试性问题的有效途径。本文从富客户端应用单元测试遇到的问题入手,介绍了 Presentation Model 模式,并结合实例,详细讲解如何在 SWT 应用中使用 Presentation Model 模式,从而降低 SWT 应用单元测试的难度。

发布日期: 2012 年 1 月 12 日
级别: 初级
访问情况 : 1547 次浏览
评论: 


富客户端应用中单元测试的难题

富客户端应用以丰富的界面展现及良好的交互性,重新回到了人们的视野中。但客户端的事件驱动性,在带来快速交互与反馈的同时,也带来了设计上的复杂性。当 Web 应用开发常用的 MVC 模式应用于富客户端应用时,总会觉得无法得心应手。

设计一个包含 UI 的客户端,通常需要考虑三个方面的问题:

  • UI 的状态
  • 逻辑
  • 数据

这里我们说的“逻辑”是指 UI 控制逻辑,而不是业务逻辑。在富客户端应用中,这三个基本元素往往是紧密耦合的,如图 1 所示。


图 1. 富客户端应用结构
图 1. 富客户端应用结构

另外,UI 组件的创建和初始化需要环境的支持,控制逻辑与状态紧密耦合,事件的触发需要人为的操作,因此测试 UI 组件一般使用自动化工具来模拟键盘和鼠标操作,无法进行单元测试,如单独测试控制逻辑。这样会加重功能测试负担,并且会与 UI 的结构 / 层次紧密耦合,轻微的 UI 修改也会使测试用例失效。更重要的是,这样的测试无法做到单元测试的细粒度,代码分支和条件很难覆盖全。这也正是富客户端单元测试的难点。为了解决这个问题,我们需要一个更好的富客户端应用设计。


Presentation Model

2004 年 Martin Fowler 提出了 Presentation Model ( 以下简称 PM 模式 ) ,用于改善用户界面设计中状态、逻辑及数据对 UI 组件本身的耦合。使用 PM 模式的富客户端应用设计如图 2 所示。


图 2. 使用 PM 模式的富客户端应用设计
图 2. 使用 PM 模式的富客户端应用设计

从图 2 可以看出,PM 模式中增加了一种新的对象—— View Model,将 UI 组件中保存的状态、逻辑和数据抽出,在 View Model 中实现;同时将 UI 组件中的状态和数据与 View Model 中保存的状态和数据进行同步复制。PM 模式中一个很重要的理念是 View Model 对 UI 组件没有任何的依赖,甚至根本不需要知道 UI 组件的存在。UI 组件对 View Model 是感知的,可以紧密耦合。

View Model 是一个 Pure Object,在 Java 语言中可以是一个 Java Bean。与其他 UI 层的设计模式不同,PM 模式将彼此相关的 UI 状态及状态的控制逻辑封装在一起,并从与环境或平台相关的 UI 控件中抽出。这部分代码脱离 UI 控件后,可以利用依赖注入将其与外部环境隔离,这时 View Model 就成为了一个低耦合度,并且非常容易测试的对象。

对 View Model 的测试不仅是控制逻辑上的测试,同时可以对状态进行测试验证,而这往往是一般 UI 测试所做不到的。可以想象一下,当用户在界面输入了一些非法的数据,保存按钮这时应当为 Disable 状态。这样的测试对于一个富客户端应用来说非常必要,但很难实现自动化,利用 PM 模式,这样的测试将变成一个简单的对 View Model 状态的 Assert。

更多 Presentation Model 模式的介绍请参考 这里


Data Binding — PM 模式中的粘合剂

我们已经了解了什么是 PM 模式,但是从上面的介绍可以感觉到,PM 仅仅是一种设计上的理念,在实现时还有诸多的困难,比如如何复制状态和数据等问题。

在以往的实践中,这往往是一个需要大量重复代码,从一个对象向另外一个对象拷贝数据的过程。这样的代码需要监听 UI 控件及模型对象的事件,并触发数据复制的过程。实现这个过程非常枯燥、容易出错,而且与 UI 控件紧密耦合 (UI 事件的监听 ),难以测试。因此,PM 模式的实现不是仅仅在设计上需要改变,而且需要基础的服务来简化实现的难度,这个服务就是 Data Binding ---- 数据绑定。

数据绑定可以在数据发生变化时,将其复制到目标数据对象中。这个复制过程可以是单向的,也可以是双向的。SWT 提供了一组针对 Java Bean 与 SWT/JFace 控件的数据绑定服务。其实 SWT 中的数据绑定服务并不是专门针对 SWT/JFace 实现的,这个实现可以用于任何两个 PO 对象的绑定 ( 后面的“更近一步”的例子中,我们将会看到应用 ) 。SWT/JFace 数据绑定的教程请参见 JFace Data Binding - Tutorial

下面我们参考一个简单的例子来了解如何用 SWT 的数据绑定来实现 PM 模式。

我们的例子很简单,需要实现一个购物支付的界面,需求如下。

1. 可以选择用现金方式支付或者会员方式支付。

2. 当选择现金时,需要显示出支付的金额,实际用户支付的金额,及找零的金额。

  • 其中用户实际支付的现金金额为输入项,其他为只读项。并且当输入实际支付金额时,需要及时显示计算出的找零数。

3. 当选择会员支付,并且会员预存余额足够支付时,显示会员预存剩余的金额及需要支付的金额。

4. 当选择会员支付,并且会员预存余额不足以支付时,显示会员预存余额,需要支付的总金额,同时显示现金需要支付的金额,用户实际支付的现金金额,及需要找零的金额。

  • 其中用户实际支付的现金金额为输入项,其他为只读项。并且当输入实际支付金额时,需要及时显示计算出的找零数。

从需求中可以发现,无论选择何种支付方式,现金支付的部分 UI 及逻辑是可以重用的。另外,其中有部分数据需要实时的计算及回显。

用 SWT 实现这个应用很简单,这里我们不再详细讨论。我们来看一下用 PM 模式的实现。下图是业务模型的定义。


图 3. 业务模型
图 3. 业务模型

SWT 的 Data Binding 的实现,需要绑定的双方在数据发生变化时能够提供相应的事件。SWT 及 JFace 中的 UI 控件实现了数据及状态变化的事件定义,已经符合 Data Binding 的要求。为了能够实现双向的数据绑定,即 UI 中的数据与业务模型中的数据双向复制。我们需要业务模型类实现基类 NotifyPropertyChanged。NotifyPropertyChanged 是一个自定义的工具类,是为了更简单的实现数据变化通知,利用了 SWT Data Binding 提供的 PropertyChangeSupport 类来完成事件管理。其定义如下:


清单 1. 自定义数据变化通知类 NotifyPropertyChanged
				
 package lucifer.swt.databinding.util; 

 import java.beans.PropertyChangeListener; 
 import java.beans.PropertyChangeSupport; 

 public class NotifyPropertyChanged { 
 protected PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport( 
			 this); 

 public void addPropertyChangeListener(PropertyChangeListener listener) { 
 propertyChangeSupport.addPropertyChangeListener(listener); 
 } 

 public void addPropertyChangeListener(String propertyName, 
 PropertyChangeListener listener) { 
 propertyChangeSupport.addPropertyChangeListener(propertyName, listener); 
 } 

 public void removePropertyChangeListener(PropertyChangeListener listener) { 
 propertyChangeSupport.removePropertyChangeListener(listener); 
 } 

 public void removePropertyChangeListener(String propertyName, 
 PropertyChangeListener listener) { 
 propertyChangeSupport.removePropertyChangeListener(propertyName,listener); 
 } 
 } 

并且在业务模型属性 setXXX 方法中增加 firePropertyChange 方法调用。比如在 CashPayment 类的 setChange 方法定义如下:

 public void setChange(double change) { 
 propertyChangeSupport.firePropertyChange(“change”, this.change, 
 this.change = change); 
    } 

根据需求,CashPayment 在 realPayAmount 发生变化时,需要实时的计算找零的数量。由于业务对象中定义有 change,因此这个计算属于业务逻辑而不是显示逻辑,应当由业务模型实现。

 public void setRealPayAmount(double realPayAmount) { 
 if (this.realPayAmount != realPayAmount) { 
 propertyChangeSupport.firePropertyChange(“realPayAmount”, 
 this.realPayAmount, this.realPayAmount = realPayAmount); 
 recalculate(); 
 } 
 } 

 private void recalculate() { 
 setChange(getRealPayAmount() - getAmount()); 
    } 

完成的 CashPayment 的定义如下:


清单 2. 自定义 CashPayment 类
				
 package lucifer.swt.databinding.model; 

 public class CashPayment extends Payment { 
 private double realPayAmount; 
 private double change; 

 public double getRealPayAmount() { 
 return realPayAmount; 
 } 

 public double getChange() { 
 return change; 
 } 

 public void setRealPayAmount(double realPayAmount) { 
 if (this.realPayAmount != realPayAmount) { 
 propertyChangeSupport.firePropertyChange(“realPayAmount”, 
 this.realPayAmount, this.realPayAmount = realPayAmount); 
 recalculate(); 
 } 
 } 

 public void setChange(double change) { 
 propertyChangeSupport.firePropertyChange(“change”, this.change, 
 this.change = change); 
 } 

 @Override 
 public void setAmount(double amount) { 
 if (getAmount() != amount) { 
 super.setAmount(amount); 
 recalculate(); 
 } 
 } 

 private void recalculate() { 
 setChange(getRealPayAmount() - getAmount()); 
 } 
 } 

其他的业务模型定义类似,请参考本文附带的源码。

基于需求及业务模型对界面的要求,我们来定义 View Model。View Model 中的数据需要与 UI 中的数据及状态同步,因此,View Model 也需要继承 NotifyPropertyChanged 基类。

从上面的讨论可知,所有的业务模型已经具备数据绑定的条件,也就是说,我们可以把业务模型直接绑定到 UI 控件上显示数据,那么为什么还需要定义独立的 View Model ? Presentation Model 模式需要将 UI 的状态及控制逻辑提取到 View Model 中,而我们的业务模型并没有包含任何的 UI 控制逻辑及状态。因此,我们需要独立的 View Model 来完成 UI 状态的绑定及控制逻辑的定义。换句话说,如果不需要控制 UI 状态,也就没有必要独立定义 View Model,业务模型也可以作为 View Model 来使用。从需求可以看到,我们需要保存支付方式的数据,及不同支付方式中,是否需要显示现金支付部分 UI 的状态。


清单 3. 保存支付方式数据及显示部分 UI 状态
				
 // 篇幅限制,部分代码省略,完整代码请参考本文所附完整源代码
 public class CheckoutViewModel extends NotifyPropertyChanged { 
 public final static PayType DEFAULT_PAY_TYPE = PayType.Cash; 

 private final List<PayType> payTypes; // 可以选择的支付类型
    private PayType payType; 	 // 当前选择的支付类型
    private CashPayment cashPayment; // 现金支付的数据
 private MemberAccountPayment memberPayment; // 会员支付的数据

 public CheckoutViewModel(Order order, Member member) { 
 this.order = order; 
 this.member = member; 
 payTypes = new ArrayList<PayType>(); 
 payTypes.add(PayType.Cash); 
 payTypes.add(PayType.Member); 
        payType = DEFAULT_PAY_TYPE; 
 // 初始化默认支付方式的数据
 OnPayTypeChanged(); 	
 } 
 // 修改支付方式
 public void setPayType(PayType payType) { 
 if (!Util.equals(this.payType, payType)) { 
 propertyChangeSupport.firePropertyChange(“payType”, this.payType, 
 this.payType = payType); 
 OnPayTypeChanged(); // 修改支付类型后更改相应的支付数据
 } 
 } 

 public boolean getHasCashPay() { 
 return getCashPayment() != null; // 当前支付方式是否包含现金支付
 } 

 public boolean getHasMemberPay() { 
 return getMemberPayment() != null; // 当前支付方式是否包含会员支付
 } 

 void OnPayTypeChanged() { 
 if (Util.equals(getPayType(), PayType.Cash)) { 
 convertToCashPayment(); 
 } else if (Util.equals(getPayType(), PayType.Member)) { 
 convertToMemberPayment(); 
 } else { 
 throw new IllegalArgumentException(“Unknow PayType:” + getPayType()); 
 } 
 } 
 // 将当前的支付方式转换成现金支付
 void convertToCashPayment() { 
 setMemberPayment(null); 
 CashPayment cashPayment = new CashPayment(); 
 cashPayment.setAmount(order.getAmount()); 
 setCashPayment(cashPayment); 
 } 
 // 将当前的支付方式转换成会员支付
 void convertToMemberPayment() { 
 MemberAccountPayment memberPayment = new MemberAccountPayment(); 
 if (member.getAccountBalance() >= order.getAmount()) { 
 memberPayment.setAmount(order.getAmount()); // 预存余额足够支付
 setCashPayment(null); 
 } else { // 会员预存不够支付,需要现金补充
 memberPayment.setAmount(member.getAccountBalance()); 
 double leftAmount = order.getAmount() - member.getAccountBalance(); 
 CashPayment cashPayment = new CashPayment(); 
 cashPayment.setAmount(leftAmount); 
 setCashPayment(cashPayment); 
 } 
 setMemberPayment(memberPayment); 
 } 
 void setCashPayment(CashPayment cashPayment) { 
    if (!Util.equals(this.cashPayment, cashPayment)) { 
       CashPayment oldCashPayment = this.cashPayment; 
       boolean oldHasCashPayment = getHasCashPay(); 
 this.cashPayment = cashPayment; 
 propertyChangeSupport.firePropertyChange(“cashPayment”, 
          oldCashPayment, cashPayment); 
 if ((oldHasCashPayment) ^ (getHasCashPay())) { 
          propertyChangeSupport.firePropertyChange(“hasCashPay”, 
 oldHasCashPayment, getHasCashPay()); 
 } 
        } 
    } 

 void setMemberPayment(MemberAccountPayment memberPayment) { 
 if (!Util.equals(this.memberPayment, memberPayment)) { 
 MemberAccountPayment oldMemberPayment = this.memberPayment; 
 boolean oldHasMemberPayment = getHasMemberPay(); 
 this.memberPayment = memberPayment; 
 propertyChangeSupport.firePropertyChange(“memberPayment”, 
 oldMemberPayment, memberPayment); 
 if (oldHasMemberPayment ^ getHasMemberPay()) { 
 propertyChangeSupport.firePropertyChange(“hasMemberPay”, 
 oldHasMemberPayment, getHasMemberPay()); 
 } 
 } 
 } 
 } 

根据理解的不同,以上的 View Model 实现中的部分代码逻辑可能需要放入到业务模型中,我们在这里主要讨论 PM 的实现,因此不深入讨论这个话题。

到这里,我们还没有实现任何和真正 UI 显示相关的代码,但是,View Model 已经完整的定义了需求中的重点 UI 功能,这就是 PM 中 View Model 对 UI View 没有依赖的含义。从设计的角度来看,取消依赖性,可以提高 View Model 的可重用性。比如一个 View Model 可用不同的方式来展现,同时可以保证逻辑一致性。

下面我们来简单的看看 UI View 的实现。View 的实现与普通的 SWT 实现区别不大,为了提高可重用性,我们没有直接在 ViewPart 中定义控件,而是使用一个 Composite 的子类来封装所有的控件,再将 Composite 聚合到 ViewPart 中。以下是部分代码片段:


清单 4. 实现 UI View
				
  // 创建及布局 UI 控件
  Label payTypeLabel = new Label(parent, SWT.NONE); 
  payTypeLabel.setText(“支付方式”); 
  payTypes = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); 

  // 数据绑定
  PayTypeConverter payTypeConverter = new PayTypeConverter(); 
 ISWTObservableList payTypesWidgetItems = WidgetProperties.items().observe(payTypes); 
 IObservableList payTypesBeanValue = BeanProperties.list(“payTypes”) 
 .observe(viewModel); 
 UpdateListStrategy payTypesModelToTargetStrategy = new UpdateListStrategy( 
 UpdateListStrategy.POLICY_UPDATE); 
 payTypesModelToTargetStrategy.setConverter(payTypeConverter); 
 payTypesBinding = getBindingContext().bindList(payTypesWidgetItems, 
 payTypesBeanValue, 
 new UpdateListStrategy(UpdateListStrategy.POLICY_NEVER), 
 payTypesModelToTargetStrategy); 
 ISWTObservableValue payTypesWidgetSelection = WidgetProperties 
 .selection().observe(payTypes); 
 IObservableValue payTypeBeanValue = BeanProperties.value(“payType”) 
 .observe(viewModel); 
 UpdateValueStrategy payTypeUpdateStrategy = new UpdateValueStrategy( 
 UpdateValueStrategy.POLICY_UPDATE); 
 payTypeUpdateStrategy.setConverter(payTypeConverter); 
 payTypeBinding = getBindingContext().bindValue(payTypesWidgetSelection, 
 payTypeBeanValue, payTypeUpdateStrategy, payTypeUpdateStrategy); 

在数据绑定的代码中,我们使用到了一个 Converter,将 View Model 中使用的数据类转换成更易于理解的方式来显示。Converter 需要继承 IConverter 接口。在绑定中,我们有两种 PayType 的数据需要转换,一种是普通的 PayType 枚举数据,用来定义当前用户选择的支付类型,另外一种是 PayType 的 List,用来保存可以选择的支付类型。这两中类型本质上都是在转换同一种数据,示例中使用了一个 Converter 类,在运行态来判断正在转换的是什么类型。在真实的应用中,可以使用更好的实现方式(完整的 View 实现请参考源代码)。


对 UI 进行单元测试

上一节的描述中,我们定义了一个简单的 PM 的实现。我们可以看到由于对 View Model 的抽象, View 的实现中只有 UI 控件的创建,布局及绑定的代码,View 中并没有包含任何的逻辑和状态控制。在进行单元测试时,集中测试 View Model 即可以覆盖完整的逻辑及状态验证。


清单 5. 对 View Model 集中测试
				
 public class TestCheckoutViewModel { 
 private static final double TEST_MEMBER_BALANCE = 50.0d; 
 private static final double TEST_ORDER_AMOUNT = 100.0d; 
 private CheckoutViewModel testViewModel; 
 private Order testOrder; 

 @Before 
 public void setup() { 
 testOrder = new Order(); 
 testOrder.setAmount(TEST_ORDER_AMOUNT); 
 Member testMember = new Member(); 
 testMember.setAccountBalance(TEST_MEMBER_BALANCE); 

 testViewModel = new CheckoutViewModel(testOrder, testMember); 
 } 

 @After 
 public void teardown() { 
 testViewModel = null; 
 testOrder = null; 
 } 

 @Test 
 public void testDefaultPayTypeShouldBeCash() { 
 assertEquals(PayType.Cash, testViewModel.getPayType()); 
 } 

 @Test 
 public void testChangePayTypeShouldTriggerEvent() { 
 final List<Object> triggered = new ArrayList<Object>(); 

 testViewModel.addPropertyChangeListener(“payType”, 
 new PropertyChangeListener() { 
 @Override 
 public void propertyChange(PropertyChangeEvent event) { 
 triggered.add(new Object()); 
 } 
 }); 

 testViewModel.setPayType(PayType.Member); 
 assertEquals(1, triggered.size()); 
 } 

 @Test 
 public void testDefaultPayTypesShouldIncludeCashAndMember() { 
 assertEquals(2, testViewModel.getPayTypes().size()); 
 assertTrue(testViewModel.getPayTypes().contains(PayType.Cash)); 
 assertTrue(testViewModel.getPayTypes().contains(PayType.Member)); 
 } 

 @Test 
 public void testDefaultPaymentShouldBeCashPayment() { 
 assertNotNull(testViewModel.getCashPayment()); 
 assertNull(testViewModel.getMemberPayment()); 
 assertTrue(testViewModel.getHasCashPay()); 
 assertFalse(testViewModel.getHasMemberPay()); 
 assertEquals(PayType.Cash, testViewModel.getPayType()); 
 } 

 @Test 
 public void testChangeToCashPaymentShouldOnlyHaveCashPayment(){ 
 testViewModel.setPayType(PayType.Cash); 
 assertNotNull(testViewModel.getCashPayment()); 
 assertNull(testViewModel.getMemberPayment()); 
 assertTrue(testViewModel.getHasCashPay()); 
 assertFalse(testViewModel.getHasMemberPay()); 
 assertEquals(PayType.Cash, testViewModel.getPayType()); 
 } 

 @Test 
 public void testChangeToMemberPaymentShouldOnlyHaveMemberPaymentIfBalanceIsEnough(){ 
 Member member = new Member(); 
 member.setAccountBalance(testOrder.getAmount() + 10); 

 CheckoutViewModel viewModel = new CheckoutViewModel(testOrder, member); 
 viewModel.setPayType(PayType.Member); 
 assertNotNull(viewModel.getMemberPayment()); 
 assertNull(viewModel.getCashPayment()); 
 assertFalse(viewModel.getHasCashPay()); 
 assertTrue(viewModel.getHasMemberPay()); 
 assertEquals(PayType.Member, viewModel.getPayType()); 
 } 

 @Test 
 public void 
 testChangeToMemberPaymentShouldHaveMemberAndCashPaymentIfBalanceIsnotEnough(){ 
 testViewModel.setPayType(PayType.Member); 
 assertNotNull(testViewModel.getMemberPayment()); 
 assertNotNull(testViewModel.getCashPayment()); 
 assertTrue(testViewModel.getHasCashPay()); 
 assertTrue(testViewModel.getHasMemberPay()); 
 assertEquals(PayType.Member, testViewModel.getPayType()); 
 } 
 } 

( 以上的测试用例完成简单的 View Model 的测试,并非完整的 Unit-Test)


更进一步

UI 中的控件往往都有很好的封装,封装的 UI 控件更易于重用。我们可以将 UI 封装的思想继续沿用于 PM 模式。以下的静态类图展现了一种简易的解决方案。


图 4. 实现 PM 模式的 UI 组件
图 4. 实现 PM 模式的 UI 组件

DataContext 接口提供了保持数据上下文的抽象描述,其抽象实现 BindingComposite 是应用 PM 模式的 UI 组件的父类,实现了对数据上下文的读写方法。抽象方法 attachTo 和 detach 由具体的 UI 控件实现,用于实现对当前数据上下文的绑定或取消绑定。接口与抽象类均为泛型类型,其参数类型为其实现控件所依赖的 View Model 决定。

接口与抽象类的引入使得支持 PM 模式的控件有了统一访问接口。容器控件不再需要了解子控件如何绑定其 View Model,只需调用子控件的 attachTo 方法将与其对应的 View Model 绑定即可。CheckoutComposite 实现片段如下所示:


清单 6. 利用统一访问接口测试
				
 public class CheckoutComposite extends BindingComposite<CheckoutViewModel> { 
    ... 
 private Composite cashPaymentSection; 
 private Binding hasCashPayBinding; 
 private Binding cashPaymentBinding; 
 private MemberPaymentComposite memberPaymentSection; 
    ... 
    
 private void createControls(Composite parent) { 
        ... 
 memberPaymentSection = new 
 MemberPaymentComposite(getBindingContext(), 
 paymentContainer, SWT.BORDER); 

 cashPaymentSection = new CashPaymentComposite(getBindingContext(), 
 parent, SWT.BORDER); 
 cashPaymentSection.setLayoutData(containerData); 
    } 

 @Override 
 protected void attachTo(CheckoutViewModel viewModel) { 
        ... 
 ISWTObservableValue cashPaymentSectionWidgetVisible = WidgetProperties 
 .visible().observe(cashPaymentSection); 
 IObservableValue hasCashPayBeanValue = BeanProperties.value( 
“hasCashPay”).observe(viewModel); 
 hasCashPayBinding = getBindingContext().bindValue( 
 cashPaymentSectionWidgetVisible, hasCashPayBeanValue, 
 new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER), 
 new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE)); 
 IObservableValue cashPaymentSectionWidgetDataContext = BeanProperties 
 .value(“dataContext”).observe(cashPaymentSection); 
 IObservableValue cashPaymentBeanValue = BeanProperties.value( 
“cashPayment”).observe(viewModel); 
 cashPaymentBinding = getBindingContext().bindValue( 
 cashPaymentSectionWidgetDataContext, cashPaymentBeanValue, 
 new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER), 
 new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE)); 
 IObservableValue memberPaymentSectionWidgetDataContext = BeanProperties 
 .value(“dataContext”).observe(memberPaymentSection); 
 IObservableValue memberPaymentBeanValue = BeanProperties.value( 
“memberPayment”).observe(viewModel); 
 getBindingContext().bindValue( 
 memberPaymentSectionWidgetDataContext, memberPaymentBeanValue, 
 new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER), 
 new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE)); 
 ISWTObservableValue memberPaymentSectionWidgetVisible = WidgetProperties 
 .visible().observe(memberPaymentSection); 
 IObservableValue hasMemberPaymentBeanValue = BeanProperties.value( 
“hasMemberPay”).observe(viewModel); 
 getBindingContext().bindValue( 
 memberPaymentSectionWidgetVisible, hasMemberPaymentBeanValue, 
 new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER), 
 new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE)); 
    } 
    
 @Override 
 protected void detach() { 
 unbind(hasCashPayBinding); 
 unbind(cashPaymentBinding); 
    } 
 } 

组件化的 PM UI 控件可以从其容器控件中脱离,独立的完成单元测试。完整测试过的 PM UI 控件可以在多个容器中重用。另一方面,从一种物理展现抽象出来的 View Model,也可以被用于其他的物理展现,并能保证不同展现中的逻辑一致性。


总结

虽然 PM 模式在一定程度上提高了 UI 的可测试性,但是还存在大量的代码无法覆盖,比如,无法测试绑定部分的代码逻辑。而绑定的定义使用了大量字符串来描述 Bean 属性名称,这很容易引入错误,而且在对 Bean 代码重构时,IDE 无法识别出这些字符串中的属性名,不会自动修改,也不会产生编译错误,从而成为了隐藏的、只有运行态才能检查出来的错误,这些错误往往是更大隐患。因此,需要更多的辅助类来使这部分代码更简洁,就像自动化的 Binding 可以避免重复拷贝代码,从而减少错误出现的几率一样。

另外,PM 模式在 SWT/JFace 中的应用还有大量的工作要做。比如, SWT 的数据绑定涉及数据校验,但实际项目中对校验可能有很多特殊要求,比如需要将多个数据校验结果的聚合与另外一个 UI 控件的属性绑定等。这些在当前的实现中是没有涉及的。有兴趣的读者可以进一步研究这方面的设计与实现,使得 PM 模式在 SWT/JFace 中能应用的更好。


参考资料

学习

讨论

关于作者

鲁韵涛,10 年以上的研发经验,目前就职于 IBM 中国研发中心。

关于报告滥用的帮助

报告滥用

谢谢! 此内容已经标识给管理员注意。


关于报告滥用的帮助

报告滥用

报告滥用提交失败。 请稍后重试。


developerWorks:登录


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 使用条款

 


当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

请选择您的昵称:

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

(长度在 3 至 31 个字符之间)


单击提交则表示您同意developerWorks 的条款和条件。 使用条款.

 


为本文评分

评论

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=785240
ArticleTitle=Presentation Model 模式在 SWT 程序中的应用
publish-date=01122012

标签

Help
使用 搜索 文本框在 My developerWorks 中查找包含该标签的所有内容。

使用 滑动条 调节标签的数量。

热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。

我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。

使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。