複雜性是規模擴展的大敵
你知道的,程式碼越簡單越好
如果真的無法消除複雜性,那就把它隔離開來
譬如說要計算 sin 的時候,我們只要直接去呼叫 sin 函數就好而不用去管內部的實作
實際上計算 sin 的實作非常的複雜,但是因為他的介面簡單,所以我們用起來也不會很複雜
v1
這裡的複雜度在於我們要排除無效的客戶
如果有一天判斷無效客戶的方式更改了那麼這裡面的程式碼也要跟著做修正
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 函數裡面
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 函數會不會比較好?
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);
}
}
}
這邊我們要建構一款捉迷藏遊戲,我們想要在螢幕上顯示一個小小的「眼睛圖示」
如果敵人全部都看不到玩家眼睛就會閉起來,
但如果有敵人可以看到玩家這個眼睛就會睜開
閉上眼睛就表示玩家是安全的
睜開眼睛表示玩家有被發現的風險
我們來看一下程式碼
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();
}
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();
}
如果遇到起霧的天氣眼睛圖示就要睜開
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();
}
}
從上述的程式我們可以發現每次我們新增一個條件我們就要在「五個地方」同步做修改來實現這個邏輯
這樣做有一個相當大的問題,就是他無法無法把複雜性局限在局部範圍內
但是其實總共只有三個變因會影響眼睛圖示
只要滿足下面三個條件,眼睛圖示就應該閉上
讓我們重構這段程式
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 原則 (開放擴充封閉修改)
先跟你分享一個我曾經遇到的需求。
Apr 23, 2025進入 Line Office Account,在帳號一覽選擇你要使用 Message API 的 Official Account設定 > Messaging API > 啟用 Messaging API
Apr 23, 2025如何消除掉那種實際上可以避免的狀況(或者說是「使用上的錯誤」,像是在關閉檔案之後又想要寫入檔案,或是在物件完成初始化之前就去調用那個物件裡的方法)
May 22, 2024大部分情況下,我們會傾向找出更通用的做法
Apr 25, 2024or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up