有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。之所以这样做,原因有一下几点:(1)你可能需要使用不同的用户界面来表现相同的业务逻辑,如果同时承担两种责任,用户界面会变得过分复杂;(2)与GUI隔离后,领域对象的维护和演化都会更容易,甚至可以让不同的开发者负责不同部分的开发。
尽管可以轻松地将“行为”划分到不同部位,“数据”却往往不能如此。同一项数据有可能急需要内嵌于GUI控件,也需要保存于领域模型里。自从MVC模式出现后,用户界面框架都是用多层系统来提供某种机制,使你不但可以提供这类数据,并保持他们同步。
如果代码是以两层方式开发,业务逻辑被内嵌于用户界面之中,就有必要将行为分离出来。其中的主要工作就是函数的分解和搬移。但数据就不同了:不能仅仅只是移动数据,必须将它复制到新对象中,并提供相应的同步机制。
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分离的领域类了。