『重構--改善既有代碼的設計』讀書筆記---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()。

相關文章
相關標籤/搜索