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

Summary:

有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。

Motivation:

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

尽管可以轻松地将“行为”划分到不同部位,“数据”却往往不能如此。同一项数据有可能急需要内嵌于GUI控件,也需要保存于领域模型里。自从MVC模式出现后,用户界面框架都是用多层系统来提供某种机制,使你不但可以提供这类数据,并保持他们同步。

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

Mechanics: 

1.修改展现类,使其成为领域类的Observer

如果尚未有领域类,就建立一个。

如果没有“从展现类到领域类”的关联,就将领域类保存于展现类的一个字段中。

2.针对GUI类中的领域数据,使用Self Encapsulate Field。

3.编译,测试。

4.在事件处理函数中调用设值函数,直接更新GUI组件。

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

进行这个改变时,对于组件,不要使用取值函数,应该直接取用,因为稍后我们将修改取值函数,使其从领域对象(而非GUI组件)取值。设置函数也将做类似修改。

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

5.编译,测试。

6.在领域类中定义数据及其相关访问函数。

确保领域类中的设值函数能够触发Observer模式的同步机制。

对于被观察的数据,在领域类中使用与展现类所用的相同类型(通常是字符串)来保存。后续重构中可以自由改变这个数据类型。

7.修改展现类中的访问函数,将它们的操作对象改为领域对象(而非GUI组件)

8.修改Observer的updated () ,使其从相应的领域对象中所需数据复制给GUI组件

9. 编译,测试

范例

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

一开始,所有的函数都放在IntervalWindow类中。所有文本框都能够相应“失去焦点”这一事件。

public class IntervalWindow extends Frame
{
    TextField startField;

    TextField endField;

    TextField lengthField;

    class SysFocus extends FocusAdapter
    {
        public void focusLost( FocusEvent event )
        {
             Object object = event.getSource();
            if( object == startField )
            {
                startFieldFocusLost( event );
            }
            else if( object == endField )
            {
                endFieldFocusLost( event );
            }
            else if( object == lengthField )
            {
                lengthFieldFocusLost( event );
            }

        }
    }

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


void startFieldFocusLost( FocusEvent event )
    {
        if( isNotInteger( startField.getText() ) )
        {
            startField.setText( "0" );
        }
        caculateLength();
    }

    void endFieldFocusLost( FocusEvent event )
    {
        if( isNotInteger( endField.getText() ) )
        {
            endField.setText( "0" );
        }
        caculateLength();
    }

    void lengthFieldFocusLost( FocusEvent event )
    {
        if( isNotInteger( lengthField.getText() ) )
        {
            lengthField.setText( "0" );
        }
        caculateEnd();
    }
  void caculateLength()
    {
       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 caculateEnd()
    {
        try
        {
            int start = Integer.parseInt( startField.getText() );
            int length = Integer.parseInt( lengthField.getText() );
            int end = start + length;
            endField.setText( String.valueOf( end ) );
        }
        catch( NumberFormatException e )
        {
            throw new RuntimeException( "Unexpected Number format Error" );
        }
    }


我们的任务就是将与展现无关的计算逻辑从GUI中分离出来。基本上这就意味着将calculateLength和calculateEnd移到一个独立的领域类去。为了这一目的,我们需要能够在不引用窗口类的前提下获取Start、End和Length三个文本框的值。唯一的办法就是将这些数据复制到领域类中,并保持与GUI数据同步。这就是Duplicate Observed Data的任务。

截至目前,我们还没有一个领域类,所以要着手建立一个

public class Interval extends Observable
{

}
IntervalWindow类需要与此崭新的领域类建立一个关联:
private Interval subject;
然后,合理的初始化subject字段,并把IntervalWindow变成Interval的一个Observer。这很简单,只需把下列代码放进IntervalWindow构造函数中就可以了:
subject = new Interval();
subject.addObserver( this );
update( subject, null );
其中对update的调用可以确保:当我们把数据复制到领域类后,GUI将根据领域类进行初始化。update()是在java.util.Observer接口中声明的,因此必须让IntervalWindow实现这一接口, 然后为IntervalWindow类建立一个update().

接下来,我们开始修改文本框。从End开始。先运用Self Encapsulate Field.文本框的更新时通过getText()和setText()两函数实现的,因此我们所建立的访问函数需要调用这两个函数:

String getEnd()
{
    return endField.getText();
}

void setEnd( String arg )
{
   endField.setText( arg );
}
然后,找出endField的所有引用点,将他们替换为适当的访问函数。这是Self Encapsulate Field的标准过程。然而档处理GUI时,情况更复杂些:用户可以直接(通过GUI)修改文本框内容,不必调用setEnd()。因此我们需要在GUI的事件处理中调用setEnd()。这个动作把End文本框设定为其当前值。当然,这没带来什么影响,但是通过这样的方式,可以确保用户的输入确实是通过设置函数进行的:
void endFieldFocusLost( FocusEvent event )
{
     setEnd( endField.getText() );
     if( isNotInteger( getEnd() ) )
     {
         setEnd( "0" );
     }
     caculateLength();
}

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

现在,在领域类中加入end字段,给它的初始值和GUI给的初始值是一样的。然后再 加入取值/设值函数:

public class Interval extends Observable
{
    private String end = "0";

    public String getEnd()
    {
        return end;
    }

    public void setEnd( String end )
    {
        this.end = end;
        setChanged();
        notifyObservers();
    }
}

由于使用了Observer模式,我们必须在设值函数中发出通知。

现在我们修改IntervalWindow类的访问函数,令它们改用Interval对象:

String getEnd()
{
    return subject.getEnd();
}

void setEnd( String arg )
{
   subject.setEnd( arg );
}

同时也修改update()函数,确保GUI对Interval对象发来的通知做出响应:

public void update( Observable arg0, Object arg1 )
{
    endField.setText( subject.getEnd() );
}

这是另一个需要直接访问文本框的地点。如果我们调用的是设值函数,程序将陷入无限递归调用。

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

另两个文本框也如法炮制。完成之后,我们可以使用Move Method将calculateEnd()和calculateLength()搬到Interval 去。这么一来,我们就拥有一个包容所有领域行为和领域数据、并与GUI分离的领域类了。

转载于:https://my.oschina.net/u/134516/blog/133709

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值