### Duplicate Observed Data(复制「被监视数据」)
(译注:本节大量保留domain,presentation,event,getter/setter,observed等字眼。所谓presentation class,用以处理「数据表现形式」;所谓domain class,用以处理业务逻辑。)
你有一些domain class置身于GUI控件中,而domain method需要访问之。
将该笔数据拷贝到一个domain object中。建立一个Observer模式,用以对domain object和GUI object内的重复数据进行同步控制(sync.)。
![](https://box.kancloud.cn/2016-08-15_57b1b56d175ab.gif)
**动机(Motivation)**
一个分层良好的系统,应该将处理用户界面(UI)和处理业务逻辑(business logic)的代码分开。之所以这样做,原因有以下几点:(1) 你可能需要使用数个不同的用 户界面来表现相同的业务逻辑;如果同时承担两种责任,用户界面会变得过分复杂; (2) 与GUI隔离之后,domain class的维护和演化都会更容易;你甚至可以让不同的开发者负责不同部分的开发。
尽管你可以轻松地将「行为」划分到不同部位,「数据」却往往不能如此。同一笔 数据有可能既需要内嵌于GUI控件,也需要保存于domain model里头。自从MVC(Model-View-Controller)模式出现后,用户界面框架都使用多层系统(multitiered system)来提供某种机制,使你不但可以提供这类数据,并保持它们同步(sync.)。
如果你遇到的代码是以双层(two-tiered)方式开发,业务逻辑(business logic)被内嵌于用户界面(UI)之中,你就有必要将行为分离出来。其中的主要工作就是函数的分解和搬移。但数据就不同了:你不能仅仅只是移动数据,你必须将它复制到新建部位中,并提供相应的同步机制。
**作法(Mechanics)**
(译注:建议搭配范例阅读)
- 修改presentation class,使其成为 domain class 的 Observer[GoF]。
- 如果尚未有domain class,就建立一个。
- 如果没有「从presentation class到domain class的关联性(link), 就将domain class保存于咖presentation class的一个值域中。
- 针对GUI class内的domain data,使用Self Encapsulate Field 。
- 编译,测试。
- 在事件处理函数(event handler)中加上对设值函数(setter)的调用,以「直接访问方式」(译注:亦即直接调用组件提供的相关函数)更新GUI组件。
- 在事件处理函数中放一个设值函数(setter),利用它将GUI组件更新为domain data的当前值。当然这其实没有必要,你只不过是拿它的值设定它自己。但是这样使用setter,便是允许其中的任何动作得以于日后被执行起来,这是这一步骤的意义所在。
- 进行这个改变时,对于组件,不要使用取值函数(getter),应该采取「直接取用」方式(译注:亦即直接调用GUI组件所提供的函数),因为稍后我们将修改取值函数(getter),使其从domain object(而非GUI组件)取值。设值函数(setter)也将遭受类似修改。
- 确保测试代码能够触发新添加的事件处理(event handler)机制。
- 编译,测试。
- 在domain class中定义数据及其相关访问函数(accessors)。
- 确保domain class中的设值函数(setter)能够触发Observer模式的通报机制(notify mechanism)。
- 对于被观察(被监视)的数据,在domain class中使用「与presentation class所用的相同型别」(通常是字符串)来保存。后续重构中你可以自由改变这个数据型别。
- 修改presentation class中的访问函数(accessors),将它们的操作对象改为 domain object (而非GUI组件)。
- 修改observer(译注:亦即presentation class)的update(),使其从相应的domain object中将所需数据拷贝给GUI组件。
- 编译,测试。
**范例(Example)**
我们的范例从图8.1所示窗口开始。其行为非常简单:当用户修改文本框中的数值,另两个文本框就会自动更新。如果你修改Start或End,length就会自动成为两者计算所得的长度;如果你修改length,End就会随之改变。
![](https://box.kancloud.cn/2016-08-15_57b1b56d3065e.gif)
图8.1 一个简单的GUI窗口
一开始,所有函数都放在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();
//译注:侦测到哪一个文本框失去键盘焦点,就调用其event-handler.
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();
}
~~~
你也许会奇怪,为什么我这样实现一个窗口呢?因为在我的IDE集成开发环境(Cafe)中,这是最简单的方式。
如果文本框内的字符串无法转换为一个整数,那么该文本框的内容将变成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 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 ()移到一个独立的domain class去。为了这一目的,我需要能够在不引用(指涉,referring)窗口类的前提下取用Start、End和 length 三个文本框的值。惟一办法就是将这些数据复制到domain class中,并保持与GUI class数据同步。这就是Duplicate Observed Data 的任务。
截至目前我还没有一个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。文本框的更新是通过getText()和setText()两函数实现的,因此我所建立的访问函数(accessors)需要调用这两个函数:
~~~
//译注:class IntervalWindow...
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 length = Integer.parseInt(_lengthField.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 的标准过程。然而当你处理GUI class 时,情况还更复杂些:用户可以直接(通过GUI )修改文本框内容,不必调用setEnd() 。 因此我需要在GUI class 的事件处理函数中加上对setEnd() 的调用。这个动作把文本框设定为其当前值。当然,这没带来什么影响,但是通过这样的方式,我 们可以确保用户的输入的确是通过设值函数(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(); //译注:notificaiton code
}
~~~
由于使用了Observer 模式,我必须在设值函数(setter)中加上「发出通告」动作 (即所谓notify code )。我把_end 声明为一个字符串,而不是一个看似更合理的整数,这是因为我希望将修改量减至最少。将来成功复制数据完毕后,我可以自由自在地于domain class 内部把_end 声明为整数。
现在,我可以再编译并测试一次。我希望通过所有这些预备工作,将下面这个较为棘手的重构步骤的风险降至最低。
首先,修改IntervalWindow class 的访问函数,令它们改用Interval 对象:
~~~
class IntervalWindow...
String getEnd() {
return _subject.getEnd();
}
void setEnd (String arg) {
_subject.setEnd(arg); //(A) 译注:本页最下对此行有些说明
}
~~~
同时也修改update() 函数,确保GUI 对Interval 对象发来的通告做出响应:
~~~
class IntervalWindow...
public void update(Observable observed, Object arg) {
_endField.setText(_subject.getEnd());
}
~~~
这是另一个需要「直接取用文本框」的地点。如果我调用的是设值函数(setter),程序将陷入无限递归调用(译注:这是因为IntervalWindow 的设值函数setEnd() 调用了Interval.setEnd() ,一如稍早(A)行所示;而Interval.setEnd() 又调用notifyObservers() ,导致IntervalWindow.update() 又被调用)。
现在,我可以编译并测试。数据都恰如其分地被复制了。
另两个文本框也如法炮制。完成之后,我可以使用Move Method 将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。这种情况下,你需要在domain model中建立一个listener class和一个event class (如果你不在意依存关系的话,也可以使用AWT class)。然后,你需要对domain object注册listeners,就像前例对observable对象注册observes一样。每当domain object发生变化(类似上例的update()函数被调用),就向listeners发送一个事件(event)。IntervalWindow class可以利用一个inner class (内嵌类)来实现监听器接口(listener interface),并在适 当时候调用适当的update()函数。
- 译序 by 侯捷
- 译序 by 熊节
- 序言
- 前言
- 章节一 重构,第一个案例
- 起点
- 重构的第一步
- 分解并重组statement()
- 运用多态(Polymorphism)取代与价格相关的条件逻辑
- 结语
- 章节二 重构原则
- 何谓重构
- 为何重构
- 「重构」助你找到臭虫(bugs)
- 何时重构
- 怎么对经理说?
- 重构的难题
- 重构与设计
- 重构与性能(Performance)
- 重构起源何处?
- 章节三 代码的坏味道
- Duplicated Code(重复的代码)
- Long Method(过长函数)
- Large Class(过大类)
- Long Parameter List(过长参数列)
- Divergent Change(发散式变化)
- Shotgun Surgery(散弹式修改)
- Feature Envy(依恋情结)
- Data Clumps(数据泥团)
- Primitive Obsession(基本型别偏执)
- Switch Statements(switch惊悚现身)
- Parallel Inheritance Hierarchies(平行继承体系)
- Lazy Class(冗赘类)
- Speculative Generality(夸夸其谈未来性)
- Temporary Field(令人迷惑的暂时值域)
- Message Chains(过度耦合的消息链)
- Middle Man(中间转手人)
- Inappropriate Intimacy(狎昵关系)
- Alternative Classes with Different Interfaces(异曲同工的类)
- Incomplete Library Class(不完美的程序库类)
- Data Class(纯稚的数据类)
- Refused Bequest(被拒绝的遗贈)
- Comments(过多的注释)
- 章节四 构筑测试体系
- 自我测试代码的价值
- JUnit测试框架
- 添加更多测试
- 章节五 重构名录
- 重构的记录格式
- 寻找引用点
- 这些重构准则有多成熟
- 章节六 重新组织你的函数
- Extract Method(提炼函数)
- Inline Method(将函数内联化)
- Inline Temp(将临时变量内联化)
- Replace Temp with Query(以查询取代临时变量)
- Introduce Explaining Variable(引入解释性变量)
- Split Temporary Variable(剖解临时变量)
- Remove Assignments to Parameters(移除对参数的赋值动作)
- Replace Method with Method Object(以函数对象取代函数)
- Substitute Algorithm(替换你的算法)
- 章节七 在对象之间搬移特性
- Move Method(搬移函数)
- Move Field(搬移值域)
- Extract Class(提炼类)
- Inline Class(将类内联化)
- Hide Delegate(隐藏「委托关系」)
- Remove Middle Man(移除中间人)
- Introduce Foreign Method(引入外加函数)
- Introduce Local Extension(引入本地扩展)
- 章节八 重新组织数据
- Self Encapsulate Field(自封装值域)
- Replace Data Value with Object(以对象取代数据值)
- Change Value to Reference(将实值对象改为引用对象)
- Replace Array with Object(以对象取代数组)
- Replace Array with Object(以对象取代数组)
- Duplicate Observed Data(复制「被监视数据」)
- Change Unidirectional Association to Bidirectional(将单向关联改为双向)
- Change Bidirectional Association to Unidirectional(将双向关联改为单向)
- Replace Magic Number with Symbolic Constant(以符号常量/字面常量取代魔法数)
- Encapsulate Field(封装值域)
- Encapsulate Collection(封装群集)
- Replace Record with Data Class(以数据类取代记录)
- Replace Type Code with Class(以类取代型别码)
- Replace Type Code with Subclasses(以子类取代型别码)
- Replace Type Code with State/Strategy(以State/strategy 取代型别码)
- Replace Subclass with Fields(以值域取代子类)
- 章节九 简化条件表达式
- Decompose Conditional(分解条件式)
- Consolidate Conditional Expression(合并条件式)
- Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
- Remove Control Flag(移除控制标记)
- Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件式)
- Replace Conditional with Polymorphism(以多态取代条件式)
- Introduce Null Object(引入Null 对象)
- Introduce Assertion(引入断言)
- 章节十一 处理概括关系
- Pull Up Field(值域上移)
- Pull Up Method(函数上移)
- Pull Up Constructor Body(构造函数本体上移)
- Push Down Method(函数下移)
- Push Down Field(值域下移)
- Extract Subclass(提炼子类)
- Extract Superclass(提炼超类)
- Extract Interface(提炼接口)
- Collapse Hierarchy(折叠继承关系)
- Form Template Method(塑造模板函数)
- Replace Inheritance with Delegation(以委托取代继承)
- Replace Delegation with Inheritance(以继承取代委托)
- 章节十二 大型重构
- 这场游戏的本质
- Tease Apart Inheritance(梳理并分解继承体系)
- Convert Procedural Design to Objects(将过程化设计转化为对象设计)
- Separate Domain from Presentation(将领域和表述/显示分离)
- Extract Hierarchy(提炼继承体系)
- 章节十三 重构,复用与现实
- 现实的检验
- 为什么开发者不愿意重构他们的程序?
- 现实的检验(再论)
- 重构的资源和参考资料
- 从重构联想到软件复用和技术传播
- 结语
- 参考文献
- 章节十四 重构工具
- 使用工具进行重构
- 重构工具的技术标准(Technical Criteria )
- 重构工具的实用标准(Practical Criteria )
- 小结
- 章节十五 集成
- 参考书目