重構實例介紹 – 分析篇

重構是什麼?這問題其實真的很難回答。

我個人覺得重構是一種讓程式保持活力的一種方法,讓它能隨著時間而不斷地進化。如果我們一直放任自己不對程式碼做適當的整理,而是靠著不求甚解的修修補補來維繫它的生命,很快地程式碼就會變得殘破不堪、臃腫肥大而難以維護。

而且有的時候,我們也想讓程式碼能隨著我們觀念的增長,以適應未來的變化;今天我們會覺得這樣的寫法很讚,但明天可能又會學到更好的寫法,重構就能給我們改變程式碼的機會。

但是這些都是很籠統的解釋,有沒有什麼方法可以讓我們更瞭解重構呢?我想,只有用實際的例子來說明是最直接的吧。但是太精簡的範例可能表達不了重構的意圖,而過於複雜的範例又會讓重構的焦點模糊,要找到一個適合的例子可能比重構本身還困難。

後來某次的改版機會下,我分析了伙伴之前所製作的功能並做了一次重構,發現這個功能的規模不算太大,而且也很容易展現出重構後的優點;因此,我便將這個功能稍微做了簡化以方便說明,希望能讓大家瞭解重構究竟是在做些什麼。

不過這個範例雖然不大,但也還是需要一番功夫來解說;因此我將會把它分成兩個部份來說明,第一篇是分析,第二篇則是實戰。

接下來就一起來看看這個例子吧。

需求說明

這是一個裝修問卷的需求,基本上是在收集客戶想要幫房子裝修的資訊,要注意的重點如下:

  • 裝修類型是指常見的房屋裝修重點,例如廚房、浴室或地板等。
  • 裝修類型包含了客戶基本資訊以及裝修內容。
  • 一個裝修問卷只會對應一種裝修類型,同時也只記錄一位客戶。
  • 裝修內容則包含裝修類型裡會需要裝修的項目,像是廚房會需要換裝瓦斯爐、上下櫃等。
  • 隨著裝修類型的不同,裝修內容也會不同。
  • 未來會陸續新增裝修類型,因此裝修的內容也會有所變動。

以下是這個需求所提供的樣板,表單頁:

完成頁:

在伙伴的努力下,現在這個功能已經達到客戶的需求並上線一段時間了,維護也都是交由伙伴自己來進行;但在某次系統改版時,為了能對系統做出整體改版計劃,所以我得瞭解這個功能細部的狀況。

以下就讓我們一起來從這個完成後的版本開始看起吧。

註:你可以在我的 GitHub 上找到完整的程式碼

程式解說

在重構之前,一定要先瞭解我們即將要重構的程式碼。如果完全不曉得它是做什麼的,那麼重構就會存在著一定的風險。

由於我們是採用 Zend Framework 來建置這個專案,因此程式將分成 Controller 、 View 及 Model 三個部份。以下我們先對它們的作用一一說明,然後再分析它們為何需要重構。

Controller

Controller 的部份主要只有 IndexController 這個類別:

[/refactoring_sample/application/controllers/IndexController.php]
class IndexController extends Zend_Controller_Action
{
    // ...
}

它包含了 index 、 step2 及 step3 三個 action ,也是本功能主要的流程。

indexAction() 方法只是單純地顯示連結用的 template 而已,沒有實作任何程式碼。

    public function indexAction()
    {
    }

step2Action() 方法負責表單的顯示與處理表單回傳的資料。這裡我們會先接收 decoration 參數,以決定要後面的程式要處理的裝修類型。

接著再透過 Request 物件的 isPost() 方法,來判斷是否是 PostBack 。如果是 PostBack 的話就先檢查表單資料,正確就寫入資料表,反之則顯示錯誤訊息。

註:這裡為了減少篇幅,故省略掉了找不到裝修類型的錯誤判斷。

    public function step2Action()
    {
        $decorationName = strtolower($this->getRequest()->getParam('decoration'));
        $error = false;
        $messenger = $this->_helper->flashMessenger;
        /* @var $messenger Zend_Controller_Action_Helper_FlashMessenger */
        if ($this->getRequest()->isPost()) {
            $filter = new Zend_Filter_StripTags();
            $callback = array($filter, 'filter');
            $formData = array_map($callback, $this->getRequest()->getPost());
            $formData = array_map('trim', $formData);
            // 檢查表單必填值
            $checkFunctionName = '_check' . ucwords(strtolower($decorationName)) . 'FormData';
            $this->$checkFunctionName($formData, $error, $messenger);
            if (!$error) {
                $decorationTable = $this->_createDecorationTable($decorationName);
                $decorationRow = $decorationTable->createRow($formData);
                $decorationRow->save();
                $this->_helper->redirector->gotoSimple('step3', null, null, array(
                    'decoration' => $decorationName,
                    'id' => $decorationRow->id,
                ));
            } else {
                $params = array(
                    'decoration' => $decorationName,
                );
                $this->_helper->redirector->gotoSimple('step2', null, null, $params);
            }
        }
        $this->view->decorationName = $decorationName;
        $this->view->decorationDisplayName =
                $this->_decorationDisplayNameList[strtolower($decorationName)];
        $this->view->messages = $messenger->getMessages();
    }

而且因為我們已經知道裝修類型會隨著時間而增多,因此伙伴在撰寫程式時,已經考慮兩個可能會變化的部份。

第一個是在檢查表單內容的部份;在 step2Action() 方法接收了表單資料後,我們透過參數取得的裝修類型名稱來決定要呼叫哪個檢查方法。但因為目前我們只有 kitchen 這個裝修類型,所以在 IndexController 類別中只有 _checkKitchenFormData() 這個方法。

    public function step2Action()
    {
       // ...
            $checkFunctionName = '_check' . ucwords(strtolower($decorationName)) . 'FormData';
            $this->$checkFunctionName($formData, $error, $messenger);
       // ...
    }
    protected function _checkKitchenFormData($formData, &$error, Zend_Controller_Action_Helper_FlashMessenger &$messenger)
    {
        if (0 === strlen($formData['name'])) {
            $error = true;
            $messenger->addMessage('請輸入姓名');
        }
        if (0 === strlen($formData['phone'])) {
            $error = true;
            $messenger->addMessage('請輸入電話');
        }
        if (0 === strlen($formData['address'])) {
            $error = true;
            $messenger->addMessage('請輸入地址');
        }
        if (!array_key_exists('kitchenQuestion01', $formData)
                && !array_key_exists('kitchenQuestion02', $formData)) {
            $error = true;
            $messenger->addMessage('請選擇裝修內容');
        }
        if (!array_key_exists('kitchenQuestion03', $formData)
                && !array_key_exists('kitchenQuestion04', $formData)) {
            $error = true;
            $messenger->addMessage('請選擇設備是否保留');
        }
        if (!array_key_exists('kitchenQuestion05', $formData)) {
            $error = true;
            $messenger->addMessage('請選擇現有廚具');
        }
    }

另一個是建立資料表的部份,這裡是透過 _createDecorationTable() 這個方法來動態決定要使用哪個資料表,也就是一般常見的工廠方法。

    protected function _createDecorationTable($decorationName)
    {
        $tableName = 'Application_Model_DbTable_Decoration' . ucfirst($decorationName) . 's';
        return new $tableName();
    }
    public function step2Action()
    {
        // ...
        $decorationTable = $this->_createDecorationTable($decorationName);
        // ...
    }

step3Action() 方法則是用來顯示完成後的頁面。這裡首先會取得 step2Action() 方法新增的自動編號,然後才從資料表裡取得對應的問卷資料來顯示。

    public function step3Action()
    {
        $decorationName = strtolower($this->getRequest()->getParam('decoration'));
        $decorationId = (int) $this->getRequest()->getParam('id');
        $decorationTable = $this->_createDecorationTable($decorationName);
        $decorationRow = $decorationTable->find($decorationId)->current();
        if (!$decorationRow) {
            $this->_redirect('/');
        }
        $this->view->decorationName = $decorationName;
        $this->view->decorationDisplayName =
                $this->_decorationDisplayNameList[strtolower($decorationName)];
        $this->view->decorationRow = $decorationRow;
        $this->view->decorationMap = $this->_exportMapData($decorationName);
        $this->view->actionController = $this;
    }

與 step2Action() 方法相同,在 step3Action() 方法也呼叫了 _createDecorationTable() 方法來動態取得對應的資料表,接著再透過 id 參數取得在 step2Action() 方法新增的資料列。

因為存在資料表裡的資料並不包含中文訊息,因此我們需要將它做轉換。這個動作原本是可以在樣版裡做的,但為了不在樣版裡有太多的判斷式,因此採用了名稱對照表的方法。

_exportMapData() 方法會從 application/controllers/DecorationMap 資料夾下載入對應的名稱對照表 (即 kitchen.php) ,然後將它 assign 到 view 的 decorationMap 變數。

    protected function _exportMapData($decorationName)
    {
        $includeFile = __DIR__ . '/DecorationMap/' . $decorationName . '.php';
        if (!file_exists($includeFile)) {
            $this->_redirect('/');
        } else {
            $decorationMap = include($includeFile);
            return $decorationMap;
        }
    }

名稱對照表內容如下:

[/refactoring_sample/application/controllers/DecorationMap/kitchen.php]
return array(
    'kitchenQuestion01' => array(
        'y' => '吊櫃',
        'n' => '',
    ),
    'kitchenQuestion02' => array(
        'y' => '下櫃',
        'n' => '',
    ),
    'kitchenQuestion03' => array(
        'y' => '瓦斯爐',
        'n' => '',
    ),
    'kitchenQuestion04' => array(
        'y' => '排油煙機',
        'n' => '',
    ),
    'kitchenQuestion05' => array(
        '1' => '無',
        '2' => '泥作',
        '3' => '組合式',
        '4' => '歐化',
        '5' => '不鏽鋼',
    ),
);

另外 IndexController 類別裡也定義了一個 $_decorationDisplayNameList 屬性,它的目的是存放 decoration 參數所對應的中文名稱。因此在 step2Action() 方法及 step3Action() 方法中都會先找出裝修類型所對應的中文名稱,再 assign 到 view 的 decorationDisplayName 變數中。

[/refactoring_sample/application/controllers/IndexController.php]
    protected $_decorationDisplayNameList = array(
        'kitchen' => '廚房',
    );
    // ...
    public function step2Action()   {
        // ...
        $this->view->decorationDisplayName =
                $this->_decorationDisplayNameList[strtolower($decorationName)];
        // ...
    }

接下來就是 View 的部份。

View

View 的部份除了三個對應 action 的 template scripts 外,還有裝修類型所對應的裝修內容表單及完成頁。

index.phtml 為對應 indexAction() 方法的樣版,顯示表單的連結。

[/refactoring_sample/application/views/scripts/index/index.phtml]
<h1>房屋裝修</h1>
<ul>
    <li><a href="<?php
echo $this->url(array(
    'controller' => 'index',
    'action' => 'step2',
    'decoration' => 'kitchen',
)); ?>">廚房</a></li>
</ul>

step2.phtml 為對應 step2Action() 方法的樣版,用來顯示表單;因為我們已經預知每個裝修內容的選項是不一樣的,因此將會變化的子表單部份獨立成子樣版。

[/refactoring_sample/application/views/scripts/index/step2.phtml]
<h1><?php echo $this->escape($this->decorationDisplayName); ?></h1>
<form action="" method="post">
    <fieldset>
        <legend>連絡資訊</legend>
        <p><label for="name">姓名</label><input type="text" name="name" id="name" /></p>
        <p><label for="phone">電話</label><input type="text" name="phone" id="phone" /></p>
        <p><label for="address">住址</label><input type="text" name="address" id="address" /></p>
    </fieldset>
    <fieldset>
        <legend>裝修選項</legend>
        <?php echo $this->partial('index/' . $this->decorationName . '/form.phtml'); ?>
    </fieldset>
    <p><button type="submit">完成送出</button></p>

每個會變化的子表單都放在同名的裝修類型資料表下,例如廚房的部份就是:

[/refactoring_sample/application/views/scripts/index/kitchen/form.phtml]
<dl>
    <dt>裝修內容</dt>
    <dd>
        <p>
            <label><input type="checkbox" name="kitchenQuestion01" value="y" /><span>吊櫃</span></label>
            <label><input type="checkbox" name="kitchenQuestion02" value="y" /><span>下櫃</span></label>
        </p>
    </dd>
    <dt>設備是否保留</dt>
    <dd>
        <p>
            <label><input type="checkbox" name="kitchenQuestion03" value="y" /><span>瓦斯爐</span></label>
            <label><input type="checkbox" name="kitchenQuestion04" value="y" /><span>排油煙機</span></label>
        </p>
    </dd>
    <dt>現有廚具</dt>
    <dd>
        <p>
            <label><input type="radio" name="kitchenQuestion05" value="1" /><span>無(需新製)</span></label>
            <label><input type="radio" name="kitchenQuestion05" value="2" /><span>泥作</span></label>
            <label><input type="radio" name="kitchenQuestion05" value="3" /><span>組合式(分件廚具)</span></label>
            <label><input type="radio" name="kitchenQuestion05" value="4" /><span>歐化(木質桶身)</span></label>
            <label><input type="radio" name="kitchenQuestion05" value="5" /><span>不鏽鋼桶身</span></label>
        </p>
    </dd>
</dl>

另外 step2.phtml 也會在表單資料驗證錯誤時,將對應的錯誤訊息顯示出來。

[/refactoring_sample/application/views/scripts/index/kitchen/form.phtml]
    <?php if (!empty($this->messages)) : ?>
    <ul>
    <?php foreach ($this->messages as $message) : ?>
        <li><?php echo $this->escape($message); ?></li>
    <?php endforeach; ?>
    </ul>
    <?php endif; ?>
</form>

step3.phtml 為對應 step3Action() 方法的樣版,用來顯示完成頁;這裡也和 step2Action() 方法一樣用子樣版來顯示不同的裝修類型結果。

[/refactoring_sample/application/views/scripts/index/step3.phtml]
<h1><?php echo $this->escape($this->decorationDisplayName); ?></h1>
<h2>連絡資訊</h2>
<ul>
    <li>姓名: <?php echo $this->escape($this->decorationRow->name); ?></li>
    <li>電話: <?php echo $this->escape($this->decorationRow->phone); ?></li>
    <li>住址: <?php echo $this->escape($this->decorationRow->address); ?></li>
</ul>
<h2>裝修選項</h2>
<?php echo $this->partial('index/' . $this->decorationName . '/ok.phtml', null, $this); ?>

子樣版如下:

[/refactoring_sample/application/views/scripts/index/kitchen/ok.phtml]
<dl>
    <dt>裝修內容:</dt>
    <dd>
        <p>
            <?php echo $this->actionController->buildOptionString(
                    $this->decorationMap['kitchenQuestion01'],
                    $this->decorationRow['kitchenQuestion01']); ?>
            <?php echo $this->actionController->buildOptionString(
                    $this->decorationMap['kitchenQuestion02'],
                    $this->decorationRow['kitchenQuestion02']); ?>
        </p>
    </dd>
    <dt>設備是否保留:</dt>
    <dd>
        <p>
            <?php echo $this->actionController->buildOptionString(
                    $this->decorationMap['kitchenQuestion03'],
                    $this->decorationRow['kitchenQuestion03']); ?>
            <?php echo $this->actionController->buildOptionString(
                    $this->decorationMap['kitchenQuestion04'],
                    $this->decorationRow['kitchenQuestion04']); ?>
        </p>
    </dd>
    <dt>現有廚具:</dt>
    <dd>
        <p>
            <?php echo $this->actionController->buildOptionString(
                    $this->decorationMap['kitchenQuestion05'],
                    $this->decorationRow['kitchenQuestion05']); ?>
        </p>
    </dd>
</dl>

比較特別的是在 ok.html 這個子樣版裡,我們用到了定義在 IndexController 類別裡的 buildOptionString() 方法,以取得資料列欄位值對應到 decorationMap 的中文名稱。

Model

Model 的部份比較簡單,目前只有一個 Zend_Db_Table 類別。

[/refactoring_sample/application/models/DbTable/DecorationKitchens.php]
class Application_Model_DbTable_DecorationKitchens extends Zend_Db_Table_Abstract
{
    protected $_name = 'decoration_kitchens';
}

而對應的 database schema 如下:

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
CREATE DATABASE `refactoring` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE `refactoring`;
DROP TABLE IF EXISTS `decoration_kitchens`;
CREATE TABLE IF NOT EXISTS `decoration_kitchens` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `name` varchar(100) default NULL,
  `phone` varchar(100) default NULL,
  `cellphone` varchar(100) default NULL,
  `address` varchar(200) default NULL,
  `kitchenQuestion01` enum('y','n') default 'n',
  `kitchenQuestion02` enum('y','n') default 'n',
  `kitchenQuestion03` enum('y','n') default 'n',
  `kitchenQuestion04` enum('y','n') default 'n',
  `kitchenQuestion05` enum('1','2','3','4','5') default NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='廚房問卷';

以上就是第一版程式所需要的所有檔案和它們的運作原理。

加入新裝修類型

瞭解了程式碼之後,接著我們來看如何加入一個新的裝修類型;假設新的裝修類型是「浴室」 (Bathroom) ,也已經有了對應的 database schema 及樣版。

我們需要更動以下幾個部份:

註:以下將會以一般 patch 檔的方式來表示更動後的差異,即在程式碼的行頭加上加號 (+) 或減號 (-) 來分別表示新增程式碼或刪減程式碼;而行頭沒有特別標示的話,就表示沒有更動;至於移除檔案、資料夾、類別屬性或整個類別方法的部份,就不會再特別用程式碼標明,以節省篇幅。

第一步:新增 Bathroom 的 DbTable 類別並匯入 schema 到資料庫中。

[/refactoring_sample/application/models/DbTable/DecorationBathrooms.php]
+class Application_Model_DbTable_DecorationBathrooms extends Zend_Db_Table_Abstract
+{
+    protected $_name = 'decoration_bathrooms';
+}

資料表的 schema 如下:

DROP TABLE IF EXISTS `decoration_bathrooms`;
CREATE TABLE IF NOT EXISTS `decoration_bathrooms` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `name` varchar(100) default NULL,
  `phone` varchar(100) default NULL,
  `address` varchar(200) default NULL,
  `bathroomQuestion01` enum('1','2','3','4') default NULL,
  `bathroomQuestion02` enum('1','2','3','4') default NULL,
  `bathroomQuestion03` enum('1','2','3','4','5') default NULL,
  `bathroomQuestion04` enum('y','n') default 'n',
  `bathroomQuestion05` enum('y','n') default 'n',
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='衛浴問卷';

第二步:在 IndexController 類別的 $_decorationDisplayNameList 變數中加入新的中文名稱對應。

     protected $_decorationDisplayNameList = array(
         'kitchen' => '廚房',
+        'bathroom' => '浴室',
     );

第三步:在 IndexController 類別中新增 _checkBathroomFormData() 方法。

+    protected function _checkBathroomFormData($formData, &amp;$error, Zend_Controller_Action_Helper_FlashMessenger &amp;$messenger)
+    {
+        if (0 === strlen($formData['name'])) {
+            $error = true;
+            $messenger->addMessage('請輸入姓名');
+        }
+        if (0 === strlen($formData['phone'])) {
+            $error = true;
+            $messenger->addMessage('請輸入電話');
+        }
+        if (0 === strlen($formData['address'])) {
+            $error = true;
+            $messenger->addMessage('請輸入地址');
+        }
+        if (!array_key_exists('bathroomQuestion01', $formData)) {
+            $error = true;
+            $messenger->addMessage('請選擇坪數');
+        }
+        if (!array_key_exists('bathroomQuestion02', $formData)) {
+            $error = true;
+            $messenger->addMessage('請選擇馬桶');
+        }
+        if (!array_key_exists('bathroomQuestion03', $formData)) {
+            $error = true;
+            $messenger->addMessage('請選擇面盆');
+        }
+    }

第四步:在 application/controllers/DecorationMap 目錄下加入名稱對照檔。

[/refactoring_sample/application/controllers/DecorationMap/bathroom.php]
<?php
return array(
    'bathroomQuestion01' => array(
        '1' => '1 坪以下',
        '2' => '1 ~ 1.3 坪',
        '3' => '1.3 ~ 1.5 坪',
        '4' => '1.5 坪以上',
    ),
    'bathroomQuestion02' => array(
        '1' => '新製 - 一般馬桶',
        '2' => '新製 - 免治馬桶',
        '3' => '不需更換',
        '4' => '未決定',
    ),
    'bathroomQuestion03' => array(
        '1' => '新製 - 長柱',
        '2' => '新製  -短柱',
        '3' => '新製 - 單孔',
        '4' => '不需更換',
        '5' => '未決定',
    ),
);

第五步:加入 Bathroom 的子表單樣版及完成頁樣版。

[/refactoring_sample/application/views/scripts/index/bathroom/form.phtml]
<dl>
    <dt>坪數</dt>
    <dd>
        <p>
            <label><input type="radio" name="bathroomQuestion01" value="1" /><span>1 坪以下</span></label>
            <label><input type="radio" name="bathroomQuestion01" value="2" /><span>1 ~ 1.3 坪</span></label>
            <label><input type="radio" name="bathroomQuestion01" value="3" /><span>1.3 ~ 1.5 坪</span></label>
            <label><input type="radio" name="bathroomQuestion01" value="4" /><span>1.5 坪以上</span></label>
        </p>
    </dd>
    <dt>馬桶</dt>
    <dd>
        <p>
            <label><input type="radio" name="bathroomQuestion02" value="1" /><span>新製 - 一般馬桶</span></label>
            <label><input type="radio" name="bathroomQuestion02" value="2" /><span>新製 - 免治馬桶</span></label>
            <label><input type="radio" name="bathroomQuestion02" value="3" /><span>不需更換</span></label>
            <label><input type="radio" name="bathroomQuestion02" value="4" /><span>未決定</span></label>
        </p>
    </dd>
    <dt>面盆</dt>
    <dd>
        <p>
            <label><input type="radio" name="bathroomQuestion03" value="1" /><span>新製 - 長柱</span></label>
            <label><input type="radio" name="bathroomQuestion03" value="2" /><span>新製  -短柱</span></label>
            <label><input type="radio" name="bathroomQuestion03" value="3" /><span>新製 - 單孔</span></label>
            <label><input type="radio" name="bathroomQuestion03" value="4" /><span>不需更換</span></label>
            <label><input type="radio" name="bathroomQuestion03" value="5" /><span>未決定</span></label>
        </p>
    </dd>
</dl>
[/refactoring_sample/application/views/scripts/index/bathroom/ok.phtml]
<dl>
    <dt>坪數</dt>
    <dd>
        <p>
            <?php echo $this->actionController->buildOptionString(
                    $this->decorationMap['bathroomQuestion01'],
                    $this->decorationRow['bathroomQuestion01']); ?>
        </p>
    </dd>
    <dt>馬桶</dt>
    <dd>
        <p>
            <?php echo $this->actionController->buildOptionString(
                    $this->decorationMap['bathroomQuestion02'],
                    $this->decorationRow['bathroomQuestion02']); ?>
        </p>
    </dd>
    <dt>面盆</dt>
    <dd>
        <p>
            <?php echo $this->actionController->buildOptionString(
                    $this->decorationMap['bathroomQuestion03'],
                    $this->decorationRow['bathroomQuestion03']); ?>
        </p>
    </dd>
</dl>

第六步:最後在 index.phtml 加入 Bathroom 的新連結。

[/refactoring_sample/application/views/scripts/index/index.phtml]
 <h1>房屋裝修</h1>
 <ul>
     <li><a href="<?php
 echo $this->url(array(
     'controller' => 'index',
     'action' => 'step2',
     'decoration' => 'kitchen',
 )); ?>">廚房</a></li>
+    <li><a href="<?php
+echo $this->url(array(
+    'controller' => 'index',
+    'action' => 'step2',
+    'decoration' => 'bathroom',
+)); ?>">浴室</a></li>
 </ul>

接著我們來分析這樣的程式寫法有什麼缺點。

程式分析

從上面新增一個裝修類型的過程來看,除了新增必要的 DbTable 類別和樣版外,大部份的修改都集中在 IndexController 類別上;可以想見未來在不斷增加裝修類型後, IndexContrller 類別會越來越龐大。

物件導向開發中,有個很重要的原則就是 SRP (Single Responsibility Principle) ,但顯然 IndexController 類別已經違反了這個原則。原因就是我們把所有的邏輯都集中在 IndexController 類別的身上,使得它所背負的工作超過它原先擔任的角色。

所以這裡可以改進的地方在於,讓 IndexController 類別除了流程的調整外,儘可能不要因為新增裝修類型而有所更動。

另外 View 應該是直接輸出 Model 的資料,或是透過 View Helper 或樣版語法來處理資料的呈現;但是在 step3Action() 方法中,卻因為 View 需要轉換資料列欄位值的名稱,而呼叫了 IndexController 類別的 buildOptionString() 方法;這使得 View 層不適當地依賴了 Controller 層,造成程式碼維護上的困難。

而且名稱對應表也必須透過 IndexController 類別來載入,這使得我們在其他 Controller 中必須重複貼上載入檔案的程式碼,這也是一個可以改進的地方。

下一篇,我們就來針對這些問題一一重構吧!

繼續閱讀:重構實例介紹 - 實戰篇