# 把複雜性局限在局部範圍內 複雜性是規模擴展的大敵 你知道的,程式碼越簡單越好 如果真的無法消除複雜性,那就把它隔離開來 ## 一個簡單的範例 譬如說要計算 sin 的時候,我們只要直接去呼叫 sin 函數就好而不用去管內部的實作 實際上計算 sin 的實作非常的複雜,但是因為他的介面簡單,所以我們用起來也不會很複雜 ## 隱藏內部的細節 v1 這裡的複雜度在於我們要排除無效的客戶 如果有一天判斷無效客戶的方式更改了那麼這裡面的程式碼也要跟著做修正 ```cpp= void findRecentPurchasers( const vector<Customer *> &customers, Date startingDate, vector<Customer *> *recentCustomers) { Date currentDate = getCurrentDate(); for (Customer *customer : customers) { if (customer->m_validFrom >= currentDate && customer->m_validUntil <= currentDate && !customer->m_isClosed && customer->m_lastPurchase >= startingDate) { recentCustomers->push_back(customer); } } } ``` v2 這邊把判斷客戶是否有效封裝進 isValid 函數裡面 ```cpp= void findRecentPurchasers( const vector<Customer *> &customers, Date startingDate, vector<Customer *> *recentCustomers) { Date currentDate = getCurrentDate(); for (Customer *customer : customers) { if (customer->isValid() & customer->m_lastPurchase >= startingDate) { recentCustomers->push_back(customer); } } } ``` v3 最好的方式是從上游著手,就是上游直接傳有效客戶 validCustomers 進來即可 Q:那上游如果傳的客戶名單包含無效客戶要怎麼辦? 寫一個 getValidCustomers 函數會不會比較好? ```cpp= void findRecentPurchasers( const vector<Customer *> & validCustomers, Date startingDate, vector<Customer *> * recentCustomers) { Date currentDate = getCurrentDate(); for (Customer * customer : validCustomers) { if (customer->m_lastPurchase >= startingDate) { recentCustomers->push_back(customer); } } } ``` ## 分散各處的狀態與複雜性的關係 這邊我們要建構一款捉迷藏遊戲,我們想要在螢幕上顯示一個小小的「眼睛圖示」 如果敵人全部都看不到玩家眼睛就會閉起來, 但如果有敵人可以看到玩家這個眼睛就會睜開 閉上眼睛就表示玩家是安全的 睜開眼睛表示玩家有被發現的風險 我們來看一下程式碼 ```cpp= class Player : public Character, public AwarenessEvents { public: Player(); void onSpotted(Character * otherCharacter) override; void onLostSight(Character * otherCharacter) override; protected: int m_spottedCount; }; Player::Player() : m_spottedCount(getAwarenessManager()->getSpottedCount(this)) { if (m_spottedCount == 0) getEyeIcon()->close(); getAwarenessManager()->subscribe(this, this); } void Player::onSpotted(Character * otherCharacter) { if (m_spottedCount == 0) getEyeIcon()->open(); ++m_spottedCount; } void Player::onLostSight(Character * otherCharacter) { --m_spottedCount; if (m_spottedCount == 0) getEyeIcon()->close(); } ``` ## 加上失去行動能力的條件 ```cpp= Player::Player() : m_status(STATUS::Normal), m_spottedCount(getAwarenessManager()->getSpottedCount(this)) { if (m_spottedCount == 0) getEyeIcon()->close(); getAwarenessManager()->subscribe(this, this); } void Player::setStatus(STATUS status) { if (status == m_status) return; if (m_spottedCount == 0) { if (status == STATUS::Normal) getEyeIcon()->close(); else if (m_status == STATUS::Normal) getEyeIcon()->open(); } m_status = status; } void Player::onSpotted(Character * otherCharacter) { if (m_spottedCount == 0 && m_status == STATUS::Normal) getEyeIcon()->open(); ++m_spottedCount; } void Player::onLostSight(Character * otherCharacter) { --m_spottedCount; if (m_spottedCount == 0 && m_status == STATUS::Normal) getEyeIcon()->close(); } ``` ## 加上起霧的條件 如果遇到起霧的天氣眼睛圖示就要睜開 ```cpp= Player::Player() : m_status(STATUS::Normal), m_spottedCount(getAwarenessManager()->getSpottedCount(this)) { if (m_spottedCount == 0 && getWeatherManager()->getCurrentWeather() != WEATHER::Foggy) { getEyeIcon()->close(); } getAwarenessManager()->subscribe(this, this); getWeatherManager()->subscribe(this); } void Player::setStatus(STATUS status) { if (status == m_status) return; if (m_spottedCount == 0 && getWeatherManager()->getCurrentWeather() != WEATHER::Foggy) { if (status == STATUS::Normal) getEyeIcon()->close(); else if (m_status == STATUS::Normal) getEyeIcon()->open(); } m_status = status; } void Player::onSpotted(Character * otherCharacter) { if (m_spottedCount == 0 && m_status == STATUS::Normal && getWeatherManager()->getCurrentWeather() != WEATHER::Foggy) { getEyeIcon()->open(); } ++m_spottedCount; } void Player::onLostSight(Character * otherCharacter) { --m_spottedCount; if (m_spottedCount == 0 && m_status == STATUS::Normal && getWeatherManager()->getCurrentWeather() != WEATHER::Foggy) { getEyeIcon()->close(); } } void Player::onWeatherChanged(WEATHER oldWeather, WEATHER newWeather) { if (m_spottedCount == 0 && m_status == STATUS::Normal) { if (oldWeather == WEATHER::Foggy) getEyeIcon()->close(); else if (newWeather == WEATHER::Foggy) getEyeIcon()->open(); } } ``` ## 重新思考作法 從上述的程式我們可以發現每次我們新增一個條件我們就要在「五個地方」同步做修改來實現這個邏輯 這樣做有一個相當大的問題,就是他無法無法把複雜性局限在局部範圍內 但是其實總共只有三個變因會影響眼睛圖示 只要滿足下面三個條件,眼睛圖示就應該閉上 1. 沒有任何敵人看到玩家 2. 玩家沒有喪失行動能力 3. 天氣沒有起霧 讓我們重構這段程式 ```cpp= Player::Player() : m_status(STATUS::Normal) { refreshStealthIndicator(); getAwarenessManager()->subscribe(this, this); getWeatherManager()->subscribe(this); } void Player::setStatus(STATUS status) { m_status = status; refreshStealthIndicator(); } void Player::onSpotted(Character * otherCharacter) { refreshStealthIndicator(); } void Player::onLostSight(Character * otherCharacter) { refreshStealthIndicator(); } void Player::onWeatherChanged(WEATHER oldWeather, WEATHER newWeather) { refreshStealthIndicator(); } void Player::refreshStealthIndicator() { if (m_status == STATUS::Normal && getAwarenessManager()->getSpottedCount(this) == 0 && getWeatherManager()->getCurrentWeather() != WEATHER::Foggy) { getEyeIcon()->close(); } else { getEyeIcon()->open(); } } ``` 經過重構之後我們可以發現,現在複雜性被限制在 refreshStealthIndicator 裡面 這樣也讓可讀性增加許多。 如果要新增新的條件,只要在 refreshStealthIndicator 添加一個檢查就可以了 如果我們有 10 個條件,那之前的做法可能會需要比現在的程式碼多上 10 倍左右 之前寫的程式碼具有二次複雜性(quadratic complexity) 這種設計實作出來的程式碼增加的行數與條件數量平方成正比 如果程式碼的複雜度呈現二次方增長的趨勢,那你很快就會遇到瓶頸了 ## 把複雜性局限在局部範圍內,只進行簡單的互動 要避免的是就是讓系統的不同部分進行複雜的互動,你還是可以接受一些複雜的細節, 但只要可以把複雜性局限在局部範圍內就可以了。 就算是內部細節很複雜的元件,只要有簡單的介面就可以進行簡單的互動,這樣絕對不會讓你的專案掛掉 如果每次添加新的功能,就必須在很多地方寫程式碼那肯定就是一個不好的跡象 這個剛好可以呼應 open closed 原則 (開放擴充封閉修改)