最近的感想。

念書的時候常覺得書上的理論跟實際上的使用或實作有一段距離,我說不上來那是落差還是什麼,但就是有點搭得起來又哪裡怪怪的。後來工作後我有點疑問──在學校念的那些理論,真的有用嗎?如果業界是以實務為主,為什麼我們需要學那些理論?從這個問題就看得出來我當學生的時候很少好好理解過為啥自己要念那些書…

我最近得到的答案是──理論會提供我們背景知識與架構,可以讓我們能較快速的理解系統跟軟體的運作方式,而不至於迷失在茫茫程式碼海中,要從許多的片段中慢慢拼湊才能知道整個系統的樣貌。當然,理論提供的是偏向概念上的了解,它不會有太細部的東西,實務上的細節是會隨著情境不同而調整的。

軟體的結構也一樣。最近工作上,我發現因為對整個軟體的架構比較熟悉了,所以能比較快猜測、反應出可能是哪邊有問題,不像之前一遇到問題就得重新 trace,也似乎比較看得出來改動哪個部分會影響到那些東西。開始稍微能體會軟體有其架構的重要性,當它有個結構、有些規則可循,programmer 比較能知道改動的影響範圍,出問題時也能比較快找到。我們可以不用去記憶一些特例跟「這邊改了那邊還有那那邊也需要改」的事情,這也會減少出錯的可能。對我這種不喜歡記那麼多細節還會搞混的人來說這真是太棒了!

最近在看《深入淺出設計模式》,我想 design pattern 也是一種軟體上的結構,如果知道某個部分是使用某個 pattern,就能馬上了解其大致的運作。不過我不了解 pattern 的時候,看那些 code 只有一個感想──幹嘛把事情搞得那麼複雜?

可惜的是,我現在仍然不覺得「軟體有其架構帶來的效益」這件事能有精確量化的指標。因為沒有對照組,沒有一套沒有架構的同樣軟體去比較有架構與沒有架構的差別,可能只能用間接的方式去量度關於軟體有架構的益處。

封裝會改變的東西,減少改動一個部分會影響到其他部分的狀況。

將 class 中容易改變的部分封裝到另一個 class 有助於保護原本 class 有不必要的改變。

程式改變可能改動到相關功能,如果一個 class 中常修改的地方並非該 class 負責的事情,修改它是不必要的,而且可能引入其他功能受到影響的風險。如果能將常修改的地方封裝,就可以保護原本 class 的其他功能不受影響。

「封裝」不單指將東西包成 class,把一堆 property 放到 map 之類的 container 也是封裝。

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