Try   HackMD

守則 4 - 先找出三個例子, 才能改用通用的做法

大部分情況下,我們會傾向找出更通用的做法

譬如以下的例子

Sign * findRedSign(const vector<Sign *> & signs) { for (Sign * sign : signs) if (sign->color() == Color::Red) return sign; return nullptr; }

我們可能會想說,上面的程式適用的範圍是否太窄?
或許我們可以讓他支援搜尋其他顏色的號誌

Sign * findSignByColor(const vector<Sign *> & signs, Color color) { for (Sign * sign : signs) if (sign->color() == color) return sign; return nullptr; }

感覺好像還不夠?
說不定某天我們會需要透過「位置、距離、號誌上的文字」來搜尋?
來寫個通用一點的

struct SignQuery { SignQuery() : m_colors(), m_location(), m_distance(FLT_MAX), m_textExpression(".*") { ; } bool matchSign(const Sign * sign) const { return matchColors(m_colors, sign->color()) && matchLocation(m_location, m_distance, sign->location()) && regex_match(sign->text(), m_textExpression); } vector<Color> m_colors; Location m_location; float m_distance; regex m_textExpression; }; Sign * findSign(const SignQuery & query, const vector<Sign *> & signs) { for (Sign * sign : signs) if (query.matchSign(sign)) return sign; return nullptr; } Sign * findRedSign(const vector<Sign *> & signs) { SignQuery query; query.m_colors = { Color::Red }; return findSign(query, signs); }

這樣就可以根據號誌的「顏色、位置、距離、文字」來搜尋了!!!
讚讚!

YAGNI (You Aren't Gonna Need It)

上面通用的程式,問題在哪裡?

通用程式的撰寫是根據「想象中」的需求而不是「實際」的需求寫出來的。這樣會有什麼問題?

  1. 多數的程式碼可能永遠用不到
  2. 通用的程式比較複雜增加理解困難
  3. 參數怎麽傳遞
  4. 通用程式有什麼假設?
  5. 通用的程式比較複雜,所以也比較難修改
  6. 浪費時間去讓程式符合不存在的需求

下面這個做法,才是找出一個紅色號誌的最佳做法

Sign * findRedSign(const vector<Sign *> & signs) { for (Sign * sign : signs) if (sign->color() == Color::Red) return sign; return nullptr; }

等你拿到了另一個使用案例,再去寫程式解決那個使用案例的情況吧。
請不要嘗試去猜測 ,你所遇到的第二個使用案例會是什麼樣子。
專心寫程式,去解決你能理解的問題就好!

等到有多個類似的案例(超過三)再考慮寫通用的解法

比 YAGNI (你根本用不到 )更糟糕的 問題

當我們遇到沒考慮到的實際案例時,想直接套用原本的程式碼會更加困難

譬如以下案例

  • 找出一個紅色的號誌
  • 在中正路和中山路的轉彎處,找出一個紅色的「停車」號誌
  • (需改程式)找出中正路上所有的紅色或綠色號誌
  • (需改程式)找出市民大道或水南路上所有包含「公里」文字的白色號誌
  • (需改程式)在磨坊街902號附近找出一個藍色的號誌,或是包含「巷」這個文字的號誌

通用解法改完的程式碼
https://github.com/the-rules-of-programming/examples/blob/5dbda4387361350c886cbb1d2156217c5f2b9cd1/rule4.cpp#L429

問題在哪裡?

  1. 因為通用程式很複雜,改動的時候必須小心翼翼
  2. 被限制在通用程式的框架下導致程式越改越複雜
  3. 當你手裡拿著錘子時,所有的東西看起來都好像是釘子。你會傾向使用通用案例去解決現有的問題,而不是找出更好的方式解決

(上面讓我想到 HasParents#build_with_parents

比較一下:處理兩種做法

void example() { SignQuery * blueQuery = new SignQuery; blueQuery->m_colors = { Color::Blue }; SignQuery * locationQuery = new SignQuery; locationQuery->m_areas = { mainStreet }; SignQuery query; query.m_boolean = SignQuery::Boolean::Or; query.m_queries = { blueQuery, locationQuery }; vector<Sign *> locationSigns = findSigns(query, signs); }

這種做法更符合 KISS 的精神,好懂也好維護

vector<Sign *> locationSigns; for (Sign * sign : signs) { if (sign->color() == Color::Blue || matchArea(mainStreet, sign->location())) { locationSigns.push_back(sign); } }