A cohensive class does one thing really well and does not try to do or be something else.

來來來翻譯一下:一個 class 只做一件事,不會插手做別人的事。

但是,這「一件事」的大小跟規模是由設計的人定義的,可以很大也可以很小,定義清楚即可。

class 內聚力越高,class 間的耦合度(改動一個 class 就要改動其他 code 的程度)越低,越容易 reuse 及擴展。

讓一個 class 只做一件事,只有那件事情需要修改時才會讓這個 class 改變。這樣每次修改的影響範圍可以縮小,可能產生 bug 的範圍也就縮小啦。另一個好處是 debug 的時候,programmer 可以較迅速的知道可能出問題的範圍,降低時間成本。

檢查內聚力的方式:做一項改變時是否牽動到許多 class?是的話表示內聚力低、耦合度高,程式不易修改跟擴充。

Delegate 中文叫「委派」。

將某些操作(例如是否相等)的細節交給 object 自己處理。

用一點簡單的 code 來說明。首先是沒有 delegate 的寫法(這裡先不管 member 放在 public 不是好習慣的問題):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Foo
{
public:
Foo(int x, int y) : m_x(x), m_y(y) { };

int m_x;
int m_y;
};

int main()
{
Foo foo1(10, 20), foo2(200, 300);

if (foo1.m_x == foo2.m_x && foo1.m_y == foo2.m_y)
{
// do sth.
}

return 0;
}

這種方式,當在任何地方需要比較兩個 Foo object 時,都要像上面第 14 行這樣寫。不覺得這樣都把 Foo 肚子裡的東西挖出來到處亂放嗎?如果之後 Foo 要多加一個 member,所有比較 Foo object 的地方都要改,光是有 20 個地方要改就有得受了,更何況還可能漏掉勒。所以啦,delegate 的觀念就可以在這裡拯救可憐的工程師,以下是有 delegate 觀念的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Foo
{
public:
Foo(int x, int y) : m_x(x), m_y(y) { };

bool IsEqual(const Foo& rFoo) const
{
return (m_x == rFoo.m_x && m_y == rFoo.m_y);
};

int m_x;
int m_y;
};

int main()
{
Foo foo1(10, 20), foo2(200, 300);

if (foo1.IsEqual(foo2))
{
// do sth.
}

return 0;
}

增加 IsEqual() 後,我們就可以把判斷放進 class Foo,由 Foo 自己處理如何判斷相等的這件事。外面只需要 call IsEqual() 就可以比較兩個 Foo object,也就是外界將判斷相等的事情「委派」給 Foo。這時候如果多加一個 member m_z,只需要改 IsEqual() 的 implement 就可以了,外面 20 個比較的地方都不用改!世界變得一片美好。

文言一點來說,delegate 有助於保持 loosely coupled。loosely coupled 表示 object 彼此獨立(也可以看成 object 跟其他 code),對一 object 的修改不會引起一連串其他 object 或 code 的修改。

問題時間

  • 為什麼不用 C++ 的 operator==

    operator== 也可以。這裡想表達的是 delegate 的觀念,如何實作不是重點。反過來說,不同語言也有不同的特性跟用法,而一個觀念可以用很多種方式實作。

  • 為什麼感覺有封裝的味道?

    我也這麼覺得。一些 OO 原則跟觀念彼此根本一家親,運用 A 的同時也運用了 B。我暫時想不出更好的範例了,就先這樣吧!:P

設計軟體事先處理整體系統輪廓,將大系統切成多個較小的問題,再逐一反覆處理每個小問題直到完成整個系統。

Feature Driven Development 及 Use Case Driven Development 是「將系統切成小問題再逐一反覆處理」的方式。

Feature Driven Development

以 feature 為主軸,以功能為切入角度。

挑出特定功能,規劃、分析及開發該功能直到完成。

看系統的角度比較 granular。

適合功能較個別獨立、未密切相連的系統。

Use Case Driven Development

以 use case diagram 為主軸,以 scenario(使用情境)及流程為切入角度。從 use diagram 拿出一個個 use case 做,挑出 use case 的 scenraio,寫 code 支援該 scenario,直到完成 use case 的所有 scenario 以及完成所有 use case。

看系統的角度比較整體。

適合由一堆流程構成的系統,例如差勤請假系統。

碎念時間

實際上開發軟體會混合多種方式,例如從 use case driven 開始,接著在 use case 挑出小功能進行設計(這是 feature driven),最後在實作階段用 test driven 來思考如何 implement。

開發軟體的方式很多,各有其適用之所在,重點不在於哪個方法最好,而是哪些方法的搭配使用能較好的解決所面對的問題。

很久很久以前,在那個網路只有撥接還常常撥不上去、撥上去也不能用太久的年代,我開始學寫網頁。那時候網路上的資源沒有現在豐富,搜尋引擎還在用蕃薯藤跟奇摩。當時覺得對初心者的教學好重要,所以一直想著如果之後能夠寫些跟技術有關的東西,要寫寫教學文章。後來高中在社團當教學,寫過社課的教學講義,不過沒放上網路,至於主題是什麼就別問了。

就這樣,我對寫技術相關的文章或 blog,一直想以寫教學的角度撰寫。但是隨著年歲增長,學的東西越多,越發覺自己會的很少,要寫出好教學沒那麼簡單,小時候的豪氣干雲(?)也漸漸弱了下來。唉,還是小時候比較天不怕地不怕。

開這個 blog 以前,我早就有個放只有自己看得懂的筆記的 wiki,但總覺得只有自己看得懂的東西 po 出來像在丟人現眼,也就一直放著自己看。後來慢慢弄了這個 blog,還是很想寫教學(這到底是什麼制約…),但是越這樣想我寫出來的語法就越奇怪,跟寫論文的狀況有點像(咦?)。就像把書上看不懂在寫什麼鬼的文字,再轉成另一種我自己也還是看不懂的句子,這樣寫筆記的意義到底是……?Orz

到這幾天覺得其實沒必要那麼嚴肅,反正就是把從各種地方學來的知識技術,消化吸收後再用自己的話寫出來就好了。

總之就是一個將知識與技術咀嚼消化後再吐出來(?)的過程。

Architecture is your design structure, and highlights the most important parts of your application, and the relationships between those parts.

架構是系統的組織結構,包含分解開的各個元件、元件間的關係、互動機制,以及系統設計中使用的原則及決策。

這次英文比中文好懂一點。

就我的理解,architecture 是整個軟體的結構,有點像骨架。其他那些 requirement、class diagram、design pattern、use case 等等東西就像軟體的,呃,肉(?),有骨架後就能把這些內容填進去。(寫到這裡開始懷疑我到底是怎麼理解這些概念的…)

設計 architecture 的階段會整理出那些 component 是重要的、建立處理順序,以及減少風險。

如何找出重要的事情?

可以問幾個問題:

  1. 它是系統本質的一部份嗎?
    可以用「如果沒有這個功能,這系統還會是它應該是的東西嗎?」來思考。
  2. 某個功能究竟是什麼意思?
    理解功能真正的意思,不懂就問客戶,或者任何要你做這玩意的人。盡量避免你以為你知道那是什麼但其實你不知道的狀況(繞口令時間)。
  3. 如何實作某個功能?
    挑出最困難的部分。不過一項實作困難程度會因人而異。

從誰開始?

找到最重要的幾件事情後,要決定從哪個部分開始。

這裡的重點是「減少風險」。只要能減少風險,從誰開始都可以。所謂風險,諸如 schedule 來不及、做出不是客戶要的東西等等。

在設計架構的階段要做可以減少風險的事,延後不能減少風險的事情。

如何減少風險?

  • 用 scenario 找出大部分的重要 requirement
    scenario 是穿過 use case 裡的一個路徑,也就是一段系統如何被使用的敘述。
  • 延後進入細節的時間,因為在架構階段進入細節無法減少風險。
    • 不要在架構階段寫詳細的 use case。
    • 延後細節的 coding
      可以先有個框,但不要開始寫邏輯細節。
  • 有好的 design
    • 一開始就把事情做對
    • 利用 commonality analysis(共通性分析)設計具有彈性的軟體
  • 搞不清楚意思或軟體要幹嘛時去問客戶,以降低做出不是客戶要的東西的風險。

寫好軟體的方式是先做好了解需求、規劃、組織、架構以及減少風險,盡可能延後 coding、避免在前期一頭栽進細節。

首先,來點文言文…

A use case describes what your system does to accomplish a particular customer goal.

use case 是捕捉新系統或軟體變更的潛在需求之技術。每個 use case 提供一或多個 scenario,傳達系統如何與 end user 或其他系統互動,完成特定目標。

以上文言文看完我也不知道自己是不是知道它在寫什麼。(喂)

白話文來說,use case 會寫出一堆使用這軟體的情境跟過程,藉由這些情境跟流程來描述軟體要做些什麼好達到客戶的目標。(這樣有白話文一點嗎?)

use case 描述軟體要「做什麼」(what),而非描述「如何做」(how)。

組成 Use case 的三部分

  1. clear value
    軟體要幫客戶做的事,也是客戶的目標。
  2. starting and stoping point
    use case 的開始及結束點,總不會一直繞圈圈沒完沒了吧
  3. external initialtor
    既然是軟體的使用流程,總有個開始「使用軟體」的人或其他系統。

Main & Alternative path

main path 是當世界一片美好、沒有任何事情出錯時,使用者會遵循的使用流程。但通常世界不是那麼美好的,工程師的工作之一就是要找出這些不美好(?),讓軟體也能妥妥善善的處理它。alternative path 即是在 use case 中負責描述及處理這些「出錯狀況」的使用流程。

alternative path 可以是…

  1. 完全替代原本的部分 path。遇到某個選擇時可以選 main path 繼續下去,也可以選 alternative path 做。
    例如可以選吃飯或吃麵完成吃晚餐這件事。
  2. optional 的,用來處理額外、例外以及出錯的狀況。
    例如想吃牛肉麵但沒開的時候該怎麼辦。

關於 Use case

形式上沒有固定的限制,我通常會寫成流程步驟,不管形式如何,重點只有一個──看得懂、能正確表達意思。

寫 use case 的時候會進到幾乎可以將 use case 裡的 logic 變成 code 的細節部分。

use case 也要包含檢驗步驟,例如檢驗輸入是否合法、某個物件是否存在等等。

Textual Analysis

寫好 use case,然後咧?跟程式有什麼關係?這時候就是 textual analysis 上場的時候啦!

分析 use case 裡的名詞及動詞,整理出 class 及 method 即為 texttual analysis。

use case 中的名詞有可能是系統中的 class,動詞通常是 class 的 method。當然不是 use case 裡所有的名詞跟動詞都是 class 跟 method,所以需要經過分析,好決定要為那些名詞及動詞建立 class 跟 method。

做完 textual analysis 決定要有哪些 class 跟 method 後,就可以進入設計物件跟物件間關係的階段了。

做玩具的碎念心得。

用 Google Book API 找書的 Ruby 小程式。

一開始因為 anobii 改版後變得超級難用,一氣之下不用了,又找不到好用的,就想自己寫一個網路書櫃。但是網路書櫃有點大,而且暫時不想架需要維護的平台,索性簡化,結果簡化成這個小玩具(也簡化太多)

目標是學寫 Ruby 程式、包 gem 跟丟上 RubyGems。

程式邏輯本身很簡單,而且 Ruby 有很多現成的套件可以用,節省不少時間。不過沒用過這套 test framework,bundler 跟 gem 的熟悉度只有 bundle install,所以花比較多時間在 test 跟包 gem。

關於功能

原本功能縮小到只想寫 book wrapper,把書籍資料包成 class,寫完覺得實在太陽春才變成找書。擴大功能後卻開始不只想用 Google Book API,還想加上 search Amazon 跟 ISBNDB。但以原本作為練習的目的來說,這些功能有點多餘。

我有時候會因為某個功能看起來好酷就想加上去,有時候是覺得東西很陽春、太簡單,想加更複雜的能力進去。如果這些事情不斷發生,程式會越長越大、越長越大,卻永遠沒有完成的一天。這裡說的完成是指階段性完成──什麼時候要喊「夠了,可以了」然後把東西丟出去。

這些讓我在中途思考要以「看起來很厲害」為優先,還是以「完成核心功能及達到最初目的」為優先。最後決定縮小範圍,以後者為優先。

雖然弄完覺得像寫了個垃圾就是了…

關於搜尋結果

有想過搜尋結果的筆數會不會太多?考慮過用其他方式當 output,例如一開始可以設最多只找幾筆之類的。但是試了一下,書籍的搜尋結果似乎不會多到太誇張,又懶得把介面搞得太複雜,就變成很簡單的全部 search 完一次傳回結果。

關於測試

我糾結了一陣子到底要怎麼測,中間 test case 一直換一直換。測試有用 Google API 抓資料,不能太多,太多會被 403 擋掉…XD 搜尋結果不是固定的,做太詳細的資料檢驗沒什麼意義,所以最後只有兩種 test case:

  1. 用找 isbn 測 class Book 是好的
  2. 用找 title 測當搜尋結果超過 40 筆時回傳的結果筆數大於 40。
    這是因為 API 限制一次的 search 結果最多只能有 40 筆,可以用起始 index 決定要顯示哪些 search 結果。

關於 Ruby

為什麼選 Ruby?

沒為什麼……只是它有點名氣,然後我想寫點高階的東西,不然一天到晚在用 C++ 做輪子有點煩……

越方便的技術代表它抽象化的程度越高,寫起來通常比較快,像在把現成的積木拼起來。但也代表它隱藏更多細節,我有時候會覺得不知道這些細節很沒有安全感(?)。到目前為止,Ruby 寫起來有很多方便的地方,可是我隱約覺得真要用得好還是要了解背後的原理,但那些原理可就不像一開始學的這種方便性那麼容易了。

這篇記錄寫完 Ruby 程式後如何包成 gem 並丟到 RubyGems 上分享。

基本流程:

  1. source code 放到 lib/
    lib/ 底下通常有 <gem_name>.rb<gem_name>/
    <gem_name>.rb 放此 gem 最主要的 class 及 require,其餘 source code 放在 <gem_name>/
  2. test 放到 test/
  3. 在根目錄新增 <gem_name>.gemspec
  4. gem build xxx.gemspec 生出 xxx-x.x.x.gem
  5. gem push xxx-x.x.x.gem publish 到 RubyGems

以下以玩具 booksr 為例。

檔案結構:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
│  booksr.gemspec
│ Gemfile

├─lib/
│ │ booksr.rb
│ │
│ └─booksr/
│ api_handler.rb
│ book.rb
│ parser.rb

└─test/
tc_search_isbn.rb
tc_search_title.rb
ts_google_api.rb

booksr.rb 的內容:

1
2
3
4
5
6
7
8
9
10
require 'json'
require 'rest-client'

require 'booksr/api_handler'
require 'booksr/parser'
require 'booksr/book'

class Booksr
...
end

booksr.gemspec 內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Gem::Specification.new do |s|
s.name = 'booksr'
s.version = '0.1.0'
s.date = '2015-01-21'
s.summary = 'A simple book searcher'
s.description = 'Search book with title, author, isbn or keyword by Google Book API.'
s.authors = ['cjwind']
s.email = 'cwentsai@gmail.com'
s.files = Dir['lib/*.rb', 'lib/booksr/*.rb', 'Gemfile', 'README.md', 'Rakefile', '*.gemspec', 'test/*.rb'] # 此 gem 包含的 source file
s.homepage = 'https://github.com/cjwind/booksr'
s.license = 'MIT'

# depend 的 gem
s.add_runtime_dependency 'bundler'
s.add_runtime_dependency 'rest-client'

s.add_development_dependency 'rake'
s.add_development_dependency 'test-unit'
end

runtime dependency 跟 development dependency 的差別是預設上不會安裝 development dependency 的 gem。

如果有 Gemfile,要改寫成吃 .gemspec

1
2
source 'https://rubygems.org'
gemspec

它會將 runtime dependency 當作基本的 dependency,development dependency 則會開個 development group。

生 gem:

1
2
3
4
5
$ gem build booksr.gemspec`
Successfully built RubyGem
Name: booksr
Version: 0.1.0
File: booksr-0.1.0.gem

要 publish 到 RubyGems.org 前要先註冊帳號,publish 時需要輸入 Email 跟密碼驗證。

1
2
3
4
$ gem push booksr-0.1.0.gem
Enter your RubyGems.org credentials.
Don't have an account yet? Create one at https://rubygems.org/sign_up
Email:

Troubleshooting

在 win7 底下 publish 遇到以下錯誤:

1
2
3
ERROR:  While executing gem ... (Gem::RemoteFetcher::FetchError)
SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certif
icate verify failed (https://rubygems.org/api/v1/api_key)

Solution:抓 cacert.pem 放到 C:\Users\<username> 底下,並且增加檔案 .gemrc,內容為:

1
:ssl_ca_cert: c:\users\xxx\cacert.pem

碎念時間

我是先寫好 code 才開始調這些檔案結構(真相是寫 code 的時候根本不知道這些結構),跟 require 啊、bundler 啊、gem 啊不熟到爆炸,中間 unit test 還亂入,混亂了好一陣子。弄完稍微多懂一點,希望這篇沒有漏掉什麼,漏掉就再補囉!

Ref

很久很久以前(講得好像很老一樣),剛學寫程式的時候,我有個習慣是寫一點點就會執行起來看對不對。在那個連用迴圈做輸入都不太熟的超新手時期,連輸入都會做這種「測試」。這習慣延續了很久,直到現在,在剛開始學一個新語言或者環境許可的狀況下(編得過、可以跑、測起來不會太複雜),還是會這樣──寫一點點就跑起來看對不對。

今天晚上寫小玩具的時候,新語言嘛,不熟,就又出現這種習慣。就在我還沒把程式 run 下去、正覺得這種驗證很囉唆的電光石火(?)之間,我突然想到──幹嘛不把這種驗證寫成 unit test 就好?這樣就不用跑很多次、人眼校對很多次了啊!

於是乎,這次改變方式,先把要寫的東西全部寫完,再寫 unit test,直接 run unit test 看 code 有沒有寫對。在這種很瑣碎但又得確認是對的事情上,用 unit test 真是神清氣爽啊!雖然不知道之後重複利用的機會多大,但是不用在那裡對到快脫窗真好。也還真給我抓到一個漏掉 assign 的 member…

看來我的寫 code 方式終於跟 test 連上了一點,不然每次想要硬套 TDD 之類的時候,就像學了個新方法,可是並沒有改變舊習慣,兩邊有點接不起來。這次是注意到舊習慣令人厭煩可以改善的地方,再套一點新東西進去變成好一點的方式。

最近在想,獨立開發的時候,更該利用自動化測試減少花在重複性工作上的時間。我想,運用科技讓人能更妥善運用時間,才是科技的本意吧。

QMap 內的資料會以 key 的值 sort 好。以自訂 class 或 struct 作為 key 需要提供 operator<。所以將資料 insert 進 QMap,再用 iterator 取出就可以做到 sorting。

1
2
3
4
5
6
7
8
9
10
11
12
QMap<int, QString> sortMap;

sortMap.insert(31, "31");
sortMap.insert(3, "3");
sortMap.insert(2, "2");
sortMap.insert(5, "5");
sortMap.insert(7, "7");

for (QMap<int, QString>::iterator iter = sortMap.begin(); iter != sortMap.end(); ++iter)
{
// sorted
}

key 有多個欄位時可以做到不同欄位有不同 priority 的能力,如:

1
2
3
4
5
6
7
8
9
bool operator<(const XXX& rhs) const
{
if (fieldA != rhs.fieldA)
return fieldA < rhs.fieldA;
if (fieldB != rhs.fieldB)
return fieldB < rhs.fieldB;
...
return false;
}

這表示在比較上,fieldA 的 priority 比 fieldB 高,也就是先比 fieldA 再比 fieldB。

x < yy < x 都為 false 時表示 x == y