Duplicate Observed Data(复制被监视数据)

注:所谓presentation class,用以处理[数据表现形式];所谓domain class,用以处理业务逻辑。

你有一些domain data置身于GUI控件中,而domain method需要访问之。

将该笔数据拷贝一个domain object中。建立一个Observer模式,用以对domain object和GUI object内的重复数据进行同步控制(sync.)。

 

动机

一个分层良好的系统,应该将处理用户界面(UI)和处理业务逻辑(business logic)的代码分开。之所以这样做,原因有以下几点:(1)你可能需要使用数个不同的用户界面来表现相同的业务逻辑;如果同时承担两种责任,用户界面会变得过分复杂;(2)与GUI隔离之后,domain objects的维护和演化都会更容易;你甚至可以让不同的开发者负责不同部分的开发。

如果你遇到的代码是以双层(two-tiered)方式开发,业务逻辑被内嵌于用户界面(UI)之中,你就有必要将行为分离出来。其中的主要工作就是函数的分离和搬移。但数据就不同了:你不能仅仅只是移动数据,你必须将它复制到新建部位中,并提供相应的同步机制。

 

作法

1. 修改presentation class,使其成为domain classObserver[GoF]

如果尚未有domain class,就建立一个。

如果没有[从presentation class到domain class]的关联性(link),就将domain class保存于presentation class的一个值域中。

2. 针对GUI class内的domain data,使用Self Encapsulate Field(171)。

3. 编译,测试。

4. 在事件处理函数(event handler)中加上对设值函数(setter)的调用,以[直接访问方式]更新GUI组件。

在事件处理函数中放一个设值函数(setter),利用它将GUI组件更新为domain data的当前值。当然这其实没有必要,你只不过是拿它的值设定它自己。但是这样使用setter,便是允许其中的任何动作得以于日后被执行起来,这是这一步骤的意义所在。

进行这个改变时,对于组件,不要使用取值函数(getter),应该采取[直接取用]方式,因为稍后我们将修改取值函数(getter),使其从domain object(而非GUI组件)取值。设值函数(setter)也将遭受类似修改。

确保测试代码能够触发新添加的事件处理(event handling)机制。

5. 编译,测试。

6. 在domain class中定义数据及其相关访问函数(accessors)。

确保domain class中的设值函数(setter)能够触发Observer模式的通报机制(notify mechanism)。

对于被观察(被监视)的数据,在domain class中使用[与presentation class所用的相同型别](通常是字符串)来保存。后续重构中你可以自由改变这个数据型别。

7. 修改presentation class中的访问函数(accessors),将它们的操作对象改为domain object(而非GUI组件)。

8. 修改observer的update(),使其从相应的domain object中将所需数据拷贝给GUI组件。 (不要使用setter函数)。

9. 编译,测试。

 

我们的范例其行为非常简单:当用户修改文本框中的数值,另两个文本框就会自动更新.如果你修改Start或End,length就会自动成为两者计算所得的长度;如果你修改length,End就会随之变动.

一开始,所有函数都放在IntervalWindow class中.所有文本框都能够响应[失去键盘焦点](loss of focus)这一事件。
public class IntervalWindow extends Frame...
    java.awt.TextField _startField;
    java.awt.TextField _endField;
    java.awt.TextField _lengthField;


    class SymFocus extends java.awt.event.FocusAdapter
    {
       public void focusLost(java.awt.event.FocusEvent event)
       {
          Object object = event.getSource();
   
          if(object == _startField)
             StartField_FocusLost(event);
          else if(object = _endField)
             EndField_FocusLost(event);
          else if(object = _lengthField)
             LengthField_FocusLost(event);
       }

Start文本框失去焦点,事件监听器调用StartField_FocusLost()。另两个文本框的处理也类似。事件处理函数大致如下:

void StartField_FocusLost(java.awt.event.FocusEvent event) {
    if(isNotInteger(_startField.getText()))
       _startField.setText("0");
    calculateLength();
}
void EndField_FocusLost(java.awt.event.FocusEvent event) {
    if(isNotInteger(_endField.getText()))
       _endField.setText("0");
    calculateLength();
}
void LengthField_FocusLost(java.awt.event.FocusEvent event) {
    if(isNotInteger(_lengthField.getText()))
       _lengthField.setText("0");
    calculateEnd();
}

如果文本框的字符串无法转换为一个整数,那么该文本框的内容将变成0。而后,调用相关计算函数:

void calculateLength() {
    try {
       int start = Integer.parseInt(_startField.getText());
       int end = Integer.parseInt(_endField.getText());
       int length = end - start;
       _lengthField.setText(String.valueOf(length));
    } catch(NumberFormatException e) {
       throw new RuntimeException("Unexpected Number Format Error");
    }
}
void calculateEnd() {
    try {
       int start = Integer.parseInt(_startField.getText());
       int end = Integer.parseInt(_endField.getText());
       int end = start + length;
       _endField.setText(String.valueOf(end));
    } catch(NumberFormatException e) {
       throw new RuntimeException("Unexpected Number Format Error");
    }
  }
}

我的任务就是非视觉性的计算逻辑从GUI中分离出来。基本上这就意味将calculateLength()和calculateEnd()移到一个独立的domain class去。为了这一目的,我需要能够在不引用窗口类的前提取用StartEndlength三个文本框的值。唯一办法就是将这些数据复制到domain class中,并保持与GUI class数据同步。这就是Duplicate Observed Data(189)的任务。

截至目前我还没有一个domain class,所以我着手建立一个:

class Interval extends Observable {}

IntervalWindow class需要与此崭新的domain class建立一个关联:
private Interval _subject;

然后,我需要合理地初始化_subject值域,并把IntervalWindow class变成Interval class的一个Observer。这很简单,只需把下列代码放进IntervalWindow构造函数中就可以了:

_subject = new Interval();
_subject.addObserver(this);
update(_subject, null);
我喜欢把这段代码放在整个建构过程的最后。其中对update()的调用可以确保:当我把数据复制到domain class后,GUI将根据domain class进行初始化。update()是在java.util.observer接口中声明的,因此我必须让IntervalWindow class实现这一接口:

public class IntervalWindow extends Frame implements Observer

然后我还需要为IntervalWindow class建立一个update()。此刻我先令它为空:

public void update(Observable observed, Object arg)  {
}
现在我可以编译并测试了。到目前为止我还没有作出任何真正的修改。呵呵,小心驶得万年船。

接下来我把注意力转移到文本框。一如往常我每次只改动一点点。为了卖弄一下我的英语能力,我从End文本框开始。第一件要做的事就是实施Self Encapsulate Field(171)。文本框的更新是通过getText()和setText()两函数实现的,因此我所建立的访问函数(accessors)需要调用这两个函数:

String getEnd() {
    return _endField.getText();
}
void setEnd(String arg) {
    _endField.setText(arg);
}

然后,找出_endField的所有引用点,将它们替换为适当的访问函数:

void calculateLength() {
    try {
       int start = Integer.parseInt(_startField.getText());
       int end = Integer.parseInt(getEnd());
       int length = end - start;
       _lengthField.setText(String.valueOf(length));
    } catch(NumberFormatException e) {
       throw new RuntimeException("Unexpected Number Format Error");
    }
}
void calculateEnd() {
    try {
       int start = Integer.parseInt(_startField.getText());
       int end = Integer.parseInt(_endField.getText());
       int end = start + length;
       setEnd(String.valueOf(end));
    } catch(NumberFormatException e) {
       throw new RuntimeException("Unexpected Number Format Error");
    }
}
void EndField_FocusLost(java.awt.event.FocusEvent event) {
    if(isNotInteger(getEnd()))
       setEnd("0");
    calculateLength();
}

这是Self Encapsulate Field(171)的标准过程。然后当你处理GUI class时,情况还更复杂些:用户可以直接(通过GUI)修改文本框内容,不必调用setEnd()。因此我需要在GUI class的事件处理函数中加上对setEnd()的调用。这个动作把End文本框设定为其当前值。当然,这没带来什么影响,但是通过这样的方式,我们可以确保用户的输入的确是通过设值函数(setter)进行的:
void EndField_FocusLost(java.awt.event.FocusEvent event) {
    setEnd(_endField.getText());
    if(isNotInteger(getEnd()))
       setEnd("0");
    calculateLength();
}

上述调用动作中,我并没有使用上一页的getEnd()取得End文本框当前内容,而是直接取用该文本框。之所以这样做是因为,随后的重构将使上一页的getEnd()从domain object(而非文本框)身上取值。那时如果这里用的是getEnd()函数,每当用户修改文本框内容,这里就会将文本框又改回原值。所以我必须使用 [直接访问文本框]的方式获得当前值。现在我可以编译并测试值域封装后的行为了。

现在,在domain class中加入_end值域:

class Interval...
    private String _end = "0";

在这里,我给它的初始值和GUI class给它的初值是一样的。然后我再加入取值/设值函数(getter/setter):
class Interval...
    String getEnd() {
       return _end;
    }
    void setEnd(String arg) {
       _end = arg;
       setChanged();
       notifyObservers();
    }

由于使用了Observer模式,我必须在设值函数(setter)中加上[发出通告]动作(即所谓notification code)。我把_end声明为一个字符串,而不是一个看似更合理的整数,这是因为我希望将修改量减至最少。将来成功复制数据完毕后,我可以自由自在地于 domain class内部把_end声明为整数。

现在,我可以再编译并测试一次。我希望通过所有这些预备工作,将下面这个较为棘手的重构步骤的风险降至最低。

首先,修改IntervalWindow class的访问函数,令它们改用Interval对象:
class IntervalWindow...
    String getEnd() {
       return _subject.getEnd();
    }
    void setEnd(String arg) {
       _subject.setEnd(arg);
    }

同时也修改update()函数,确保GUIInterval对象发来的通告做出响应:
class IntervalWindow...
    public void update(Observable observed, Object arg) {
       _endField.setText(_subject.getEnd());
    }
这是另一个需要[直接取用文本框]的地点。如果我调用的是设值函数(setter),程序将陷入无限递归调用(这是因为IntervalWindow的设值函数setEnd()调用了Interval。setEnd(),一如稍早行所示:而Interval.setEnd()又调用 notifyObservers(),导致IntervalWindow.update()又被调用)。

现在,我可以编译并测试,数据都恰如其分地被复制了。

另两个文本框也如法炮制。完成之后,我可以使用Move Method(142)将calculateEnd()和calculateLength()搬到Interval class。这么一来,我就拥有一个[包容所有domain behavior和domain data]并与GUI code分离的domain class了。

如果上述工作都完成了,我就会考虑彻底摆脱这个GUI class。如果GUI class是个较为老旧的AWT class,我会考虑将它换成一个比较好看的Swing class,而且后者的坐标定位能力也比较强。我可以在domain class之上建立一个Swing GUI。这样,只要我高兴,随时可以去掉老旧的GUI class。

使用事件监听器(Event Listeners)
如果你使用事件监听器(event listener)而不是Observer/Observable模式,仍然可以实施Duplicate Observed Data(189)。这种情况下,你需要在domain model中建立一个listener class和一个event class。然后,你需要对domain object注册listeners,就像前例对observable对象注册observers一样。每当domain object发生变化(类似上例的update()函数被调用),就向listeners发送一个事件(event)。IntervalWindow class可以利用一个inner class(内嵌类)来实现监听器接口(listener interface),并在适当时候调用适当的update()函数。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值