当MVC出现的时候,极大的推动了Model与View分离的潮流。然而对于一些已存在的老系统或者没有维护好的系统,你都会看到当前存在大把的巨大类----将Model,View,Controller都写在了一个widget中。一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。原因如下

  1. 如果你此时需要用不同的用户界面来展示数据,比如微软Excel中的饼状图和折线图,他其实内部展示的数据是一样的,但如果你把这两层用户界面逻辑都放在一个widget中去的话,你就会让这个wiget变得复杂无比,因为他同时承担了两个责任,一个是“饼状图”一个是“折线图”。
  2. 当你让Model与GUi分离之后,你可以让他们两个之间的维护和演化变得更加容易,你甚至可以让不同的开发者进行分别的开发。

分离之中最困难的就是数据的分离,因为你可以很轻松的把行为划分到不同部位,但数据却没这么容易。因为你需要考虑它的同步问题,举个例子,如果你此时的GUI空间需要显示你Model中的name放到一个单独的label中去,那么你可能需要内嵌于GUI的同时,也需要在Model中也保存一份。自从MVC出现之后,用户界面框架都使用多层系统来提供某种机制,使得你不但可以提供这类数据,并保持它们同步

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

做法:

  • 修改View类,使其成为Model类的观察者(Observer),如果没有Model类就建立一个,如果没有从View到Model的关联,就将Model作为View的一个字段存入。
  • 针对GUI中的Model数据,使用Self Encapsulate Field
  • 编译,测试。
  • 在事件处理函数中调用设置函数,直接更新GUI。在事件处理函数中放一个设值函数,利用它将GUI组件更新为Model的当前值,当然这其实没有必要,因为你只是拿它的值设置它自己。但是这样使用设值函数,便是允许其中的任何动作得以于日后被执行起来,这是这一个步骤的意义所在。进行这个改变时,对于组件View,不要使用取值函数,应该直接取用,因为我们稍后将修改取值函数,使其从Model对象中取值而非在GUI中,设值函数也将做类似修改。
  • 编译,测试。
  • 在Model类中定义数据以及相关访问函数,确保Model类的设值函数能够触发Observer模式的通报机制(update)。对于被观察的数据,在Model使用与View中相同的数据类型(通常是字符串),后续重构你可以自由改变这个类型。
  • 修改View中的访问函数,使它的操作对象改为Model(而非GUI)。
  • 修改Observer的update(),使其从相应的Model中将所需要数据复制给GUI。(PS:Observer模式中对于数据更新存在“推“和”拉“两种方式,这里介绍的是的“拉”数据)
  • 编译,测试。

例子:

我们假设有三个文本框,一个是Start,一个是End,一个是Length,其中Length是Start和End之间的差值,你随即修改任何值,相应的另外两个都会刷新。比如你修改了Length,相应的End就会更新,你修改了Start或者End,Length就会得到更新。一开始我们的做法就是将业务逻辑都放在了View中,已知Qt中存在这样的焦点机制

void QApplication::focusChanged ( QWidget * old, QWidget * now ) [signal]

他会根据焦点的丢失,QApplication会发出相应的信号出来,这里我们需要关注的是old,因为这个指针代表了失去焦点的widget所代表的指针,我们就可以通过他来判断到底是哪个widget失去了焦点。于是我们在自己的IntervalWindow中建立与QApplication的信号槽

connect(QCoreApplication::instance, SIGNAL(focusChanged(QWidget *, QWidget *), this, SLOT(onFocusChanged(QWidget *, QWidget *))));

这样我们就可以在自己的槽函数onFocusChanged中针对上述3个widget:m_startField,m_endField,m_lengthField做对应的焦点处理

void onFocusChanged(QWidget *old, QWidget *now)
{
QWidget *w = old; if (w == m_startField)
{
startField_focusLost();
}
else if (w == m_endField)
{
endField_focusLost();
}
else if (w == m_lengthField)
{
lengthField_focusLost();
}
}

可以看到,当任意一个指针失去焦点都会进入到相应的函数当中去,处理函数大致如下

void startField_focusLost()
{
bool ok;
int num = m_startField->getText().toInt(&ok); if (ok)
{
}
else
{
m_startField->setText("");
} calculateLength();
} void endField_focusLost()
{
bool ok;
int num = m_endField->getText().toInt(&ok); if (ok)
{
}
else
{
m_endField->setText("");
} calculateLength();
} void lengthField_focusLost()
{
bool ok;
int num = m_lengthField->getText().toInt(&ok); if (ok)
{
}
else
{
m_lengthField->setText("");
} calculateEnd();
}

其中有一个需要注意的就是当用户输入的是非法字符不能成功转成数字的时候,这里将自动变成0.下面是两个具体的计算函数

void calculateLength()
{
int start = m_startField->getText().toInt();
int end = m_endField->getText().toInt(); int length = end - start; m_lengthField->setText(QString::number(length));
} void calculateEnd()
{
int start = m_startField->getText().toInt();
int length = m_lengthField->getText().toInt(); int end = start + length; m_endField->setText(QString::number(end));
}

我们的任务就是将与GUI无关的相关计算抽离出来,基本上这就意味着我们需要把calcuateLength()和calcuateEnd()放到Model中去,为了这一个目的我们需要在不能引用View类的前提下获取三个文本框的值。唯一办法就是将这些数据复制到Model类中,并且保持与GUI之间的同步,这就是Duplicate Observed Data的任务。

到目前为止我们还没有一个独立的Model类,我们建立一个

class Interval : public Observable
{
};

其中Observable是最简单的观察者模式接口,里面实现的就是类似notify来便利订阅自己的各个客户进行相应update。我们需要建立一个View到Model的关联

Interval *m_subject;

然后我们需要合理的初始化m_subject,并把View当作这个Model的观察者,这很简单,只需要把下面代码放到View的构造函数中就可以了

m_subject = new Interval();
m_subject->addObserver(this);
update(m_subject);

我们习惯把这段代码放到构造函数的最后,其中对update的额外调用可以当我们把数据放到Model类后,GUI将根据Model类进行相应初始化。当然了,我们的View类此时应该继承Observer接口

class IntervalWindow : public Observer
{
};

并且覆写update函数,此时先写上一个空实现

void update(Observable *observed)
{
}

现在我们进行编译测试,虽然我们到目前为止还没有进行任何实质性修改,但依然需要小心。

接下来我们把注意力放到文本框上,我们从End文本框开始,第一件事情就是运用Self Encapsulate Field,文本框的更新是通过getText()和setText()来实现的,因此我们所建立的访问函数需要调用这两个函数

QString getEnd()
{
return m_endField->getText();
} void setEnd(const QString &arg)
{
m_endField->setText(arg);
}

然后我们找到m_endField的所有引用点,将他们替换为相应的访问函数(这其实已经在做解耦操作,让计算逐渐脱离相关GUI的依赖

void calculateLength()
{
int start = m_startField->getText().toInt();
int end = getEnd(); int length = end - start; m_lengthField->setText(QString::number(length));
} void calculateEnd()
{
int start = m_startField->getText().toInt();
int length = m_lengthField->getText().toInt(); int end = start + length; setEnd(QString::number(end));
} void endField_focusLost()
{
bool ok;
int num = getEnd(); if (ok)
{
}
else
{
setEnd(QString::number());
} calculateLength();
}

先做自我包装再做引用点更换,这是Self Encapsulate Field的标准过程,然而当我们处理GUI的时候,情况更为复杂:用户可以通过GUI修改文本框内容,不必通过setEnd(),因此我们需要在GUI事件处理函数中调用setEnd(),这个动作把End文本框设置为当前值,这没有带来什么影响,但是通过这样的方式,可以确保用户的输入确实是通过设值函数进行的,你这样就可以预防并且控制所有可能的情况。

void endField_focusLost()
{
setEnd(m_endField->getText()); bool ok;
int num = getEnd(); if (ok)
{
}
else
{
setEnd(QString::number());
} calculateLength();
}

细心的朋友可能会看到这里为什么没有使用getEnd()而是直接去操作文本框来获取,之所以这样做是因为我们随后的重构将使getEnd()从Model对象取值,那时如果这里使用的是getEnd(),每当用户修改文本框内容,这里就会将文本框变为原来值,所以在这里需要特别注意我们必须用直接通过文本框来获取最新值,现在我们可以编译并且测试封装后的行为了。现在我们可以给Model增加m_end字段。

    private:
QString m_end;

在这里我们给他的初值和GUI给他的初值是一样的,然后我们再加入取值/设值函数结果如下

class Interval : public Observable
{
public:
Interval() :
m_end("")
{
}
QString getEnd()
{
return m_end;
} void setEnd(const QString &arg)
{
m_end = arg;
setChanged();
notifyObservsers();
}
private:
QString m_end;
};

由于使用了Observer模式,我们必须在设值函数中发出通知,在这里我们暂且把m_end的类型设值为字符串,其实作为Model本身含义来将,采用int似乎更合理,但在这个时候我们应该尽可能将修改量减到最小,以小步伐来进行重构,倘若之后成功完成复制数据,我们可以很轻松的将m_end类型改为int。

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

首先我们修改View类的访问函数,令他们改用Interval对象

class IntervalWindow : public Observer
{
public:
QString getEnd()
{
return m_subject->getEnd();
}
void setEnd(const QString &arg)
{
m_subject->setEnd(arg);
}
};

同时我们修改update()函数,确保GUI对Interval对象发出的通告做出响应

void update(Observable *observed)
{
Q_UNUSED(observed) m_endField->setText(m_subject->getEnd());
}

这是另外一个需要直接访问文本框的地方,如果我们这里不直接访问,采用setEnd()本身,那么我们的GUI控件将永远更新不到,并且程序本身会进入无限递归。总结来说,在这个重构步骤中真正需要接触GUI空间本身的就两个地方:

  1. 在事件处理函数中,为了获得GUI控件的最新值,必须通过控件本身去获取,不然如果你通过获取Model去获取,此时的Model依然是之前的那个值。
  2. 在最终得到更新的时候,要去修改GUI控件的值的时候,必须调用控件的set而不是你封装的set,不然除了控件得不到更新之外你还会进入无限循环。

总结来看,一个就是用户去接触GUI的那一刻,你需要去拿最新数据的时候,还有一个就是用最终set的时候,你需要真正set到GUI控件本身。这两个地方需要特别注意,必须直接操作,而不是调用间接委托函数。

现在我们可以编译并测试,数据都被恰如其分的复制了。另外两个文本框我们也如法炮制,完成之后,我们就可以运用Move Methd将calculateEnd()和calculateLength()搬移到Interval这个Model中去,这么一来我们就拥有了一个包容Model数据和行为并且与GUI分离的专属Model了。如果我们完成了上述重构,我们还可以做更夸张的事情就是我们可以完全摆脱这个GUI,去调用更新的GUI控件,让显示效果可以更好,这个绝对是我们不进行本次重构之前很难做到的。

当然了,有些时候可能你不想使用Observer模式,你可以使用事件监听器来同样完成Duplicate Observed Data。这种情况下你需要在Model类中建立一个监听器类和事件类,你需要对Model注册监听器,就像之前Observable对象注册Observer一样,每当Model发生变化(类似上述update()被调用),就向监听器发送一个事件,IntervalWindow可以使用一个内嵌类来实现监听器接口,并在适当的时候调用适当的update()。

『重构--改善既有代码的设计』读书笔记---Duplicate Observed Data的更多相关文章

  1. 『重构--改善既有代码的设计』读书笔记----Extract Method

    在编程中,比较忌讳的一件事情就是长函数.因为长函数代表了你这段代码不能很好的复用以及内部可能出现很多别的地方的重复代码,而且这段长函数内部的处理逻辑你也不能很好的看清楚.因此,今天重构第一个手法就是处 ...

  2. 『重构--改善既有代码的设计』读书笔记----Change Value to Reference

    有时候你会认为某个对象应该是去全局唯一的,这就是引用(Reference)的概念.它代表当你在某个地点对他进行修改之后,那么所有共享他的对象都应该在再次访问他的时候得到相应的修改.而不会像值对象(Va ...

  3. 『重构--改善既有代码的设计』读书笔记----Replace Method with Method Object

    有时候,当你遇到一个大型函数,里面的临时变量和参数多的让你觉得根本无法进行Extract Method.重构中也大力的推荐短小函数的好处,它所带来的解释性,复用性让你收益无穷.但如果你遇到上种情况,你 ...

  4. 『重构--改善既有代码的设计』读书笔记----Replace Array with Object

    如果你有一个数组,其中的元素各自代表不同东西,比如你有一个 QList<QString> strList; 其中strList[0]代表选手姓名,strList[1]代表选手家庭住址,很显 ...

  5. 『重构--改善既有代码的设计』读书笔记----Self Encapsulate Field

    如果你直接访问一个字段,你就会和这个字段直接的耦合关系变得笨拙.也就是说当这个字段权限更改,或者名称更改之后你的客户端代码都需要做相应的改变,此时你可以为这个字段建立设值和取值函数并且只以这些函数来访 ...

  6. 『重构--改善既有代码的设计』读书笔记----Move Method

    明确函数所在类的位置是很重要的.这样可以避免你的类与别的类有太多耦合.也会让你的类的内聚性变得更加牢固,让你的整个系统变得更加整洁.简单来说,如果在你的程序中,某个类的函数在使用的过程中,更多的是在和 ...

  7. 『重构--改善既有代码的设计』读书笔记----Replace Data Value with Object

    当你在一个类中使用字段的时候,发现这个字段必须要和其他数据或者行为一起使用才有意义.你就应该考虑把这个数据项改成对象.在开发初期,我们对于新类中的字段往往会采取简单的基本类型形式来保存,但随着我们开发 ...

  8. 『重构--改善既有代码的设计』读书笔记----Substitute Algorithm

    重构可以把复杂的东西分解成一个个简单的小块.但有时候,你必须壮士断腕删掉整个算法,用简单的算法来取代,如果你发现做一件事情可以有更清晰的方式,那你完全有理由用更清晰的方式来解决问题.如果你开始使用程序 ...

  9. 『重构--改善既有代码的设计』读书笔记----Introduce Foreign Method

    当你无法获得一个类的源代码或者没有权限去修改这个类的时候,你对于这种为你服务的类,你可能会出现需要别的需求的时候,比如一个Date类,你需要能够让他本身直接返回出他的后一天的对象,但他没有,这个时候你 ...

随机推荐

  1. 28个Unix/Linux的命令行神器

    下面是Kristóf Kovács收集的28个Unix/Linux下的28个命令行下的工具(原文链接),有一些是大家熟悉的,有一些是非常有用的,有一些是不为人知的.这些工具都非常不错,希望每个人都知道 ...

  2. Get familiar with key Frameworks of ios

    Frameworks make your life easier as an iOS Developer. They allow you to reuse code written by other ...

  3. ubuntu 交换ctrl与caps lock 键

    The relevant option is no longer available in the settings menu in Ubuntu 13.10; this has been repor ...

  4. In Java, what is the default location for newly created files?

    If the current directory of the application. If e.g. you create a File by using new FileOutputStream ...

  5. java实现简单的文件筛选

    package filenameFilter; import java.io.File; /* * 实现功能: * 获取指定路径下的指定格式的文件; * */ public class Test { ...

  6. 手机终于能连接android studio

    折腾了三天,终于能连上了,网上各种方法都试了,就是不行,结果安装了腾迅安全管家,自动安装了手机驱动就好了,原来一直都是驱动惹的祸啊......

  7. CSS3实现兼容性的渐变背景效果

    一.CSS3实现兼容性渐变背景效果,兼容FF.chrome.IE 渐变效果,现在主流的浏览器FF.Chrome.Opera.IE8+都可以通过带有私有前缀的CSS3属性来轻松滴实现渐变效果,IE7及以 ...

  8. [C#] 常用工具类——文件操作类

    /// <para> FilesUpload:工具方法:ASP.NET上传文件的方法</para> /// <para> FileExists:返回文件是否存在&l ...

  9. MVC-@html.ActionLink的几种参数格式

    一 Html.ActionLink("linkText","actionName") 该重载的第一个参数是该链接要显示的文字,第二个参数是对应的控制器的方法, ...

  10. HDU1711-----Number Sequence-----裸的KMP

    题目地址:http://acm.hdu.edu.cn/showproblem.php?pid=1711 题目意思: 找出b在a中的起始位置,没有则是-1 解题思路: 裸的KMP,不多说 不会KMP的话 ...