『重构--改善既有代码的设计』读书笔记---Duplicate Observed Data

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

  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模式中对于数据更新存在“推“和”拉“两种方式,这里介绍的是的“拉”数据)
  • 编译,测试。

例子:this

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

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作对应的焦点处理code

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();
    }
}

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

void startField_focusLost()
{
    bool ok; 
    int num = m_startField->getText().toInt(&ok);
    
    if (ok)
    {
    }
    else
    {
        m_startField->setText("0");
    }

    calculateLength();
}

void endField_focusLost()
{
    bool ok; 
    int num = m_endField->getText().toInt(&ok);
    
    if (ok)
    {
    }
    else
    {
        m_endField->setText("0");
    }

    calculateLength();
}

void lengthField_focusLost()
{  
    bool ok; 
    int num = m_lengthField->getText().toInt(&ok);
    
    if (ok)
    {
    }
    else
    {
        m_lengthField->setText("0");
    }

    calculateEnd();
}

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

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(0));
    }

    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(0));
    }

    calculateLength();
}

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

    private:
        QString m_end;

在这里咱们给他的初值和GUI给他的初值是同样的,而后咱们再加入取值/设值函数结果以下

class Interval : public Observable
{
    public:
        Interval() :
            m_end("0")
    {
    }
        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()。

相关文章
相关标签/搜索