首先,來點文言文…

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

今年,專業上:

  • 開了這個 blog
  • 看完《Effective C++》(跳過 template 就是了,那塊太不熟根本看不懂)
  • 《深入淺出物件導向分析與設計》一到九章(剩一章,耶!)
  • 正在看《程式設計師的自我修養──連結、載入、程式庫》
  • 邊做邊學 Ruby on Rails
  • 對 C++ 有再熟一點,吧?
  • 設計架構崩崩中
  • 有比較會切細工作,吧?
  • 多會一點 Git

2015 年目標:

  • 吃掉《深入淺出物件導向分析與設計》
  • 啃掉《程式設計師的自我修養──連結、載入、程式庫》
  • 看《深入淺出設計模式》
  • 看《Clean Code》(寫到這裡我在想是不是在找死?)
  • 寫個 Ruby Gem
  • 練習 OOAD
  • 練習「想清楚再做」跟「避免分析癱瘓」(有時候會一直在那裡想然後做不了決定 X_X)
  • 找些 open source project 來 follow(好像有點抽象)
  • 填補理論跟實務之間的空虛地帶(具體上還沒決定,先列些主題)
    • OS
    • Compiler
    • Multi-Thread Programming

我也不知道寫這些目標會不會照著做,很具體的應該會,寫得越抽象的越容易歪掉就是了。明年底再來看哪些有做到、哪些歪掉、又有什麼是吃飽太閒在亂玩。:P

一個檔案 include 另一個檔案時,這兩個檔案便形成 compilation dependency。一個被 include 的檔案一旦修改,跟它有 compilation dependency 的檔案(無論直接或間接 include)都需要重新 compile。假設 A.h include B.h,當 B.h 改變或 B.h include 的檔案改變時,所有 include A.h 的檔案都需要重新 compile。

應該盡量降低 compile dependency,以減少修改一個 header 而需要重新 compile 的檔案數量,免得改個檔案全世界都要重新 compile。(工程師無數青春在哭泣)

原則上盡量以 declaration 的 dependency 取代 definition 的 dependency

  • 可以用 object pointer 或 object reference,就不要用 object。

    可以在 class 裡宣告 pointer 或 reference 作為 member,就不要定義 object 當 member,因為定義 object 就需要該 class 的 definition。

  • 分開 class 的宣告與實作

    讓 include(提供 definition)可以從 header file 移到真正使用這些 class、function 的 cpp file。

    • Handle class

      使用兩個 class,一個代表宣告,一個是真正的實作。前者通常稱為 Handle class,只宣告 function 並有個 pointer 指向後者。外界使用 Handle class,而 Handle class 會轉 call 真正 implement 的 class 達到功能。這個方式用到第一點的概念──避免使用 object,改用 pointer。

      因為兩個 class 會有相同的 function,會造成同時 maintain 兩份類似 class 的問題。

    • Interface class(abstract base class)

      interface class 沒有 member data、沒有 constructor,只有一 virtual destructor 跟一堆 pure virtual function。實作的 class 繼承 interface class 進行功能實作,以 virtual function 的機制讓外界用 interface class 的 pointer 使用。

    這兩種方法的 trade off 是速度會慢一點、多用點 memory,不過普遍上來說好處比 trade off 重要。

Ref

  • 《Effective C++》item 31

program by constract(按契約程式設計)就是寫 code 跟用那份 code 的人說好,這些 function 要做什麼、負責什麼、使用的人要怎麼用,大家照說好的去做。

defensive programming(防禦性程式設計)則是大家互相不信任,會做許多檢查以確保自己不會當掉、使用的人用錯也不會當。defensive programming 會產生許多用於檢查的 code,讓 code 變得冗長、效率也可能較差,所以一般建議在 input 要進入系統時做檢查,系統內部互相 call 時就不再做檢查。當檢查發現問題、出現不該出現的錯誤時,較建議盡快讓程式 fail 好找出錯誤,而非忽略錯誤、繼續讓系統執行下去。

Ref

Linux 本身沒有 thread 的概念,它把 process 跟 thread 都視為 Task

fork() 用來生 child process,生出來的是概念上的 process。

clone() 可以生出跟 parent process 共用 memory space、file descriptor 的 table、signal handler 的 table 等東西的 child process,也就是概念上的 thread。Linux 用 clone() 實作 thread 概念。

clone() 有 glibc 包過的:

1
2
3
4
5
#include <sched.h>

int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

也有純 system call:

1
2
3
long clone(unsigned long flags, void *child_stack,
void *ptid, void *ctid,
struct pt_regs *regs);

UML class diagram 只是個概觀,不會說明所有細節(例如如何實作),可以為需要建立的 class 進行 modeling。

用一張圖解釋:

繼承

1
2
3
4
class Parent
class Child

Child -up-|> Parent

實作(implement)

1
2
3
4
interface Interface
class Concrete

Concrete .up.|> Interface

class 間的關係

抽象上來看 class 之間的關係,程式語言裡不見得有對應的語法,但是釐清抽象上的關係有助寫 code。只要對系統來說是合理的,class 間的關係可能因為系統的演進而改變。

關聯強度:composition > aggregation > association

Association

class 間有關係但關聯性較弱。

1
2
3
class Foo
class Bar
Foo -right-> Bar

class Foo 知道 class Bar

Aggregation

class 間是 has a 的關係。class A 包含 class B,但 B 不是 A 的一部分。

main object 不負責 composed object 的 create 及 destroy,composed object 可以獨立於 main object 之外。例如車子跟輪胎間的關係(Car has a Tire),輪胎可以獨立於車子之外(可以從車子上拆下來)。

UML 以空心菱形表示:

1
Car o-right- Tire

Composition

class 間是 own 的關係。(碎念:own 跟 has 根本沒辦法用中文區分…)

main object own composed object.

composed object 是 main object 的一部分(a part of),composed object 不會獨立存在於 main object 之外。main object 負責 composed object 的 create 及 destroy,main object destroy 時 composed object 也會 destroy。例如生物跟細胞間的關係(Animal owns Cell)。

UML 以實心菱形表示:

1
2
3
4
class Animal
class Cell

Animal *-right- Cell

composition 跟 aggregation 最大的不同在於 composed object 的生命週期是否與 main object 有關。

Dependency

class 間是依賴關係時,UML 以虛線箭頭表示:

1
2
3
4
5
6
class Animal
class Water
class Air

Animal .right.> Water
Animal .left.> Air