<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>同人的生活派對 &#187; 編程技巧</title>
	<atom:link href="http://www.lifeparty.idv.tw/blog/archives/category/%e8%bb%9f%e9%ab%94%e9%96%8b%e7%99%bc/%e7%b7%a8%e7%a8%8b%e6%8a%80%e5%b7%a7/feed" rel="self" type="application/rss+xml" />
	<link>http://www.lifeparty.idv.tw/blog</link>
	<description>君子學以聚之,問以辨之,寬以居之,仁以行之</description>
	<lastBuildDate>Wed, 01 Sep 2010 05:44:17 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0</generator>
		<item>
		<title>測試驅動開發的步驟</title>
		<link>http://www.lifeparty.idv.tw/blog/archives/2737</link>
		<comments>http://www.lifeparty.idv.tw/blog/archives/2737#comments</comments>
		<pubDate>Thu, 07 Jan 2010 06:46:19 +0000</pubDate>
		<dc:creator>jim yeh</dc:creator>
				<category><![CDATA[問題解決]]></category>
		<category><![CDATA[專案管理]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[溝通]]></category>
		<category><![CDATA[生活感觸]]></category>
		<category><![CDATA[編程技巧]]></category>
		<category><![CDATA[職場]]></category>
		<category><![CDATA[衝突]]></category>
		<category><![CDATA[設計原則]]></category>
		<category><![CDATA[開發流程]]></category>
		<category><![CDATA[閱讀]]></category>

		<guid isPermaLink="false">http://www.lifeparty.idv.tw/blog/?p=2737</guid>
		<description><![CDATA[敏捷開發並不是教條式的照本宣科，開發者要懂得變通最重要的是用心思考，而非把必要的思考都看成精神層面的問題，這並非適用於敏捷開發的心智模式。以下是同人在 Facebook 的 Scrum community in Taiwan 的回應，但文辭有略為做過一番修飾，可以用來澄清我對測試驅動開發步驟的看法。]]></description>
			<content:encoded><![CDATA[<p>昨天寫的〈<a href="http://www.lifeparty.idv.tw/blog/archives/2716">測試驅動開發要徹底重構</a>〉，曾經提到 Steven 說到「如果像閣下文中說：『系統不斷演進及需求不斷改變之下，也可能會使架構或設計愈來愈複雜而變得難以維護』，我則認為是在進行 TDD 時候沒有徹底去重構系統。」，同人認為這段話有根本上的邏輯的繆誤，於是回應：</p>
<blockquote><p>假如上面的話是成立的，那麼代表只要徹底重構系統就不會造成「系統不斷演進及需求不斷改變之下，也可能會使架構或設計愈來愈複雜而變得難以維護」嗎？如果是這樣，那 XP 根本就不需要 Refactoring 這個實務來改善程式的結構，因為你都徹底重構程式，程式結構變差的情況是根本不可能發生。</p>
<p>而且依照本人 20 年來軟體開發的經驗，我還不沒有看到有程式一開始就寫得很好，到後來可以不用改變架構而符合新需求的。倒是隨著對問題的更清楚，或是程式需求的變化，讓程式必須重構的現象時常履見不鮮！</p>
<p>所以如果自以為自己博覽群書，很懂得TDD的實務。也不要忘了用心思考，以免自己對TDD的最佳實務的認知，不小心犯了根本上的邏輯繆誤？</p></blockquote>
<p>結果，後來同人在 <a href="http://www.facebook.com/group.php?gid=179345672472">Facebook 的 Scrum community in Taiwan</a> 看到 Steven 做了以下的回應：</p>
<blockquote><p>先說件簡單易明的事情，其實我很不喜歡閣下把 TDD 放到 "精神" 層次，卻忘記了基本步驟。</p>
<p>之前的討論根本連事實層次都被忽略，根本談不上是什麼多少年的經驗或者如何用心思考，連 TDD 的最基本步驟也忘記，TDD 的基本步，不是閣下做多少年工作就可以把人家的定義去改變的，我也不明白為何有 20 年工作經驗就可以把 "Refactoring" 說成 "並不必然是 TDD 的必要的步驟"，這不是邏輯問題，更不是有過什麼開發經驗然後用心思考就可以改變的事情，這是就算對軟體開發的認知不夠閣下那麼 "全面" 的都能看出的謬誤，如果閣下這樣就認為是因為說不過閣下就建議多看書本，本人深表遺憾。</p>
<p>我是來討論問題的，我沒有興趣去傷害閣下感情，好好閱讀書本，只是反映 TDD 三個步驟是什麼根本不存在爭議，更沒有 "好好閱讀什麼書或文章才能跟我討論" 的意思，我還未自大得要別人看過多少書才可以討論問題，亦正如討論問題我也不用跟別人說我有多少年工作經驗一樣，而且本人是衷心認為多讀書是有益的（不管是閣下還是什麼人），多讀書亦不是為上來辯論的，不過閣下如果感到有所不悅的，我就先行道歉，還是希望冷靜一點討論問題。</p>
<p>而簡單的例子也是思辦的過程的一部份，一方面是簡單易明地討論問題，另一方面是如果連簡單的例子也說不通，又怎麼能去談更複雜的問題呢？</p>
<p>上面提到："那XP根本就不需要 refactoring 這個實務來改善程式的結構，因為你都徹底重構程式，程式結構變差的情況是根本不可能發生。"</p>
<p>不如冷靜一點再讀讀這句子，"XP 不需要 Refactoring 是因為徹式地進行 Refactoring"，我就看不明白這是什麼邏輯，一邊說不需要，另一邊說徹底地進行。</p>
<p>這裡的問題是軟件是會改變的，可能是新增功能，也可能發現有其他問題，每次帶來的變更其實都需要進行重構的。所以說 XP 不需要 Refactoring 也不正確。</p>
<p>世上的確沒有一寫就好的代碼，而且世界是會變的，Refactoring 就是避免以後的更改越來越困難。把 "徹底地進行重構" 理解成 "XP 不需要重構這實踐" 完全沒有邏輯可言。</p>
<p>我也沒有反對不用改變程式架構就能滿足新需求，亦沒有否定 Refactoring 的重要性，只不過我還是建議新的功能以 TDD 方式進行開發，有測試、有代碼、然後進行重構的。</p>
<p>在足夠測試覆蓋下進行重構是可使系統在不斷演進及需求不斷改變之下，使架構或設計仍然處於可以維護的狀態，相反我指出的是，如果程式架構和設計越來越難維護，是重構的力度不足夠。</p>
<p>前面還提到："但重構的目的為何？就重構的定義在不改變功能的情況下改善程式結構，以增加程式碼的彈性以利未來增加或改變功能。因此如同那第三步所言，為了去掉重覆性而重構。"</p>
<p>如果重構只是為了去掉重覆性，那 TDD 的第三步不如叫 "Remove Duplication" 好了，無可否認代碼重覆是很常見的問題，但把這裡的重構限制成消除代碼其實會局限系統的將來發展，而且到了 TDD 的第三步，系統是應該有足夠的測試去覆蓋系統，重構的力度沒理由只局限於新增功能和現有程式的重覆。</p>
<p>我就相信閣下是 Refactoring 的專家，也應該會知道一些 Refactoring 的模式是完全相反的，例如 "Pull Up Method" 和 "Push Down Method"，更是需要觀察當時的情況來作決定，而沒有一面倒那個才是好的模式，我實在不明白為何要把 TDD 的 Refactoring 局限到只做 "去掉重覆性"。</p>
<p>這是實務上會發生的事情，消除代碼重覆以外的重構還是會發生的，如果只是單單只是 "消除重覆"，這會是另一個我認為進行重構不夠徹底的事情。</p>
<p>上面已經不單單是用心思考，而是由理論到實踐都可以看得到的事情。</p>
<p>跟討論 Refactoring 和 TDD 觀點以外的聲音，就引用 Chet Henderickson 的說法，全部都是我錯好了，現在可以解決問題嗎？</p></blockquote>
<p>看了 Steven 的回應，同人當下的反應是不想浪費時間與心力與他周旋下去，但後來想到或許是因為 <a href="http://cb.esast.com/cb/wiki/9584">1/9 敏捷開發分享會</a>我要分享實施 XP 的經驗與心得，也許這是一個巧妙的<a href="http://en.wikipedia.org/wiki/Synchronicity">同步事件</a>。我可以趁這個機會，導正對 XP 或是 Agile 的一些錯誤觀念。</p>
<p>例如敏捷開發並不是教條式的照本宣科，開發者要懂得變通最重要的是用心思考，而非把必要的思考都看成精神層面的問題，這並非適用於敏捷開發的心智模式。以下是同人在 Facebook 的 Scrum community in Taiwan 的回應，但一些詞句有略為做過一番修飾，以清楚表達我對測試驅動開發步驟的看法。</p>
<p>呵呵，Steven Mak 的回應很有趣，把別人說成錯的不代表自己就是對的。這跟他先前面對觀點的差異就要人好好的看書的行為是如出一轍，但問題是要別人好好看書也不代表說這句話的人書看得比別人廣泛或是深入。更何況，看很多書是一回事，有沒有讀懂書中作者所傳達的意思又是另外一回事。從 Steven 喜歡用斷章取義的方式解讀我的觀點，以抽取他想要的意義來看，恐怕他看書的目的只是揀選他要的部分，沒有弄懂作者想要傳達的意思的可能性可能居多吧！</p>
<p>對了，我忘了 Steven 的中文可能不夠好，沒辦法弄懂我所表達的意思。如果是這樣，他其實大可以告訴我，我不會叫他去好好地學中文，或是嘲笑他那麼簡單的句子都看不懂，而是想辦法要怎麼表達才會讓他了解我的想法。省去他猜錯我的意思，而顯露出他並沒用心體會或是根本不想思考別人的觀點的窘態。</p>
<p>當然，以上的假設可能是不成立的，他的中文其實很好。但如果是這樣，他的邏輯思維能力真的要加強，因為思考為什麼是最基本的能力，不是什麼經神層次。</p>
<p>記得去年同人應邀到中山大學演講，有機會與鄭炳強教授在餐敍之中交流軟體開發的觀念。他特別強調軟體工程最重要的不是 know how，而是 know why。因為遇到不同需要而要採用適當的方法，唯有具備 know why 的能力才能做到。以 Steven 那麼重視看書來增加知識的態度來看，他應該也很重視軟體工程的學界權威的看法吧？可是如果鄭教授看到 Steven 把 know why 的思維定位成精神層次，我想也很可能會令他很搖頭吧！</p>
<p>回歸正題，Robert C. Martin 在《<a href="http://www.books.com.tw/exep/prod/booksfile.php?item=0010293039">敏捷軟體開發</a>》這本書（林昆穎，吳京子，2005）中提到測試驅動開發法，他說：</p>
<blockquote><p>所有的產出的程式碼都是為了讓失敗的單元測試通過而編寫的。首先，我們先寫一個單元測試案例，由於它所要測試的功能並不存在，所以它不會通過，然後我們再編寫程式碼，使得測試案例通過。</p>
<p>編寫測試案例與程式碼之間的反覆來回非常快速。只花費一會兒的功夫。測試案例與程式碼一起演進，而測試案例會比程式碼更早一點。</p>
<p>結果，一組非常完整的測試案例隨著程式碼一起增長，這些測試可讓程式員檢驗程式是否正常運作。如果有一組小搭檔做了些小修改，他們可執行這些測試案例，以確保修改並未對程式碼造成任何破壞。這會極有利於進行重整。</p></blockquote>
<p>咦，堂堂一位敏捷開發大師級人物，怎麼他也沒提到 TDD 要重構，只說可以有助於未來的重構，難道他也擅改了 TDD 的步驟了嗎？讓我們看另外一本書。點空間的朱子傑和陳盈學翻譯的《<a href="http://www.books.com.tw/exep/prod/booksfile.php?item=0010326978&amp;">The object primer 3/e 中文版－靈活模型驅動開發與UML2</a>》中有更具體說明TDD的步驟，而且還附了流程圖（有興趣自己買書來看）。</p>
<blockquote>
<ol>
<li>快速地加入一段測試，基本上只要能夠測試一段程式碼即可，此時你的測試結果會是失敗的。</li>
<li>執行你的測試，一般是全部的測試套件（test suite）。有時候為了速度上的考量，你可能只決定執行這一部分的測試。目的是確認新的測試結果確實是失敗的。</li>
<li>更新你的功能程式碼，使得測試可以通過。</li>
<li>再次執行你的程式。</li>
<li>如果測試還是失敗，再執行步驟3。</li>
<li>如果通過測試，便重新開始（此時若有重複的程式碼，你便需要重構你的設計）</li>
</ol>
</blockquote>
<p>上面的步驟有提到重構，但也提到重構的前提是如果有重覆的程式碼才要重構。那如果沒有重覆程式碼，那還需要重構嗎？還是如 Steven 所說，去除重覆的程式碼的重構還是不夠徹底的重構？</p>
<p>答案很顯而易見，TDD的步驟本來就沒有規定一定要重構。事實上，重構對有經驗的開發者就是像吃飯或喝水般如此直覺，在開發程式的過程中，諸如像改變函數或變數的名字讓它們變得有意義或容易了解，一段重覆被呼叫的程式碼把它們提取成函數或類別。他們經常無意識地去重構，但有意識地面對他們開發過程所遇到的問題，諸如程式碼的重覆或不易了解等問題，而不是漫無目的的為重構而重構。</p>
<p>是以在開發功能的程式碼，因為編程手法的熟練，重覆碼根本就不存在，難道還需要一個重構的 process 嗎？由此可知，Steven 的徹底重構之說，其實就是邏輯上所謂的以偏概全，用部分的事實擴大到全面性的觀點。但真相是 TDD 未必要進行重構，除非你需要它。所以這當然是邏輯的問題，其實從我指出他邏輯的矛盾之後，他的反應更顯出欠缺邏輯的思辨能力。</p>
<blockquote><p>不如冷靜一點再讀讀這句子，"XP 不需要 Refactoring 是因為徹式地進行 Refactoring"，我就看不明白這是什麼邏輯，一邊說不需要，另一邊說徹底地進行。</p></blockquote>
<p>Steven 上面的質疑乍看來似乎有道理，但仔細看看，原來他把時間的因素拿掉了，有<a href="http://en.wikipedia.org/wiki/Straw_Man">偷換觀念</a>的嫌疑。我原先的說法是說因為先前徹底地重構，所以未來就不需要重構了，被他偷換成一邊說不需要，另一邊說徹底進行。</p>
<p>照他原來的邏輯，如果先前徹底重構的程式碼到後來還需要再次重構，那原來的重構還能叫徹底重構嗎？我們常看到有些人會在問題發生的時候，會說因為別人寫的程式碼出問題，是因為沒有徹底的重構，但在明眼人的眼中，這種說法可以用台語說「出一支嘴」！要嘛有徹底重構先見之明的人，你就再一開始就告訴大家什麼叫做徹底的重構，保證以後不會出問題，省得以後出了問題再說重構不夠徹底，後見之明說重構不夠徹底，這種說法誰都會說但卻根本做不到！</p>
<p>更何況重構的方法有好幾種，決定該怎麼重構的時間還沒到，如何徹底重構？或許有人會想依據未來的預測來徹底重構，但這樣想的人大概忘了敏捷方法是不進行預測的，只有因應現實而改變。</p>
<p>蘇格拉底說：「事情的好壞不重要，重要的是你的想法和看法。」本來針對事情來討論是一件好事，目的是為了思考彼此之間的差異點而獲得成長。無奈 Steven 一開始為了捍衛他的觀點，就針對和他觀點不同觀點的人，拼命強調同人說 TDD 不必然需要 Refactoring 的觀念是錯的，叫我要多看書暗示我不夠用功，其實這樣的行為模式突顯他面對意見紛歧沒有反省思辨的能力。</p>
<p>至於同人提出邏輯思辨的想法就叫做精神層面的說法，同人只能說這其實表現出 Steven 自以為是的「自我感覺良好」，運用修辭技巧閃來閃去還是難逃嚴謹的邏輯批判。他要表演這些同人其實沒什麼意見，他可以繼續活在他的象牙塔之中。但對於同人而言，我的收穫只是多看到一個負面教材，這對我 1/9 敏捷開發分享會的聽眾和閱讀我文章的讀者來說可是一大貢獻呀。Steven 說他不小心傷害我的感情；呵呵，少來了，不禮貌與不尊重的行為代表他不夠理性，只是顯出他的無理而已。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.lifeparty.idv.tw/blog/archives/2737/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>測試驅動開發要徹底重構？</title>
		<link>http://www.lifeparty.idv.tw/blog/archives/2716</link>
		<comments>http://www.lifeparty.idv.tw/blog/archives/2716#comments</comments>
		<pubDate>Wed, 06 Jan 2010 05:29:16 +0000</pubDate>
		<dc:creator>jim yeh</dc:creator>
				<category><![CDATA[問題解決]]></category>
		<category><![CDATA[專案管理]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[溝通]]></category>
		<category><![CDATA[生活感觸]]></category>
		<category><![CDATA[編程技巧]]></category>
		<category><![CDATA[開發流程]]></category>

		<guid isPermaLink="false">http://www.lifeparty.idv.tw/blog/?p=2716</guid>
		<description><![CDATA[對 David Ko 提出 Kent 認為 Red/green/refactor 是 TDD 的三字箴言的說法，同人倒是覺得有探討的必要。以下分享我在 Facebook 回應 David Ko 的觀點，這些觀點應該可以解釋為什麼測試開發不需要徹底重構；其實重構並不是問題，而是到底什麼叫做徹底？而且如果 TDD 可以徹底重構，那麼一開始就可以讓設計一次到位，那寫好的測試程式以後也用不著了，不正是多此一舉？]]></description>
			<content:encoded><![CDATA[<p>同人在〈<a href="http://www.lifeparty.idv.tw/blog/archives/2669">測試驅動開發的精神</a>〉中，提到「Refactoring 並不必然是 TDD 的必要的步驟」的觀點。<a href="http://www.wretch.cc/blog/kojenchieh">David Ko</a> 回應他猜測 Steven 是根據 Kent 在《Test-driven Development》一書的前言中提到的說法，來認為測試和 Refactoring 也是TDD很重要的一環。他說：</p>
<blockquote>
<blockquote><p>&#8230;here&#8217;s what we do: we drive development with automated test, a style of development called Test-Driven Development(TDD). In Test Driven Development:<br />
* Write new code only if an automated test has failed<br />
* Eliminate duplication<br />
&#8230;&#8230;</p>
<p>The two rules imply an oder to the tasks of programming.<br />
1. Red &#8211; Write a little test that doesn&#8217;t work &#8230;<br />
2. Green &#8211; Make the test work quickly&#8230;<br />
3. Refactor &#8211; Eliminate all of the duplication&#8230;.</p>
<p>Red/green/refactor &#8211; the TDD mantra</p></blockquote>
<p>在這段敘述中，Kent 認為 Red/green/refactor 是 TDD 的三字箴言，是 TDD 這個 practice 重要的工作。</p>
<p>所以 Steven 才會強調在做 TDD 時，"測試是第一步"，"Refactoring 本身是 TDD 三步之中其中必要的一步"</p></blockquote>
<p>假如正如 David Ko 所說，測試與 Refactoring 是 TDD 很重要的一環，而來強調 TDD 不應該忽略 Refactoring 的工作，這樣的觀點我可以接受，但同人隨後又看到 Steven 的說法，覺得他自以為是的論點讓我很遺憾。</p>
<blockquote><p>本人從來沒有指出 TDD 是測試 Practice，亦沒有認為測試是 TDD 的目的。</p>
<p>上面的 "功能" 指代碼的內容規格，寫測試時候就是思考新功能的行為，用測試用例的方式去表達出來。就正如：</p>
<p>AssertEquals(3, add(1,2))</p>
<p>那麼簡單，已經是對 add() 作出規範，投入兩個參數，輸出一個數字作結果，並規範了新 fn 的名字是 "add"。寫這句 Assert 時候就應該思考到 add 有兩個參數，投入 1 和 2 就會得 3 這個數字。</p>
<p>重構是 TDD 其中一步，不存在混淆點，再者，沒有測試覆蓋下的根本不能說成重構；另一方面，先寫代碼後補測試是很痛苦的事情。</p>
<p>如果像閣下文中說：「系統不斷演進及需求不斷改變之下，也可能會使架構或設計愈來愈複雜而變得難以維護」，我則認為是在進行 TDD 時候沒有徹底去重構系統。</p>
<p>我建議閣下好好閱讀 TDD 的書本，Refactoring 是 TDD 必需的步驟，由 Kent Beck 到 Robert Martin 以至其他不太有名的 TDD 書本作者，都指出 TDD 的第三步是重構，而不是 "Refactoring 並不必然是 TDD 的必要的步驟。"</p></blockquote>
<p>看到「我建議閣下好好閱讀 TDD 的書本」這種不尊重不同觀點的說法，同人根本就不想浪費時間來跟對方爭論。即使耐著性子仔細看他的論點，像那些「沒有測試覆蓋下的根本不能說成重構；另一方面，先寫代碼後補測試是很痛苦的事情。」、「如果像閣下文中說：『系統不斷演進及需求不斷改變之下，也可能會使架構或設計愈來愈複雜而變得難以維護』，我則認為是在進行 TDD 時候沒有徹底去重構系統。」等說法，更讓同人懷疑他對 TDD 的認知，其實是過於偏向理念化而不夠切合實際。</p>
<p>不過，對 David Ko 提出 Kent 認為 Red/green/refactor 是 TDD 的三字箴言的說法，同人倒是覺得有探討的必要。以下分享我在 <a href="http://www.facebook.com/group.php?gid=179345672472">Facebook 的 Scrum Community in Taiwan</a> 回應 David Ko 的觀點，這些觀點應該可以解釋為什麼測試開發不需要徹底重構；其實重構並不是問題，而是到底什麼叫做徹底？而且如果 TDD 可以徹底重構，那麼代表一開始就可以讓設計一次到位，那寫好的測試程式以後也用不著了，不正是多此一舉？<br />
&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;<br />
柯兄，</p>
<p>我想在點空間，您應該曾看到我與某些只談理論忽略實際的顧問級大師論戰吧？即使當時我和他私下交情還不錯，但看到他用簡單的例子把軟體開發化約成不需要研究 how，或系統分析不需要懂領域知識的觀念，就不得不告訴他，他對軟體開發的認知是不夠全面的。這次對 TDD 實務的分歧點，我想也是差不多的情況。</p>
<p>只不過我不喜歡"我建議閣下好好閱讀 TDD 的書本"這樣的說法，在實務上我看過太多空談理論而不懂思辨問題的人，這種自以為是的論調讓我不想浪費時間回應他，但以下針對柯兄所提來說說我的看法。</p>
<p>單就字面來看，測試是第一步，重構是最後一步，看起來是沒錯，但問題是為什麼呢？測試先行我想應無疑義，我文章也談了很多，在此不再多說。但重構的目的為何？就重構的定義在不改變功能的情況下改善程式結構，以增加程式碼的彈性以利未來增加或改變功能。因此如同那第三步所言，為了去掉重覆性而重構。</p>
<p>但為什麼需要去掉重覆性呢？答案不外乎兩種，一種是日後重覆性造成程式維護的困難與增加錯誤機率，另一種是程式員擔心發生以上的現象或是因為個人對程式設計的信仰所致。</p>
<p>第一種狀況的重構是合理的，因為開發者是針對現實來解決問題；第二種狀況的重構很常見卻不見得是應該被鼔勵的做法，因為開發者是針對理想或信仰來行動，卻不見得符合實際的需要。但這兩種狀況都可以用重覆程式碼的壞味道的概念來合理化重構的行動，所以重構是正確的行動嗎？答案是視情況而定。</p>
<p>一些 TDD 的書籍或教本所舉得例子都很簡單，使得每一步驟都顯得理所當然。但真實的專案真的是這樣嗎？但個人當初在導入 XP 實務的時候，所遇到的困難，都沒辦法從那些書中得到答案，而是要親身體驗；書中教你的 what 其實需要你實施後的 how 及 why，才能更為全面把那些實務做到好。</p>
<p>最後，提一提測試涵蓋面的問題。在我還沒實施 TDD 的時候，當時我很肯定 TDD 的方法，但我想到的問題是測試程式要寫到什麼程度。但等到我開始實施 TDD 的時候，才發現測試程式的涵蓋面根本就不是問題，而是你需要 test driven 幫你做什麼。如果問題具體而明確，那那會有涵蓋面或不涵蓋面的問題？這時，才了解 Peter Ho 及盈學兄當年在點空間說過 TDD 不是測試而是設計的手法。還有那三句擺在我心中的經典名言：</p>
<blockquote><p>Without refactoring, there is no room for plugging in new functionality to adapt the coming force.</p>
<p>Without testing, we couldn’t know where the edge is.</p>
<p>Without integration, the spark of life will die out.</p></blockquote>
<p>一點淺見，供參考，但我絕不會叫別人好好閱讀什麼書或文章才能跟我討論&#8230;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.lifeparty.idv.tw/blog/archives/2716/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>測試驅動開發的精神</title>
		<link>http://www.lifeparty.idv.tw/blog/archives/2669</link>
		<comments>http://www.lifeparty.idv.tw/blog/archives/2669#comments</comments>
		<pubDate>Tue, 05 Jan 2010 10:46:00 +0000</pubDate>
		<dc:creator>jim yeh</dc:creator>
				<category><![CDATA[問題解決]]></category>
		<category><![CDATA[專案監控]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[編程技巧]]></category>
		<category><![CDATA[職場]]></category>
		<category><![CDATA[設計原則]]></category>
		<category><![CDATA[開發流程]]></category>

		<guid isPermaLink="false">http://www.lifeparty.idv.tw/blog/?p=2669</guid>
		<description><![CDATA[測試驅動開發的精神，不應該用一般機械論的觀點來進行工作或任務的化約，而是基於複雜理論的重要觀念；維持穩定與變化的動態平衡，不在於掌握系統核心而在於邊緣，讓變動限定在人們可以掌握的範圍內，這或許才是測試驅動開發最關鍵的精神吧！]]></description>
			<content:encoded><![CDATA[<p>在 Facebook 的 <a href="http://www.facebook.com/group.php?gid=179345672472&amp;ref=share">Scrum Community in Taiwan </a>看到 <a href="http://www.wretch.cc/blog/kojenchieh">David Ko</a> 提到：</p>
<blockquote><p>聽到有人說 TDD 是個測試的 practice，跟 RD 不是那麼有關，我想這是誤會了。TDD是一種設計的活動，它並不是單純在做 verification，它是一種 spec 確認的活動。</p></blockquote>
<p>David Ko 還分享了一篇<a href="http://aydsoftware.blogspot.com/2009/03/tdd-synergy.html">探討 TDD 的文章</a>供大家參考。這讓同人想到我之前曾經<a href="http://www.lifeparty.idv.tw/blog/archives/266">對 InfoQ 的一篇有關軟體開發信仰問題的文章做過的評論</a>。</p>
<p>在那篇文章的評論當中，同人認為作者不應該拿 <a href="http://en.wikipedia.org/wiki/Test-driven_development">TDD</a> 與<a href="http://en.wikipedia.org/wiki/Acceptance_test">驗收測試</a>相提並論，因為 TDD 並不是測試的 practice，而是設計的活動。如果這兩種實務可以相提並論，似乎就像是爭論有了良好品質的設計就可以省略測試，或是增加測試的效率就可以取代設計的重要性。同人當時在我的評論中提到：</p>
<blockquote><p>測試的涵蓋面要夠廣，軟體開發所需的開發成本與時間就要增加，所以與其靠檢驗來控制品質，還不如把設計做好。所以，品質是設計出來的，而非檢驗出來的。</p>
<p>不過，雖然品質並不是檢驗出來的，但軟體要具備足以信賴的品質，卻不能省略測試。舉個例子來說，不管開發者如何確保他的開發所產出的品質，驗收測試是絕對不可能省略的，否則客戶是很難信任軟體是合乎他們需求的。良好的軟體設計可以減輕測試的負擔與壓力，但它絕對無法否定軟體測試的價值。</p></blockquote>
<p>因此，同人認為不該拿 TDD 來與各種形式的測試來做比較，但在 David Ko 提出這個主題後面的討論，我卻看到一個我不太認同的說法。<a href="http://www.facebook.com/profile.php?id=668168338">Steven Mak</a> 提到：</p>
<blockquote><p>世上總有些傻人愛做傻事，阻止不了他們的 <img src='http://www.lifeparty.idv.tw/blog/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' />  不單是由測試去訂定功能規格，更不要忘了 Refactoring!</p></blockquote>
<p>同人以為由測試去訂定功能這個觀點不符合 TDD 的精神，因為正如同我之前一直強調的，TDD 並不是測試。至於 <a href="http://en.wikipedia.org/wiki/Refactoring">Refactoring</a> 是不改變功能的情況下使程式的結構可以更有彈性或是更簡單，它也無法用來訂定功能規格。但 Steven Mak 卻說：</p>
<blockquote><p>TDD 就是由測試去驅動開發功能，寫測試就是第一步，到底有什麼不符合 "TDD 精神"？還有，Refactoring 本身是 TDD 三步之中其中必要的一步，沒有訂定功能用的。但我相信把 TDD 以為是測試的人是忘掉了 Refactoring 這一步。</p></blockquote>
<p>看來 Steven Mak 以測試驅動開發的第一步就是寫測試程式為理由，以為測試並沒有不符合 TDD 的精神，只是認為人們忘卻去做 refactoring，才會以為 TDD 只是用測試來訂定功能規格。同人認為這樣的觀點混淆了測試驅動開發的目的不在測試驅動本身，而是主導開發過程中可以具體落實簡單設計。也就是說測試並非目的，而只是手段而已！</p>
<p>事實上功能不是由測試來訂定的，而是由使用者實際需求來決定。測試程式只是讓開發者找到更具體的方法，來具體驗證使用者的實際需求，這也就是 David Ko 所說確認規格的活動。因此，測試驅動開發的目的是讓我們開發的程式碼能夠符合實際的需要，用可驗證的程式來確定我們開發的程式是符合實際需要的，以避免 <a href="http://en.wikipedia.org/wiki/Overengineering">over-engineering</a> 或是 under-engineering，所以其目的是為了讓我們發現更符合實際需要的設計。</p>
<p>運用測試驅動開發，可以讓開發者的設計儘量簡單，因為開發的目標很具體地呈現在眼前。開發者已經事先對需求的確認花過一番心思考量，把現階段已經確認而且最重要的需求寫成測試程式，一旦開發的程式符合測試程式的要求，那就代表程式已然滿足實際的使用者需求，而使開發者不致浪費心力在不重要或是目前無法確定的需求上。</p>
<p>因此測試驅動開發可以讓開發者立即用最<a href="http://www.lifeparty.idv.tw/blog/archives/888">簡明而單純</a>的設計完成開發工作，好處是讓開發者知道自己在做什麼，而非浪費時間在做沒有意義或無關緊要的事。但任何一開始簡明而單純的設計，在系統不斷演進及需求不斷改變之下，也可能會使架構或設計愈來愈複雜而變得難以維護。這時候單靠 TDD 的簡明與單純也可能沒辦法容納這種複雜性，而是要運用 Refactoring 來增加設計的彈性；也就是在不改變系統功能的情況下，改善系統設計的結構，使它可以因應未來增加需要的功能。</p>
<p>因此 Refactoring 與 TDD 是兩個相互獨立的 practice，雖然 TDD 可以讓重構更為可行。但兩者不必要要混為一談，也就是 Refactoring 並不必然是 TDD 的必要的步驟。</p>
<p>在大部分的情況下，用 TDD 落實簡單的設計就已經夠用了，除非我們真的發現到我們需要更大的彈性才需要進行 Refactoring 的行動。敏捷開發不是強調方法論的教條，因此不管是 TDD 或是 Refactoring，都是為了真實世界的問題的需要，而非方法論的信仰，這也是同人之前評論 InfoQ 那篇文章，最主要想要批判的重點。</p>
<p>因此，雖然 TDD 這三個字母或是測試驅動開發這六個字當中，測試驅動就佔掉了這個名詞的 2/3，但切莫以為 TDD 就是以測試去訂定功能規格，這只是對 TDD 流於表相的認識，而看不清楚這個名詞最重要的兩個字「開發」的真相。</p>
<p>測試驅動開發的精神，不應該用一般機械論的觀點來進行工作或任務的化約，而是基於複雜理論的重要觀念；維持穩定與變化的動態平衡，不在於掌握系統核心而在於邊緣，讓變動限定在人們可以掌握的範圍內，這或許才是測試驅動開發最關鍵的精神吧！</p>
]]></content:encoded>
			<wfw:commentRss>http://www.lifeparty.idv.tw/blog/archives/2669/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Java 泛型複雜嗎？</title>
		<link>http://www.lifeparty.idv.tw/blog/archives/2328</link>
		<comments>http://www.lifeparty.idv.tw/blog/archives/2328#comments</comments>
		<pubDate>Wed, 16 Dec 2009 10:53:07 +0000</pubDate>
		<dc:creator>jim yeh</dc:creator>
				<category><![CDATA[分析設計建模]]></category>
		<category><![CDATA[品質文化]]></category>
		<category><![CDATA[問題解決]]></category>
		<category><![CDATA[學習]]></category>
		<category><![CDATA[編程技巧]]></category>
		<category><![CDATA[職場]]></category>
		<category><![CDATA[設計原則]]></category>
		<category><![CDATA[軟體開發]]></category>

		<guid isPermaLink="false">http://www.lifeparty.idv.tw/blog/?p=2328</guid>
		<description><![CDATA[表面上看起來好像實作泛型可以讓某一段程式碼重複使用，但 Java 在泛型的限制，也增加他重構程式碼的困難度與複雜度。這麼說來，假如石頭成的想法是正確的，用 Java 的泛型來重構程式碼，只會讓程式員沒事自討苦吃。然而，同人在仔細研究他的程式碼之後，發現可以用更簡潔的方式來使用 Java 的泛型。]]></description>
			<content:encoded><![CDATA[<p>石頭成在〈<a href="http://blog.roodo.com/rocksaying/archives/10914229.html">與 metavige 和 alexchen 對話 Java 語言</a>〉這篇文章中，直言他對 Java 語言泛型的批評：</p>
<blockquote><p>我不認為 Java 讓我們「慢工出細活」，我覺得它帶來的是「冗餘的複雜性」。就算以 C++ 的觀點來看 Java 程式碼，我仍覺得 Java 程式碼有許多不必要的複雜度。Java 把類別變成施加在程序員身上的束縛，而是不是幫助我們進行抽象化資料處理的工具。</p>
<p>我個人認為，所謂「更強化的靜態型態」是跟 C++ 樣板相比， Java 泛型比起 C++ 樣板是在走回頭路。就我到目前為止的 Java 使用經驗來看，我幾乎以為泛型只是 Java 專門用來重新設計容器類別的特殊語法。在那以外的場所，你大概不會想用泛型來重構你的程式碼。</p>
<p>metavige 就說會想用 Strategy Pattern 來重構我在〈<a href="http://blog.roodo.com/rocksaying/archives/10890551.html">從 C++ Template 到 Java Generic，一步一步來</a>〉舉的例子，而不會用泛型。但是泛型難道不是用來處理這個問題的直覺想法嗎？ Java 沒有足夠理由說服我們不要用泛型來做，但是用泛型來做&#8230; 呃，似乎更困難。 </p></blockquote>
<p>石頭成用<a href="http://en.wikipedia.org/wiki/Generic_programming">泛型</a>來重構程式碼，是不是處理他的問題之直覺想法？我想石頭成比較偏好動態型態語言的自由，比較不欣賞「更強化的靜態型態」的做法。或許是因為 C++ 用 template 實作不會受到參數型別類型的限制，自然會讓他比較想用泛型來重構程式，以去除資料型態與演算邏輯的相依性。然而，看過石頭成所舉的實例，同人認為用泛型來實作的想法未必是直覺的做法。</p>
<p>表面上看起來好像實作泛型可以讓某一段程式碼重複使用，但 Java 在泛型的限制，也增加他重構程式碼的困難度與複雜度。這麼說來，假如石頭成的想法是正確的，用 Java 的泛型來重構程式碼，只會讓程式員沒事自討苦吃。然而，同人在仔細研究他的程式碼之後，發現可以用更簡潔的方式來使用 Java 的泛型。不過，還是讓我們先來看看石頭成的程式碼：</p>
<p><u>Cx.java</u></p>
<pre class="brush: java; tab-size: 2">
public class Cx&lt;DataType extends IDataType&lt;ReturnType&gt;, ReturnType&gt; {
	private DataType data;

	public Cx(DataType v) {
		data = v;
	}

	public ReturnType getData() {
		return data.value();
	}
}
</pre>
<p><u>IDataType.java</u></p>
<pre class="brush: java; tab-size: 2">
public interface IDataType&lt;ReturnType&gt; {
	     public ReturnType value();
}
</pre>
<p><u>N.java</u></p>
<pre class="brush: java; tab-size: 2">
public class N implements IDataType&lt;Integer&gt; {
	private Integer n;

	public N() {
		n = 0;
	}

	public N(Integer v) {
		n = v;
	}

	public Integer value() {
		return -n;
	}
}
</pre>
<p><u>M.java</u></p>
<pre class="brush: java; tab-size: 2">
public class M implements IDataType&lt;Integer&gt; {
	private Integer m;

	public M() {
		m = 0;
	}

	public M(Integer v) {
		m = v;
	}

	public Integer value() {
		return m * 10;
	}
}
</pre>
<p><u>S.java</u></p>
<pre class="brush: java; tab-size: 2">
public class M implements IDataType&lt;Integer&gt; {
	private Integer m;

	public M() {
		m = 0;
	}

	public M(Integer v) {
		m = v;
	}

	public Integer value() {
		return m * 10;
	}
}
</pre>
<p><u>Main.java</u></p>
<pre class="brush: java; tab-size: 2">
public class Main {
	public static void main(String[] args) {
		N n = new N(1);
		Cx&lt;N, Integer&gt; cn = new Cx&lt;N, Integer&gt;(n);
		System.out.println(cn.getData());

		M m = new M(1);
		Cx&lt;M, Integer&gt; cm = new Cx&lt;M, Integer&gt;(m);
		System.out.println(cm.getData());

		S s = new S("hello");
		Cx&lt;S, String&gt; cs = new Cx&lt;S, String&gt;(s);
		System.out.println(cs.getData());
	}
}
</pre>
<h4>問題</h4>
<p>石頭成認為他的程式存在兩個問題。第一個問題是原來 N、M、S 三個類別沒有繼承關係，但為了符合 Java 泛型的特性，在重構之後這三個類別卻實現了同一個界面。石頭成原來認為用泛型解決演算法重複問題時，應該依然保持那三個類別之間無關性，但重構之後卻發現 Java 的泛型並不能保持那三個類別的無關性。雖然在這個範例中影響不大，但他還是認為 Java 在泛型方面的表現，只能算是勉強及格。</p>
<p>第二個問題是他不知道如何在 Cx 中增加預設建構子，來初始化 data 成員變數。如果 Java 直接 new 一個參數化型別的實例，在下面的程式碼的第 5 行的地方，會出現「Unexpected type」的編譯錯誤訊息。但在 C++ 的 template 中，這種寫法卻是可以被接受的，令他實在是不能理解，JVM 為什麼不能自己反射參數化型別來建立物件實體，而要用這個限制來難倒程式員。</p>
<pre class="brush: java; tab-size: 2">
public class Cx&lt;DataType extends IDataType&lt;ReturnType&gt;, ReturnType&gt; {
  private DataType data;  

  public Cx() {
    data = new DataType();
  }  

  public Cx(DataType v) {
    data = v;
  }  

  public ReturnType getData() {
    return data.value();
  }
}
</pre>
<p>要對石頭成提出的這兩個問題有比較精確的理解，不能不對 <a href="http://en.wikipedia.org/wiki/Concept_%28generic_programming%29">concept 的概念</a>有所認識。在泛型程式設計中，concept 指的是一組型別，它們提供某些語法與語意操作的支援。concept 與抽象基礎類別有關，但 concept 並不需要子型別的關係。</p>
<p>在 C++，concept 這個術語只是簡單地命名並簡單描述參數型別的需求，而符合此 concept 的型別，一般被稱為 model。我們不能用程式語言明確地表達 concept，只能表示某種參數型別的物件會嘗試執行什麼操作，以及期待的工作結果；也就是讓它可以正常的編譯。一般來說，Java 及 C# 的泛型與 C++ 的泛型有點像，只不過它們用 interface 來扮演 concept 的角色。</p>
<p>但這樣一來，符合的參數型別就只能是實作某個明確的界面，當然 concept 顯然會比 interface 更有彈性，因為 concept 可明確代替一組型別，而不只是一個實作某 interface 的類別；而且運用隱式地定義自動 concept，可以讓參數型別同時使用內建型別或其它為了非特殊用途的型別。</p>
<h4>相依性</h4>
<p>從以上的觀念來看，用 C++ 的 template 來實作泛型程式設計，的確比 Java 的 interface 有更大的彈性。不過，即使是使用 concept 來表示參數型別，在這組支援泛型實作的型別中，其實也必須支援 concept 定義的基本操作。就廣義來說，model 必須依存於 concept 所定義的界面。相對於<a href="http://en.wikipedia.org/wiki/Liskov_substitution_principle">物件導向的 LSP</a> 原則，子類別應該可以替換父類別，concept 與 model 之間也有類似的關係。如果 Cb 是 Ca 的強化 concept；也就是定義更多延伸 concept 所需的操作，那麼 Cb 的 model 則必然是 Ca 的 model。</p>
<p>於是，我們看到石頭成的程式，N、M、S 都實作相同的界面 IDataType，這將不會是個問題。其實改動 N，並不會影響到 M、或是 S，因為三者並不直接相依，除非它們共同引用的界面變動才會影響到其它實作相同界面的程式。事實上，界面完全不牽涉到任何實作，除非界面定義得不夠完整，石頭成所擔心的問題才會發生。但如果是這樣，那將不是程式語言的問題，而是設計本身的問題。</p>
<h4>複雜性</h4>
<p>當然，我們也不能否認石頭成提到的另一個問題，是實際存在的事實。Java 使用 interface 來實作泛型確實有比 C++ 的型別 concept 有不足之處。以 interface 來抽象化參數型別沒辦法做到 default construction 這種型別 concept，因為它不是類別，沒有辦法自己生成物件實體，必須要靠外界呼叫者初始化參數型別物件，再傳給泛型物件才能正常工作。</p>
<p>再加上 Java 無法在執行期的時候取得參數型別的關係，我們無法直接以反射機制產生出參數型別的建構式來產生物件實體。雖然繞個圈，我們還是可以用另一種反射方式達到目的，但這樣會使得程式碼變得更複雜而且難以理解，實在是會令人望之卻步。那麼使用 Java 泛型，有沒有比較簡單的方法可以解決這個問題呢？</p>
<p><u>C++</u></p>
<pre class="brush: c++;">
DataType data; // default construction
</pre>
<p><u>Java</u></p>
<pre class="brush: java;">
//data = new DataType();
ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();
data = ((Class) pt.getActualTypeArguments()[0]).newInstance();
</pre>
<h4>解決方法</h4>
<p>其實解決的方法並不難，雖然我們在 Cx 仍然很難用參數型別反射預設建構式來建立物件實體，但如果你對同人過去發表的〈<a href="http://www.lifeparty.idv.tw/blog/archives/449">降低資料存取的複雜性</a>〉還有印象的話，你可以從這篇文章找到解決石頭成問題的方向。我們可以在這篇文章看到，在還沒使用 Java 的泛型之前，UserDataAccessorImpl 有預設建構子可供初始化 UserDataAccessor 的物件實體，但使用 Java 的泛型重構之後，泛型版本的 HibernateDataAccessor 卻只提供指定參數型別 BizObject 的 Class 建構式。所以使用同樣的原理，我們應該也可以解決石頭成的第二個問題。</p>
<pre class="brush: java; tab-size=2">
	public Cx(Class&lt;? extends IDataType&lt;ReturnType&gt; &gt; dataTypeClazz) throws Exception {
		data = dataTypeClazz.newInstance();
	}
</pre>
<p>或許有人可能會說我改變了石頭成第二個問題的需求，他要的不是以 DataType 的 Class 當參數的建構式，而是不需要任何參數的預設建構式。但其實希望在 Cx 加入預設建構式並不是需求，而是為了解決問題的需求所做的假設，要認清需求我們必須質疑存在這個假設背後的問題。</p>
<p>當我們想到 Cx 為什麼需要預設建構式的時候會發現，有些時候因為要<a href="http://www.lifeparty.idv.tw/blog/archives/271">跨越遠端界面呼叫的藩離</a>，可能沒辦法由呼叫者產生參數型別的物件傳給 Cx，而要由 Cx 自己初始參數型別物件。但通常傳入參數型別物件的 Class 是可行的，因為運用 Java 的反射機制，傳遞 Class 就好像傳遞 String 一樣簡單。</p>
<h4>更直覺的做法</h4>
<p>寫到這裡，石頭成的問題應該是解決了。不過回到這篇文章同人一開始所提到的，石頭成所舉的實例，我認為用泛型來實作的想法未必是直覺的做法。那麼同人認為更直覺的做法是什麼？其實我不會完全用泛型來解決石頭成的問題，而是運用多型來封裝演算法的差異性，再搭配泛型來強化參數型別的一致性。這樣我只需要修改石頭成的 Cx 類別與 Main 主程式碼，如下面這段程式碼所示，同人認為是更為簡單而直覺的做法。</p>
<p><u>Cx.java</u></p>
<pre class="brush: java; tab-size: 2">
public class Cx&lt;ReturnType&gt; {
	IDataType&lt;ReturnType&gt; data;

	public Cx(Class&lt;? extends IDataType&lt;ReturnType&gt; &gt; dataTypeClazz) throws Exception {
		data = dataTypeClazz.newInstance();
	}

	public Cx(IDataType&lt;ReturnType&gt; data) {
		this.data = data;
	}

	public ReturnType getData() {
		return data.value();
	}
}
</pre>
<p><u>Main.java</u></p>
<pre class="brush: java; tab-size: 2">
public class Main {
	public static void main(String[] args) {
		N n = new N(1);
		Cx&lt;Integer&gt; cn = new Cx&lt;Integer&gt;(n);
		System.out.println(cn.getData());

		M m = new M(1);
		Cx&lt;Integer&gt; cm = new Cx&lt;Integer&gt;(m);
		System.out.println(cm.getData());

		S s = new S("hello");
		Cx&lt;String&gt; cs = new Cx&lt;String&gt;(s);
		System.out.println(cs.getData());
	}
}
</pre>
<p>上面這一段程式碼，將類別 Cx 當中的 data 的型別，由 DataType 參數型別改成 IDataType 的多型界面。這樣一來，data 的抽象概念就從參數型別轉移到多型界面，此界面封裝了不同的資料型態行為的抽象概念。如此一來，Cx 可減少了一個參數型別，程式碼也變得簡明易懂。從 Main 呼叫端的程式碼就可以發現，少了一個參數型別，程式碼也變得簡潔許多。</p>
<p>這段程式碼也讓我們發現泛型可以強化多型界面的資料型別。假如沒有泛型，我們就只能在 DataType 的 value() 回傳值定義成可以包容各種資料的型別；例如 String 或 Object，然後再運用資料轉換或強制轉型的方式取得我們需要的資料格式，但這些方法很容易出錯，而且會造成程式開發不小的負擔。當我們運用泛型來強化多型界面的型別，程式員就不用在這方面傷腦筋了，而且設計的完整概念也更容易維持。</p>
<h4>泛型與多型的差別</h4>
<p>就設計的觀點來看，參數型別並不是真正的實體型別，如果沒有 default construction 的 concept，我們沒辦法拿它來產生任何實體型別的實體。多型的解決方案，是運用型別來抽象化型別，與泛型用型別的 concept 來抽象化型別是不同的抽象觀點。後者如果想在 Java 的泛型來取代多型的解法，其實是會讓程式員遇到很多麻煩的。</p>
<p>其實即使換成 C++，同人不會把不同資料型態以 DataType 參數型別抽象化。雖然 C++ 的 template 真的很有彈性，但那並不意味著使用它不需要為這些彈性付出代價。例如，使用 C++ template 編程，在除錯上或處理編譯的錯誤會比沒有使用 template 還要困難許多。</p>
<p>尤其是 C++ template 的編輯錯誤訊息，常讓人搞不清楚錯誤在那裡，往往要花費很大的工夫才找到程式員無心犯下微不足道的錯誤。所以， C++ 的 template 給予程式員相當大的彈性，但過度地使用它，也很容易讓程式碼產生不必要的複雜度。</p>
<p>依同人的經驗顯示，許多表面上泛型似乎可以用來解決的多型問題，但一旦用泛型來解，常會因為抽象化層次不同的隔閡而使問題更為複雜。</p>
<p>泛型的原理是引用 concept 來代表各種可供參數化的型別，而多型則是使用抽象類別或界面來封裝具體類別。兩者的抽象化的觀點不一樣，一種是型別的集合對個別型別的關係，另一種則是型別與型別的關係。</p>
<p>前者的關係我們稱為某些型別是某個 concept 的 model，而後者的關係則是型別之間的一般化或實現關係。兩者在本質上的概念是不一樣的，因此當我們想用 concept 來代替 <a href="http://en.wikipedia.org/wiki/Inheritance_%28computer_science%29">inheritance</a>，或是用 inheritance 來取代 concept，最後都必然會讓我們遇到實作與概念上的隔閡。</p>
<p>當一個類別 A 使用到泛型類別 B 時，我們就必須指明使用 B 所需要的參數型別，要不然就必須由使用到類別 A 的類別 C 指定需要的參數型別。所以如果類別 A 想要不侷限在某種資料型別，那麼使用泛型類別 B 的代價就必須是讓類別 A 也變成泛型物件。所以大量使用泛型的結果，會使你一層又一層地使用參數型別，這當然是比多型複雜許多。</p>
<p>此外，以泛型來取代多型更麻煩的是，不同參數型別的泛型物件是完全不同的類別，它們不像多型可以用同樣的方式來處理不同資料型態。所以雖然表面上泛型省去繼承類別或實作界面的工作，看起來比多型的解決方法有更大的自由度。但這也意味著泛型可能會缺少對問題語意封裝抽象概念，只是站在實作觀點以一致性的方法來滿足需求。</p>
<p>程式員不該為了貪圖一致性而忽略多樣性，而是應該按照實際的需要來解決問題。所以重點不是 Java 泛型是否複雜，而是我們能不能運用設計使它變得更簡單。語言的限制未必是加諸在程式員身上的束縛，而可能是讓我們進行思考以發揮設計創意的機會呀。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.lifeparty.idv.tw/blog/archives/2328/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>以字元串列常數當索引鍵值</title>
		<link>http://www.lifeparty.idv.tw/blog/archives/1950</link>
		<comments>http://www.lifeparty.idv.tw/blog/archives/1950#comments</comments>
		<pubDate>Sun, 04 Oct 2009 07:02:32 +0000</pubDate>
		<dc:creator>jim yeh</dc:creator>
				<category><![CDATA[問題解決]]></category>
		<category><![CDATA[編程技巧]]></category>
		<category><![CDATA[職場]]></category>

		<guid isPermaLink="false">http://www.lifeparty.idv.tw/blog/?p=1950</guid>
		<description><![CDATA[最近同人在開發資料剖析的程式時，碰到一個很奇怪的現象。我使用 map 容器來存放剖析資料的結果，然而卻發現以原先新增資料所用的相同鍵值，竟然會找不到該筆資料。但我的測試程式顯示容器中所存放的鍵值與資料，其內容是正確的，但為什麼用相同鍵值去找，結果竟然回傳查無資料呢？原來問題就出在以字元串列常數當索引鍵值。 由於我所開發的這個程式，它接受許多不同格式的資料，各種格式所使用的欄位名稱也都不同。為了設計的簡單化，同人把拆解格式與對映的工作分開來處理，並且把拆解與對映的資料都存放在 map 容器中，而不去設計存放資料物件的類別，以免日後受到欄位規格變動的影響而大動程式碼。 考慮資料處理的效率與易讀性，同人將容器的鍵值設為字元串列常數（const char*）。程式剖析資料的結果，以測試程式確認容器內容無誤，但等到實際呼叫此程式卻發現，無法以容器內含相同鍵值來取得所需要的資料。 怎麼會這樣呢？用走訪 map 容器的方式傾印容器內含資料沒問題，但直接用 find () 取得資料卻得到的是 map::end ()。同人不信邪，用走訪所使用的迭代器所指向的鍵值來 find ()，結果可以正確無誤找到資料。這顯示著直接 find () 和用迭代器指向的鍵值兩者並不一樣，但同樣是 const char*，內容值又一模一樣，兩者應該是相同的才對呀！ 而且，如果是用 const char* 當索引鍵值有問題，那麼為什麼以同樣的容器操作模式，在拆解格式的處理沒有問題，而卻會在欄位對映時出現錯誤呢？比較兩者的差異，我想我知道問題出在什麼地方了。我試著以整數來取代字元串列常數來當索引鍵值，反正雖然各種格式欄位互異，但最後對應到的欄位都是相同的。果然，找不到資料的問題就不再出現了。 其實字元串列鍵值的比較是基於資料存放位址而非資料內含值，只是常數變數編譯器會幫我們把相同內容的字元串列放在一起。但由於同人並不是用宣告 const 常數而是用 #define 的方式來定義常數，對於不同程式碼的相同字元串列常數，可能編譯器並不會將他們視會相同的變數，結果就會造成比對不同而無法找到資料了。 因此，用整數來取代字元串列常數當索引鍵值就可以避免以上的問題，而且也省下用來存放字元串列常數的空間，但卻有數值代碼代表意義不明而造成維護的問題。針對這個問題我們可以宣告 enum 變數來解決，但其索引鍵值還是應該用 unsigned int 而不能使用 enum 型態。為什麼呢？因為 enum 是 equality comparable 而不是 less than comparable，而 map 是 associative container，其鍵值必須是 less than comparable [...]]]></description>
			<content:encoded><![CDATA[<p>最近同人在開發資料剖析的程式時，碰到一個很奇怪的現象。我使用 <a href="http://www.cplusplus.com/reference/stl/map/" target="_blank"><code>map</code> 容器</a>來存放剖析資料的結果，然而卻發現以原先新增資料所用的相同鍵值，竟然會找不到該筆資料。但我的測試程式顯示容器中所存放的鍵值與資料，其內容是正確的，但為什麼用相同鍵值去找，結果竟然回傳查無資料呢？原來問題就出在以字元串列常數當索引鍵值。</p>
<p>由於我所開發的這個程式，它接受許多不同格式的資料，各種格式所使用的欄位名稱也都不同。為了設計的簡單化，同人把拆解格式與對映的工作分開來處理，並且把拆解與對映的資料都存放在 <code>map</code> 容器中，而不去設計存放資料物件的類別，以免日後受到欄位規格變動的影響而大動程式碼。</p>
<p>考慮資料處理的效率與易讀性，同人將容器的鍵值設為字元串列常數（<code>const char*</code>）。程式剖析資料的結果，以測試程式確認容器內容無誤，但等到實際呼叫此程式卻發現，無法以容器內含相同鍵值來取得所需要的資料。</p>
<p>怎麼會這樣呢？用走訪 map 容器的方式傾印容器內含資料沒問題，但直接用 <code>find ()</code> 取得資料卻得到的是 <code>map::end ()</code>。同人不信邪，用走訪所使用的<a href="http://www.sgi.com/tech/stl/Iterators.html" target="_blank">迭代器</a>所指向的鍵值來 <code>find ()</code>，結果可以正確無誤找到資料。這顯示著直接 <code>find ()</code> 和用迭代器指向的鍵值兩者並不一樣，但同樣是 <code>const char*</code>，內容值又一模一樣，兩者應該是相同的才對呀！</p>
<p>而且，如果是用 <code>const char*</code> 當索引鍵值有問題，那麼為什麼以同樣的容器操作模式，在拆解格式的處理沒有問題，而卻會在欄位對映時出現錯誤呢？比較兩者的差異，我想我知道問題出在什麼地方了。我試著以整數來取代字元串列常數來當索引鍵值，反正雖然各種格式欄位互異，但最後對應到的欄位都是相同的。果然，找不到資料的問題就不再出現了。</p>
<p>其實字元串列鍵值的比較是基於資料存放位址而非資料內含值，只是常數變數編譯器會幫我們把相同內容的字元串列放在一起。但由於同人並不是用宣告 <code>const</code> 常數而是用 <code>#define</code> 的方式來定義常數，對於不同程式碼的相同字元串列常數，可能編譯器並不會將他們視會相同的變數，結果就會造成比對不同而無法找到資料了。</p>
<p>因此，用整數來取代字元串列常數當索引鍵值就可以避免以上的問題，而且也省下用來存放字元串列常數的空間，但卻有數值代碼代表意義不明而造成維護的問題。針對這個問題我們可以宣告 <code>enum</code> 變數來解決，但其索引鍵值還是應該用 <code>unsigned int</code> 而不能使用 <code>enum</code> 型態。為什麼呢？因為 <code>enum</code> 是 <a href="http://www.sgi.com/tech/stl/EqualityComparable.html" target="_blank">equality comparable</a> 而不是 <a href="http://www.sgi.com/tech/stl/LessThanComparable.html" target="_blank">less than comparable</a>，而 <code>map</code> 是 <a href="http://www.sgi.com/tech/stl/AssociativeContainer.html" target="_blank">associative container</a>，其鍵值必須是 less than comparable 呀。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.lifeparty.idv.tw/blog/archives/1950/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>降低資料存取的重覆性</title>
		<link>http://www.lifeparty.idv.tw/blog/archives/449</link>
		<comments>http://www.lifeparty.idv.tw/blog/archives/449#comments</comments>
		<pubDate>Sun, 07 Jun 2009 05:20:10 +0000</pubDate>
		<dc:creator>jim yeh</dc:creator>
				<category><![CDATA[分析設計建模]]></category>
		<category><![CDATA[問題解決]]></category>
		<category><![CDATA[學習]]></category>
		<category><![CDATA[編程技巧]]></category>
		<category><![CDATA[設計原則]]></category>

		<guid isPermaLink="false">http://www.lifeparty.idv.tw/blog/?p=449</guid>
		<description><![CDATA[程式碼的重覆性使程式不容易維護、以及增加系統出錯的機率，同時使得程式的再用性難以提昇。因此降低程式碼的重覆性對程式碼的品質與彈性有很大的助益。然而，在資料存取與與物件型別的無法分離的情況下，卻會使得開發者難以對治程式碼的重覆性；相同處理資料的邏輯，受限於處理資料的不同型態，使得開發者必須依據資料型態不同，而改寫同一段程式碼，使得程式碼很難共用。 就像一個比較常見的例子是資料永續存取的實作，常會因為存取資料型態的不同，而影響到資料存取的界面，因此對每一個需要持久化的商業領域物件，都必須重覆地實作它們的 CRUD 存取功能。 雖然透過永續層的概念，我們可以將這種重覆性隔離開來，加上有些 ORM 永續框架的協助，開發資料存取物件的負擔並不太大。然而即使如此，我們有沒有可能避免重覆開發資料存取的元件，不用針對每一種物件型別都要寫一支相對的資料存取物件呢？要達到這樣的目的，我們必須分離資料存取的處理程序與物件型別，但要如何才能做到呢？ 假設我們要開發一個會員註冊的功能，如果考慮以測試來驅動開發過程，我們可能會先撰寫下面的測試程式： public class MemberTester extends TestCase { private static final String MEMBER_SERVICE = "memberSvc"; private ApplicationContext context = null; MemberTester() { context = new ClassPathXmlApplicationContext( "conf/appContext.xml"); } void testRegisterMember() { MemberService memberSvc = (MemberService) context.getBean(MEMBER_SERVICE); User member = new User(); member.setId("myId"); member.setEncodedPassword("abc"); member.setEmailAddress("myId@domain"); memberSvc.RegisterMember(member); CriteriaExpression criteria = Comparison.eq("id", [...]]]></description>
			<content:encoded><![CDATA[<p>程式碼的重覆性使程式不容易維護、以及增加系統出錯的機率，同時使得程式的再用性難以提昇。因此降低程式碼的重覆性對程式碼的品質與彈性有很大的助益。然而，在資料存取與與物件型別的無法分離的情況下，卻會使得開發者難以對治程式碼的重覆性；相同處理資料的邏輯，受限於處理資料的不同型態，使得開發者必須依據資料型態不同，而改寫同一段程式碼，使得程式碼很難共用。</p>
<p>就像一個比較常見的例子是資料永續存取的實作，常會因為存取資料型態的不同，而影響到資料存取的界面，因此對每一個需要持久化的商業領域物件，都必須重覆地實作它們的 <a href="http://en.wikipedia.org/wiki/Create,_read,_update_and_delete">CRUD</a> 存取功能。</p>
<p>雖然透過永續層的概念，我們可以將這種重覆性隔離開來，加上有些 <a href="http://en.wikipedia.org/wiki/Object-relational_mapping">ORM</a> 永續框架的協助，開發資料存取物件的負擔並不太大。然而即使如此，我們有沒有可能避免重覆開發資料存取的元件，不用針對每一種物件型別都要寫一支相對的資料存取物件呢？要達到這樣的目的，我們必須分離資料存取的處理程序與物件型別，但要如何才能做到呢？</p>
<p>假設我們要開發一個會員註冊的功能，如果考慮以測試來驅動開發過程，我們可能會先撰寫下面的測試程式：</p>
<pre class="brush: java">
public class MemberTester
    extends TestCase {
    private static final String MEMBER_SERVICE =
        "memberSvc";
    private ApplicationContext context = null;

    MemberTester() {
	context = new ClassPathXmlApplicationContext(
            "conf/appContext.xml");
    }

    void testRegisterMember() {
        MemberService memberSvc = (MemberService)
            context.getBean(MEMBER_SERVICE);
        User member = new User();

        member.setId("myId");
        member.setEncodedPassword("abc");
        member.setEmailAddress("myId@domain");

        memberSvc.RegisterMember(member);

        CriteriaExpression criteria =
            Comparison.eq("id",
            ConstantExpression.getString("myId"));
        ParameterContext parameterContext =
            new HashtableParameterContext();
        User members[] =
            memberSvc.getMembers(criteria,
            parameterContext, 0);

        assertEquals(members.size(), 1);

        for (int i = 0; i < members.length();
            i ++) {
            assertEquals(member.getId(),
                members[i].getId());
            assertEquals(
                member.getEncodedPassword(),
                members[i].getEncodedPassword());
            assertEquals(
                member.getEmailAddress(),
                members[i].getEmailAddress());
        }

        memberSvc.removeMember(member);
    }
}
</pre>
<p>從測試程式找到應用服務的界面 <em>MemberService</em> ，並進一步實現符合此界面的服務元件：</p>
<pre class="brush: java">
public interface MemberService {
    public User [] GetMembers(
        CriteriaExpression criteria,
        ParameterContext context, int maxCount);
    public void RegisterMember(User member);
    public void UpdateMemberInfo(User member);
    public void removeMember(User member);
}
</pre>
<pre class="brush: java">
public class MemberServiceImpl
    implements MemberService {
    private UserDataAccessor
        memberDataAccessor = null;

    public User [] GetMembers(
        CriteriaExpression criteria,
        ParameterContext context,
        int maxCount) {
        return memberDataAccessor.find(criteria,
            context, maxCount);
    }

    public void RegisterMember(User member) {
        memberDataAccessor.insert(member);
    }

    public void UpdateMemberInfo(User member) {
        memberDataAccessor.update(member,
            LockMode.UPGRADE);
    }

    public void removeMember(User member) {
        memberDataAccessor.remove(member,
            LockMode.UPGRADE);
    }

    public void setMemberDataAccessor(
        UserDataAccessor memberDataAccessor) {
        this.memberDataAccessor =
            memberDataAccessor;
    }

    public UserDataAccessor
        getMemberDataAccessor() {
        return memberDataAccessor;
    }

}
</pre>
<p>實作服務元件會使用到界面 <em>UserDataAccessor</em> 來存取會員資料，利用永續框架如 <a href="http://www.hibernate.org/">Hibernate</a>，可以減輕我們開發資料存取功能的實作負擔。</p>
<pre class="brush: java">
public UserDataAccessor {
    public void insert(User obj);
    public void update(User obj);
    public BizObject get(Serializable id);
    public void remove(User obj);
    User [] find(CriteriaExpression criteria,
        ParameterContext context,
        int maxCount);
}
</pre>
<pre class="brush: java">
public class UserDataAccessorImpl
    implements UserDataAccessor
    extends HibernateDataAccessor {

    public UserDataAccessorImpl() {
        super();
    }

    public void insert(User user) {
        insertBizObj(user);
    }

    public User get(Serializable id) {
        return (User) getBizObj(id);
    }

    public void update(User user) {
    	updateBizObj(user);
    }

    public void remove(User user) {
        removeBizObject(user);
    }

    public User [] find(
        CriteriaExpression criteria,
        ParameterContext context,
        int maxCount) {
        List userList = findBizObjs(criteria,
            context, maxCount);
        User users[] = new User[userList.size()];

        for (int i = 0; i < userList.size();
            i ++) {
            users[i] = userList.get(i);
        }

        return users;
    }
}
</pre>
<p>以上的程式是假設我們已經開發好供資料存取物件繼承的 <em>HibernateDataAccessor</em> 的基礎類別。它被定義為 abstract class，只提供 protected methods 供衍生類別呼叫物件存取的功能，例如上面程式片段的 <em>insertBizObj</em>、<em>updateBizObj</em>、<em>getBizObj</em>、<em>removeBizObj</em>、以及 <em>findBizObjs</em> 等方法。這樣使得永續存取物件的實作負擔並不重，但美中不足的是仍然因應不同的物件開發他們所需要的永續存取物件，而各個永續存取物件之間最大的差異卻只是不同物件的轉型而已，而顯得有些重覆而笨拙。</p>
<p>所幸 <a href="http://en.wikipedia.org/wiki/Java_(programming_language)">Java</a> 5.0 以後提供的<a href="http://en.wikipedia.org/wiki/Generic_programming">泛型程式設計</a>，可以幫我們參數化存取物件的型態，這樣就可讓資料存取的處理程序，與資料形態完全分離。因此我們可以設計適用存取各種資料型態的泛型資料存取界面如下。</p>
<pre class="brush: java">
public interface DataAccessor&lt;BizObject,
    LockType&gt;
    extends Persister&lt;BizObject,
    LockType&gt;,
    FinderExecutor&lt;BizObject&gt; {
}

public interface Persister&lt;BizObject,
    LockType&gt; {
    public void insert(BizObject obj);
    public void update(BizObject obj,
        LockType lock);
    public BizObject get(Serializable id,
        LockType lock);
    public void remove(BizObject obj,
        LockType lock);
}

public interface FinderExecutor&lt;BizObject&gt; {
    List&lt;BizObject&gt; find(
        CriteriaExpression criteria,
        ParameterContext context, int maxCount);
    void addFilter(String name, Map&lt;String,
        Object&gt; params);
    void removeFilter(String name);
}
</pre>
<p>為了讓資料存取的權責更清楚而明確，我們使 <em>DataAccessor</em> 組合兩種抽象概念，一個是永續物件的存取、另一個則是找出符合查詢條件的永續物件序列。依據 <a href="http://en.wikipedia.org/wiki/OOAD">OOAD</a> 的 <a href="http://www.objectmentor.com/resources/articles/isp.pdf">ISP</a> 原則，將這兩種不同概念分離成兩個界面，並由實作 <em>DataAccessor</em> 界面的程式來分別實作這兩個界面。</p>
<pre class="brush: java">
public class HibernateDataAccessor&lt;
    BizObject&gt; implements
    DataAccessor&lt;BizObject, LockMode&gt; {

    private Class&lt;BizObject&gt; bizObjType;
    private SessionFactory sessionFactory = null;
    private HibernateTemplate hibernateTemplate
        = null;

    public static int MAX_COUNT_IS_NOT_LIMITED
        = 0;

    public HibernateDataAccessor(
        Class&lt;BizObject&gt; bizObjType) {
        super();
        this.bizObjType = bizObjType;
    }

    public void insert(BizObject obj) {
        if (hibernateTemplate != null) {
            hibernateTemplate.save(obj);
            hibernateTemplate.flush();
        }
    }

    public BizObject get(Serializable id,
        LockMode lock) {
    	BizObject bizObj = null;

        if (id != null &#038;&#038;
            hibernateTemplate != null) {
            hibernateTemplate.flush();
            hibernateTemplate.get(bizObjType, id,
                lock);
        }

        return bizObj;
    }

    public void update(BizObject obj,
        LockMode lock) {
        if (hibernateTemplate != null) {
            hibernateTemplate.update(obj, lock);
            hibernateTemplate.flush();
        }
    }

    public void remove(BizObject obj,
        LockMode lock) {
        if (hibernateTemplate != null) {
            hibernateTemplate.delete(obj, lock);
            hibernateTemplate.flush();
        }
    }

    public void setSessionFactory(
        SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
        this.hibernateTemplate =
            new HibernateTemplate(
                this.sessionFactory);
    }

    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public List&lt;BizObject&gt; find(
        CriteriaExpression criteria,
        ParameterContext context, int maxCount) {

        if (hibernateTemplate != null)	{
            QueryCriteria fromBizObject =
                QueryCriteria.from(bizObjType);
            HibernateHqlQueryBuilder builder =
                getHqlBuilder(
                fromBizObject.where(criteria,
                context));

            hibernateTemplate.flush();
            hibernateTemplate.setMaxResults(
                maxCount);

            if (builder.getParamValues().length
                > 0) {
                return hibernateTemplate.find(
                    builder.getHqlText(),
                    builder.getParamValues());
            } else {
                return hibernateTemplate.find(
                    builder.getHqlText());
            }
        }
        else {
            return null;
        }
    }

    private HibernateHqlQueryBuilder
        getHqlBuilder(
        QueryCriteria criteria) {
        HibernateHqlQueryBuilder builder =
            new HibernateHqlQueryBuilder();
        CriteriaQueryDirector director =
            new CriteriaQueryDirector(builder);

        director.makeQuery(criteria,
            criteria.getContext());

        return builder;
    }

    public void addFilter(String name, Map&lt;String,
        Object&gt; params) {
        if (hibernateTemplate != null)
        {
            Filter filter =
                hibernateTemplate.enableFilter(
                name);
            Iterator&lt;
                Map.Entry&lt;String,
                Object&gt; &gt; it
                    = params.entrySet().iterator();

            while(it.hasNext())
            {
                Map.Entry&lt;String, Object&gt; mapEntry
                    = it.next();
                filter.setParameter(
                    mapEntry.getKey(),
                    mapEntry.getValue());
            }
        }
    }

    public void removeFilter(String name) {
        if (hibernateTemplate != null) {
            final String filterName = name; 

            hibernateTemplate.execute(
                new HibernateCallback() {
                public Object doInHibernate(
                    Session session)
                    throws HibernateException,
                    SQLException {
                        session.disableFilter(
                            filterName);

                        return null;
                }
            });
        }
    }
}
</pre>
<p>上面這段程式用到同人過去分享的「<a href="http://www.lifeparty.idv.tw/blog/archives/192">永續物件查詢的設計</a>」來一般化查詢商業物件的方法，讓我們不會受到 ORM 的框架而影響影響 DataAccossor 的操作界面。同人建立一個共通的表述語法稱為 QueryExpression，讓查詢商業物件的實作方式不會因為永續框架的改變而受到影響。</p>
<p>再加上 BizObject 與 LockMode 型態的參數化，使我們不需要為不同的物件型態或永續框架的鎖定機制而重覆開發資料存取元件。此外，考慮物件可能會有需要事先定義過濾條件的需求，我們也在 <em>FinderExecutor</em> 界面中加入了 <em>AddFilter()</em> 及 <em>RemoveFilter()</em> 的操作界面。使用 Hibernate 的泛型資料存取元件，只須略為修改我們的會員服務的實作，我們根本無須額外開發會員資料的資料存取物件。</p>
<pre class="brush: java">
public class MemberServiceImpl
    implements MemberService {

    private DataAccessor&lt;User,
        LockMode&gt; memberDataAccessor
            = null;

    public List&lt;User&gt; getMembers(
        CriteriaExpression criteria,
        ParameterContext context,
        int maxCount) {
            return memberDataAccessor.find(
                criteria, context, maxCount);
	}

    public void registerMember(
        User member) {
        memberDataAccessor.insert(
            member);
    }

    public void updateMemberInfo(
        User member) {
        memberDataAccessor.update(
            member, LockMode.UPGRADE);
    }

    public void setMemberDataAccessor(
        DataAccessor&lt;User, LockMode&gt;
        memberDataAccessor) {
        this.memberDataAccessor
            = memberDataAccessor;
	}

    public DataAccessor&lt;User, LockMode&gt;
        getMemberDataAccessor() {
        return memberDataAccessor;
    }

    public void removeMember(User member) {
        memberDataAccessor.remove(
            member, LockMode.UPGRADE);
    }

}
</pre>
<p>我們只須在注入 memberDataAccessor 時，以 User 的資料型態傳入建構子的參數初始化資料存取物件，就不需要再為 User 開發資料存取物件了，因為資料存取的處理邏輯與資料型態已經分離，彼此不再相互耦合，這正是泛型技術帶給我們的一大優勢呀。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.lifeparty.idv.tw/blog/archives/449/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>訊息交易的抽象化思考</title>
		<link>http://www.lifeparty.idv.tw/blog/archives/431</link>
		<comments>http://www.lifeparty.idv.tw/blog/archives/431#comments</comments>
		<pubDate>Tue, 03 Feb 2009 10:00:22 +0000</pubDate>
		<dc:creator>jim yeh</dc:creator>
				<category><![CDATA[分析設計建模]]></category>
		<category><![CDATA[品質文化]]></category>
		<category><![CDATA[問題解決]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[易經思維]]></category>
		<category><![CDATA[生活感觸]]></category>
		<category><![CDATA[編程技巧]]></category>
		<category><![CDATA[設計原則]]></category>

		<guid isPermaLink="false">http://www.lifeparty.idv.tw/blog/archives/431</guid>
		<description><![CDATA[在〈開發者的 common sense〉的留言中，同人看到一些網友的批評。我發現這些批評顯示了有些開發者不擅於抽象化思考，而習慣於用經驗法則來取代思考。然而，誠如 Brooks 所言：「軟體的本質是複雜的，而不是偶然發生的」對治複雜度本來就是開發者的天職，而軟體開發的抽象化思考則是其用以統理複雜度的利器。由此看來，那些網友的批評著實令人為他們捏一把冷汗呀。 從路邊的垃圾桶與路人的留言，我們可以發現他們弄錯 common sense 的意思。他們認為 common sense 不能一概而論，因為每個人的 common sense 都不同。但這樣的觀點令人感到疑惑，如果每個人的 common sense 都不一樣，所以無法一概而論，那這種 common sense 還能叫做 common sense 嗎？ 到底他們觀點上邏輯的矛盾，問題是出在那裡呢？同人認為問題並不是開發者的 common sense 不存在，而是忽略了開發者應該將經驗化成一般性的通用概念。舉例來說，軟體工程領域本身就是從實務發展出來的理論，其中許多概念就是開發者必須知道的 common sense，對開發者來說是合理的知識，也是他們都知道、無須解釋或加以論證的常識。 由此可知，如果開發者缺乏一般性的通用概念，那他碰到問題就很難舉一反三，自然也就難以掌握重點，而只能依據表相來處理問題，往往使得問題變得更複雜。oofunp 的留言就很像欠缺概念思考的開發者常見的反應，一開始以自己熟悉的技術來看問題，最後才發現自己對問題的理解是錯誤的。尤其「以為抽象化是將資料庫定義抽象化」的想法，更是整個弄錯抽象化思考的意義，結果最後他還是誤解了業務規則的意思。 同人前一篇文章所提到的業務規則，並非來自技術領域上萬用的設計，而是對問題領域經過抽象化思考後，所萃取而得到可以解決業務需求的重要概念。顯然 oofunp 的誤解是以技術的角度來看待抽象化思考，才會產生嚴重的觀念混淆。事實上，抽象化思考重視的不是技術實作，而是如何從實際問題當中萃取出重要的抽象概念，以增進我們對問題領域的瞭解，才能採用最適當的技術來開發軟體系統。 此外，過份強調技術經驗而輕忽概念性思維的開發者，很容易表現出自己對問題的盲目。就像 X files 留言的批評一樣，責怪同人沒有交待清楚是 XML 格式的問題，直到別人提出質疑才說明與列出參考文獻，認為同人缺乏部落格文章寫作的 common sense。但我的文章已經很清楚地提到是有關「交易訊息」語法的問題，難道他不瞭解交易訊息是 XML 技術的一般化抽象概念表述嗎？XML 只是實現交易訊息概念的一種技術，用以解決跨系統整合的問題。如果他要看到 XML 字眼才知道是訊息格式的問題，那改天換了另一種交易訊息的實作方式，我想大概他腦筋又要轉不過來了吧。 以上網友的三種批評，表面上看起來好像是不同的問題，但其背後都存在同樣的本質，那就是從交易訊息的問題中可見一斑，他們無法以抽象性思考來看待軟體開發的問題。但為什麼開發者需要抽象化思考呢？可能有人會認為，只有 OOA、OOD、或是 OOP 才會需要抽象化思考，不用物件導向的開發典範，應該不需要抽象化思考。其實這樣的觀念是錯誤的，不管我們採用什那一種開發典範來開發系統，抽象化思考都是必備的能力，只是不同的開發典範需要不同的抽象層面。 抽象思考本來就是人類本能的思維能力，讓我們可以因應變化掌握事物的核心關鍵。這正是《易經繫辭》所言「一陰一陽之謂道，繼之者善也，成之者性也。仁者見之謂之仁，知者見之謂之知，百姓日用而不知。故君子之道鮮矣」的道理，我們常在不知不覺中運用抽象化的概念。最近看到《世紀末軟體革命復刻版》說得好，從小我們就具備了抽象的思考能力，我們可以把我們所關心的特性，從事物中「抽」出來，看出一些形而上、感官之外、富於意義的特性來。 這本書還提到有關抽象化思考的一個重點，事物的意義是針對問題的需要而來。世間事物的特性多到數不清，但藉由抽象化的過程，我們可以把它抽象化為只剩下一些我們需要的特性。我們只要掌握這些關心的特性，就可以讓世間的事物更容易處理。例如一般人所知道的「冰」，在愛斯基摩人眼中卻可分類為十幾種，只因為需要不一樣；我們只要知道冰的概念就行了，但愛斯基摩人對於冰的用處有許多，所以需要更細膩冰的分類。因此，因應需要的不一樣，抽象化思考的結果也會有所不同。 讓我們回到訊息交易系統的開發，抽象化思考通常被稱做資訊隱藏，萃取最有意義的資訊，並去除其它不必要的資訊。以欄位內容取值為例，空白或是空值都只是眾多可能參考欄位內含值的一種，不論空白與否，都不影響系統實際運作的處理程序，因此空白或空值與否的資訊，對系統並不重要應予以略除，這是屬於語法層次的抽象化思考。 [...]]]></description>
			<content:encoded><![CDATA[<p>在〈<a href="http://www.lifeparty.idv.tw/blog/archives/430">開發者的 common sense</a>〉的留言中，同人看到一些網友的批評。我發現這些批評顯示了有些開發者不擅於<a href="http://en.wikipedia.org/wiki/Abstraction">抽象化思考</a>，而習慣於用經驗法則來取代思考。然而，誠如 <a href="http://en.wikiquote.org/wiki/Fred_Brooks">Brooks</a> 所言：「軟體的本質是複雜的，而不是偶然發生的」對治複雜度本來就是開發者的天職，而軟體開發的抽象化思考則是其用以統理複雜度的利器。由此看來，那些網友的批評著實令人為他們捏一把冷汗呀。</p>
<p>從路邊的垃圾桶與路人的留言，我們可以發現他們弄錯 common sense 的意思。他們認為 common sense 不能一概而論，因為每個人的 common sense 都不同。但這樣的觀點令人感到疑惑，如果每個人的 common sense 都不一樣，所以無法一概而論，那這種 common sense 還能叫做 common sense 嗎？</p>
<p>到底他們觀點上邏輯的矛盾，問題是出在那裡呢？同人認為問題並不是開發者的 common sense 不存在，而是忽略了開發者應該將經驗化成一般性的通用概念。舉例來說，軟體工程領域本身就是從實務發展出來的理論，其中許多概念就是開發者必須知道的 common sense，對開發者來說是合理的知識，也是他們都知道、無須解釋或加以論證的<a href="http://zh.wikipedia.org/w/index.php?title=常識&amp;variant=zh-tw">常識</a>。</p>
<p>由此可知，如果開發者缺乏一般性的通用概念，那他碰到問題就很難舉一反三，自然也就難以掌握重點，而只能依據表相來處理問題，往往使得問題變得更複雜。oofunp 的留言就很像欠缺概念思考的開發者常見的反應，一開始以自己熟悉的技術來看問題，最後才發現自己對問題的理解是錯誤的。尤其「以為抽象化是將資料庫定義抽象化」的想法，更是整個弄錯抽象化思考的意義，結果最後他還是誤解了業務規則的意思。</p>
<p>同人前一篇文章所提到的業務規則，並非來自技術領域上萬用的設計，而是對問題領域經過抽象化思考後，所萃取而得到可以解決業務需求的重要概念。顯然 oofunp 的誤解是以技術的角度來看待抽象化思考，才會產生嚴重的觀念混淆。事實上，抽象化思考重視的不是技術實作，而是如何從實際問題當中萃取出重要的抽象概念，以增進我們對問題領域的瞭解，才能採用最適當的技術來開發軟體系統。</p>
<p>此外，過份強調技術經驗而輕忽概念性思維的開發者，很容易表現出自己對問題的盲目。就像 X files 留言的批評一樣，責怪同人沒有交待清楚是 XML 格式的問題，直到別人提出質疑才說明與列出參考文獻，認為同人缺乏部落格文章寫作的 common sense。但我的文章已經很清楚地提到是有關「交易訊息」語法的問題，難道他不瞭解交易訊息是 XML 技術的一般化抽象概念表述嗎？XML 只是實現交易訊息概念的一種技術，用以解決跨系統整合的問題。如果他要看到 XML 字眼才知道是訊息格式的問題，那改天換了另一種交易訊息的實作方式，我想大概他腦筋又要轉不過來了吧。</p>
<p>以上網友的三種批評，表面上看起來好像是不同的問題，但其背後都存在同樣的本質，那就是從交易訊息的問題中可見一斑，他們無法以抽象性思考來看待軟體開發的問題。但為什麼開發者需要抽象化思考呢？可能有人會認為，只有 <a href="http://en.wikipedia.org/wiki/Object-oriented_analysis_and_design">OOA、OOD</a>、或是 <a href="http://en.wikipedia.org/wiki/Object_Oriented">OOP</a> 才會需要抽象化思考，不用物件導向的開發典範，應該不需要抽象化思考。其實這樣的觀念是錯誤的，不管我們採用什那一種開發典範來開發系統，抽象化思考都是必備的能力，只是不同的開發典範需要不同的抽象層面。</p>
<p><a href="http://www.anobii.com/books/002b0bcc4b2bca5604/" title="更多關於世紀末軟體革命復刻版"><img src="http://image.anobii.com/anobi/image_book.php?type=4&amp;item_id=002b0bcc4b2bca5604&amp;time=0" style="padding: 5px; display: inline; float: right" title="更多關於世紀末軟體革命復刻版" alt="世紀末軟體革命復刻版的圖像" width="93" height="132" /></a> 抽象思考本來就是人類本能的思維能力，讓我們可以因應變化掌握事物的核心關鍵。這正是《易經繫辭》所言「一陰一陽之謂道，繼之者善也，成之者性也。仁者見之謂之仁，知者見之謂之知，百姓日用而不知。故君子之道鮮矣」的道理，我們常在不知不覺中運用抽象化的概念。最近看到《<a href="http://www.anobii.com/books/002b0bcc4b2bca5604/" title="更多關於世紀末軟體革命復刻版">世紀末軟體革命復刻版</a>》說得好，從小我們就具備了抽象的思考能力，我們可以把我們所關心的特性，從事物中「抽」出來，看出一些形而上、感官之外、富於意義的特性來。</p>
<p>這本書還提到有關抽象化思考的一個重點，事物的意義是針對問題的需要而來。世間事物的特性多到數不清，但藉由抽象化的過程，我們可以把它抽象化為只剩下一些我們需要的特性。我們只要掌握這些關心的特性，就可以讓世間的事物更容易處理。例如一般人所知道的「冰」，在愛斯基摩人眼中卻可分類為十幾種，只因為需要不一樣；我們只要知道冰的概念就行了，但愛斯基摩人對於冰的用處有許多，所以需要更細膩冰的分類。因此，因應需要的不一樣，抽象化思考的結果也會有所不同。</p>
<p>讓我們回到訊息交易系統的開發，抽象化思考通常被稱做<a href="http://en.wikipedia.org/wiki/Information_hiding">資訊隱藏</a>，萃取最有意義的資訊，並去除其它不必要的資訊。以欄位內容取值為例，空白或是空值都只是眾多可能參考欄位內含值的一種，不論空白與否，都不影響系統實際運作的處理程序，因此空白或空值與否的資訊，對系統並不重要應予以略除，這是屬於語法層次的抽象化思考。</p>
<p>當然在實際業務的需要上，空白或空值可能會有特殊的意義，但那並非欄位取值過程本身應該處理的問題，而是由取值後續的檢核資料合理性的規則來處理，這是屬於語意層次的抽象性思考。雖然為了系統執行效能的考量，語法與語意可能會寫在同一支程式或程序當中。但以程式的設計概念來看，因為技術實作的因素混淆語意與語法，而破壞設計概念的整體性，則是<a href="http://www.lifeparty.idv.tw/blog/archives/262">無異於自廢武功而使軟體品質問題層出不窮</a>。</p>
<p>相信沒有人會否認良好的設計概念應該是「分而治之」，我們在某些訊息交易的標準中，也確實看得到語法與語意分而治之的例子。例如一些 <a href="http://en.wikipedia.org/wiki/Electronic_Data_Interchange">EDI</a> 的訊息標準會以 MIG（訊息建置指引）來規範交易訊息格式，包括欄位的強制性、可重覆出現的次數、欄位可能的內含值來代表訊息的語法，而且多半是通行於相同功能領域的共通語法。而對於相同功能領域不同的交易，則會以 SIG（系統建置指引）來規範特定交易的訊息語意，用來關聯到實務上的業務規則。</p>
<p>早年的 EDI 訊息交易的系統，很多都不是用物件導向的方法所實作出來的，但其系統結構卻與物件導向的抽象化思維不謀而合，這代表什麼呢？同人還是喜歡引用《易經繫辭》的道理，正是「天下事一致而百慮，殊途而同歸」、「易窮則變、變則通、通則久」，事物的核心本質本來就存在，只在於我們如何找出有意義的概念出來，並且加以演化而讓系統不斷進展，重點還在於我們懂不懂得抽象化思考呀。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.lifeparty.idv.tw/blog/archives/431/feed</wfw:commentRss>
		<slash:comments>9</slash:comments>
		</item>
		<item>
		<title>再談程式設計的迷思</title>
		<link>http://www.lifeparty.idv.tw/blog/archives/424</link>
		<comments>http://www.lifeparty.idv.tw/blog/archives/424#comments</comments>
		<pubDate>Fri, 16 Jan 2009 08:19:14 +0000</pubDate>
		<dc:creator>jim yeh</dc:creator>
				<category><![CDATA[CNet/ZDNet]]></category>
		<category><![CDATA[分析設計建模]]></category>
		<category><![CDATA[問題解決]]></category>
		<category><![CDATA[學習]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[編程技巧]]></category>
		<category><![CDATA[職場]]></category>
		<category><![CDATA[設計原則]]></category>

		<guid isPermaLink="false">http://www.lifeparty.idv.tw/blog/archives/424</guid>
		<description><![CDATA[昨天同人在〈又見少了概括性論點〉提到〈必須面對的真相─五大程式設計迷思〉在文章結構上的問題。其實那篇文章除了結構問題之外，同人也在該篇文章內容中，發現了一些值得探討的問題，因此想在這篇文章發表我的看法。 以該篇文章內容而言，同人認為值得探討的有兩個地方，一個是程式語言一直再改變的迷思、另一個則是作者建議讀者，儘量避免用遞迴的方式來寫作程式。第一個問題只是沒有交待清楚作者想要表達的概念，而第二個問題就是嚴重的偏見了，值得讓人進行思辨以建立正確的觀念。 為什麼程式語言一直再改變是個迷思？那篇文章就有讀者質疑這個迷思，他說： 關於這部份，我覺得不如講＂只有XXX語言才能作某件事情＂ 我不覺得＂程式語言的改變＂是迷思 不過，同人認為所謂「程式語言一直在改變」的迷思，作者的意思不單單只是說程式語言沒有一直在改變的意思，而是還有更深層的涵義沒有表達出來。事實上，程式語言一直再改變是個事實，我們不該說它們沒有一直改變。而是這些改變並不會讓我們先前對程式語言學習的投資白費。 同人認為，作者提到「電腦程式語言的演進，就像人類的語言演進一樣，有其一定的過程」是正確的。但對程式語言的學習而言，重點並不在於「程式語言不會一直在改變」，而是在於改變通常不會影響程式語言舊版本的語法，就算存在一些新舊版本不相容的影響，但只要掌握基本觀念，新的改變是不需要重新學起，而且我們經常還能在不同的程式語言之間，發現彼此相通的地方。 其實程式語言會不斷演化是必然的，「成熟」的程式語言反而是象徵衰退的開始。因為隨著時代的進步，程式設計的問題只會愈變愈複雜，造就了新的程式設計概念。如果程式語言太成熟，以致無法改變來加入新的程式設計概念，那麼它們將被時代所淘汰。所以，問題不在於程式語言是不是臻於成熟，因為改變是必然的，重點在於你投資在程式語言的學習並不會白費。 再來，作者提到「千萬不要認為電腦已經跑得很快了，就不注意演算方法的選擇。」同人認為是正確的。但他舉例指出遞迴函式效率較不佳、比非遞迴寫法的程式容易做重複計算，建議大家儘量避免用非遞迴的寫法來進行程式寫作的說法，同人就完全不能夠認同作者的看法。 沒有錯，遞迴函式的確比非遞迴的寫法會佔去更多的記憶體，但以空間換取時間的角度來看，說遞迴函式比非遞迴寫法更沒有效率，則是過份簡化的說法。即使以作者舉計算費式數列的遞迴函式的例子來看，遞迴函數可能有重複計算的問題，但到底費式數列的 n 值要多大，遞迴函數的執行速度才會發生顯著的影響，這卻是值得探討的問題。 如果在一般解決領域問題的條件限制上，都不見某種演算法的缺點，會產生顯著的影響，但卻能享受到該種演算法的優點，那麼我們是不是應該選用這樣的演算法來設計程式呢？答案想必是肯定的。這不是把問題推到硬體速度夠不夠快的問題，而是演算法的選擇應該透過客觀的分析與理性的思考，才能找到最適當的選擇。 這讓人也發現到程式設計常見的迷思，很容易誤導我們對程式開發的看法；太過相信經驗法則而缺乏對問題的深入思考，容易人云亦云地以為該用什麼演算法或避免什麼演算法，而那些都是程式設計師自以為是的偏見。 我想到哈米尼斯對同人分享永續物件設計所提出的意見，他提到： 就現實問題而言，一家公司裡，可能許多套系統都是由不同的廠商開發，若以這樣的角度來看，這一個架構似乎有點問題。 再來則是效能考量的問題，我認同這一個架構下，可以讓開發和維護變得簡潔，但若是遇到某些特定的系統，如下單系統，它強調的重點會在反應時間及大量資料處理，若是以此為考量，這樣的架構似乎又繞了太多層？ 哈米尼斯的意見正是同人在工作上常面臨的質疑，但實際上我所提出來的設計是經過驗證過的架構，運用服務導向的觀念、以及物件導向設計的設計手法，已成功整合銀行各種不同廠商開發的子系統。它實際應用在銀行的付款系統，所以對績效的要求是非常嚴苛的，然而實際上它的效能卻未見如哈米尼斯所顧慮到的問題。 受限於自己的經驗，很多程式設計師傾向於認定太多「系統間接層」不好，但那只是一己想法，而未經證實的猜測。如果真的擔心發生這樣的問題，應該採取行動進行驗證，而不是停留在想法的猜測，這才是程式設計者應有的積極態度。 事實上，間接層的設計只是把原來放在一起的程式碼區分好責任，去掉重覆性而已，我們對系統繞太多層的理解不一定是系統運作的真相。除非我們做了設計概念驗證，讓事實或數據說話，而不是經驗的推論，後者往往來自於個人徧見所致。 同人無意在這裡討論那一種程式開發典範比較好，因為那是不會有結果的爭論，只有在了解程式想要解決的問題之後，討論用什麼方式來開發程式才會有意義。 演算法的選擇也是同樣的道理，每一種演算法都有它的優缺點，我們應當針對問題的目標與限制來「思考」最適當的演算法，而非慣性地「記憶」那一種方法用在每一種情況都比較好、或比較不好。因此作者建議儘量避免用非遞迴的寫法，進行程式的開發，同人對此實在是感到不能認同。 更何況透過思考，遞迴函式重覆計算的問題並不難解決。例如我們可以在遞迴函數中，將已經計算過的數列項次值儲存下來，當過程中發現某個項次已經計算過了，我們可直接取值而避免再計算一次，這樣就可以增進遞迴函式的執行效能。而運用程式設計者的智慧，遞迴耗費大量記憶空間的限制，也並非不能克服。 同人一位朋友就分享他在繪圖程式設計的經驗，他曾改進過填色的遞迴函式。原來一個點向四面擴散填滿顏色要用到大量的遞迴空間，但只要改變以線或區域為單位向四面擴散，就能大幅減少填色所需之遞迴空間，克服遞迴函式的先天限制。 所以問題並不是遞迴可不可以用，而是程式設計者懂不懂得用思考來創新設計技巧；不懂得在解決問題上尋求挑戰與突破，那才是讓程式設計能力停滯不前的最可怕迷思呀。]]></description>
			<content:encoded><![CDATA[<p>昨天同人在〈<a href="http://www.lifeparty.idv.tw/blog/archives/424">又見少了概括性論點</a>〉提到〈<a href="http://www.zdnet.com.tw/enterprise/column/jasonlee/0,2000090349,20134985,00.htm">必須面對的真相─五大程式設計迷思</a>〉在文章結構上的問題。其實那篇文章除了結構問題之外，同人也在該篇文章內容中，發現了一些值得探討的問題，因此想在這篇文章發表我的看法。</p>
<p>以該篇文章內容而言，同人認為值得探討的有兩個地方，一個是程式語言一直再改變的迷思、另一個則是作者建議讀者，儘量避免用遞迴的方式來寫作程式。第一個問題只是沒有交待清楚作者想要表達的概念，而第二個問題就是嚴重的偏見了，值得讓人進行思辨以建立正確的觀念。</p>
<p>為什麼程式語言一直再改變是個迷思？那篇文章就有讀者質疑這個迷思，他說：</p>
<blockquote><p>關於這部份，我覺得不如講＂只有XXX語言才能作某件事情＂<br />
我不覺得＂程式語言的改變＂是迷思</p></blockquote>
<p>不過，同人認為所謂「程式語言一直在改變」的迷思，作者的意思不單單只是說程式語言沒有一直在改變的意思，而是還有更深層的涵義沒有表達出來。事實上，程式語言一直再改變是個事實，我們不該說它們沒有一直改變。而是這些改變並不會讓我們先前對程式語言學習的投資白費。</p>
<p>同人認為，作者提到「電腦程式語言的演進，就像人類的語言演進一樣，有其一定的過程」是正確的。但對程式語言的學習而言，重點並不在於「程式語言不會一直在改變」，而是在於改變通常不會影響程式語言舊版本的語法，就算存在一些新舊版本不相容的影響，但只要掌握基本觀念，新的改變是不需要重新學起，而且我們經常還能在不同的程式語言之間，發現彼此相通的地方。</p>
<p>其實程式語言會不斷演化是必然的，「成熟」的程式語言反而是象徵衰退的開始。因為隨著時代的進步，程式設計的問題只會愈變愈複雜，造就了新的程式設計概念。如果程式語言太成熟，以致無法改變來加入新的程式設計概念，那麼它們將被時代所淘汰。所以，問題不在於程式語言是不是臻於成熟，因為改變是必然的，重點在於你投資在程式語言的學習並不會白費。</p>
<p>再來，作者提到「千萬不要認為電腦已經跑得很快了，就不注意演算方法的選擇。」同人認為是正確的。但他舉例指出遞迴函式效率較不佳、比非遞迴寫法的程式容易做重複計算，建議大家儘量避免用非遞迴的寫法來進行程式寫作的說法，同人就完全不能夠認同作者的看法。</p>
<p>沒有錯，遞迴函式的確比非遞迴的寫法會佔去更多的記憶體，但以空間換取時間的角度來看，說遞迴函式比非遞迴寫法更沒有效率，則是過份簡化的說法。即使以作者舉計算費式數列的遞迴函式的例子來看，遞迴函數可能有重複計算的問題，但到底費式數列的 n 值要多大，遞迴函數的執行速度才會發生顯著的影響，這卻是值得探討的問題。</p>
<p>如果在一般解決領域問題的條件限制上，都不見某種演算法的缺點，會產生顯著的影響，但卻能享受到該種演算法的優點，那麼我們是不是應該選用這樣的演算法來設計程式呢？答案想必是肯定的。這不是把問題推到硬體速度夠不夠快的問題，而是演算法的選擇應該透過客觀的分析與理性的思考，才能找到最適當的選擇。</p>
<p>這讓人也發現到程式設計常見的迷思，很容易誤導我們對程式開發的看法；太過相信經驗法則而缺乏對問題的深入思考，容易人云亦云地以為該用什麼演算法或避免什麼演算法，而那些都是程式設計師自以為是的偏見。</p>
<p>我想到哈米尼斯對同人分享<a href="http://www.lifeparty.idv.tw/blog/archives/192">永續物件設計</a>所提出的意見，他提到：</p>
<blockquote><p>就現實問題而言，一家公司裡，可能許多套系統都是由不同的廠商開發，若以這樣的角度來看，這一個架構似乎有點問題。</p>
<p>再來則是效能考量的問題，我認同這一個架構下，可以讓開發和維護變得簡潔，但若是遇到某些特定的系統，如下單系統，它強調的重點會在反應時間及大量資料處理，若是以此為考量，這樣的架構似乎又繞了太多層？</p></blockquote>
<p>哈米尼斯的意見正是同人在工作上常面臨的質疑，但實際上我所提出來的設計是經過驗證過的架構，運用服務導向的觀念、以及物件導向設計的設計手法，已成功整合銀行各種不同廠商開發的子系統。它實際應用在銀行的付款系統，所以對績效的要求是非常嚴苛的，然而實際上它的效能卻未見如哈米尼斯所顧慮到的問題。</p>
<p>受限於自己的經驗，很多程式設計師傾向於認定太多「系統間接層」不好，但那只是一己想法，而未經證實的猜測。如果真的擔心發生這樣的問題，應該採取行動進行驗證，而不是停留在想法的猜測，這才是程式設計者應有的積極態度。</p>
<p>事實上，間接層的設計只是把原來放在一起的程式碼區分好責任，去掉重覆性而已，我們對系統繞太多層的理解不一定是系統運作的真相。除非我們做了設計概念驗證，讓事實或數據說話，而不是經驗的推論，後者往往來自於個人徧見所致。</p>
<p>同人無意在這裡討論那一種程式開發典範比較好，因為那是不會有結果的爭論，只有在了解程式想要解決的問題之後，討論用什麼方式來開發程式才會有意義。</p>
<p>演算法的選擇也是同樣的道理，每一種演算法都有它的優缺點，我們應當針對問題的目標與限制來「思考」最適當的演算法，而非慣性地「記憶」那一種方法用在每一種情況都比較好、或比較不好。因此作者建議儘量避免用非遞迴的寫法，進行程式的開發，同人對此實在是感到不能認同。</p>
<p>更何況透過思考，遞迴函式重覆計算的問題並不難解決。例如我們可以在遞迴函數中，將已經計算過的數列項次值儲存下來，當過程中發現某個項次已經計算過了，我們可直接取值而避免再計算一次，這樣就可以增進遞迴函式的執行效能。而運用程式設計者的智慧，遞迴耗費大量記憶空間的限制，也並非不能克服。</p>
<p>同人一位朋友就分享他在繪圖程式設計的經驗，他曾改進過填色的遞迴函式。原來一個點向四面擴散填滿顏色要用到大量的遞迴空間，但只要改變以線或區域為單位向四面擴散，就能大幅減少填色所需之遞迴空間，克服遞迴函式的先天限制。</p>
<p>所以問題並不是遞迴可不可以用，而是程式設計者懂不懂得用思考來創新設計技巧；不懂得在解決問題上尋求挑戰與突破，那才是讓程式設計能力停滯不前的最可怕迷思呀。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.lifeparty.idv.tw/blog/archives/424/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>穩定的程式是偶然？</title>
		<link>http://www.lifeparty.idv.tw/blog/archives/421</link>
		<comments>http://www.lifeparty.idv.tw/blog/archives/421#comments</comments>
		<pubDate>Fri, 09 Jan 2009 06:54:09 +0000</pubDate>
		<dc:creator>jim yeh</dc:creator>
				<category><![CDATA[學習]]></category>
		<category><![CDATA[專案監控]]></category>
		<category><![CDATA[專案規劃]]></category>
		<category><![CDATA[專案風險]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[編程技巧]]></category>
		<category><![CDATA[職場]]></category>
		<category><![CDATA[設計原則]]></category>
		<category><![CDATA[開發流程]]></category>

		<guid isPermaLink="false">http://www.lifeparty.idv.tw/blog/archives/421</guid>
		<description><![CDATA[如果穩定的程式真的是偶然的，程式的穩定似乎只能依賴運氣而不是人為努力，事情真的是這樣嗎？其實這位噗友太過強調環境變化的隨機性，卻忽略了適應環境變化，程式開發必然會經歷複雜演化的過程。穩定的程式是演化而來的，雖然演化的過程是偶然、但其最後結果卻是必然。換句話說，穩定的程式是偶然下的必然。]]></description>
			<content:encoded><![CDATA[<p>最近同人看到有<a href="http://www.plurk.com/p/cayqt">一則噗浪</a>提到「一個穩定的程式並非必然，而是偶然」在此噗浪的回應中，發送這則噗浪的噗友表達他對程式穩定性的看法。</p>
<blockquote><p>先問自己一個問題，一個程式有幾個 Bugs？如果是不可數，那～～它的穩定是相對非絕對，所以穩定不是絕對的，也不是必然的。</p></blockquote>
<p>同人覺得這位噗友的觀點很有趣，於是加入這則噗浪的討論。我問如果最開始的程式都是穩定的，那麼是什麼原因讓後來的程式變得不穩定？對這個問題，一些噗友提出了他們的看法，其中有一位噗友回應「是我的機器弄好的程式，跑到別人機器就不穩定了」，發送此則噗浪的噗友認為還蠻接近實際的情形。</p>
<p>他提到 Bugs 在自己的機器沒產生，在別人那邊可能會產生，問題可能是因為自己的機器有 Bugs，而不是別人的機器，他說假設機器不會出問題是會有副作用的。</p>
<p>那麼為什麼不在應用系統要運作的目標環境下直接開發程式呢？因為，嚴格來說，開發者不可能有完全一致的環境，因此開發者只能假設環境是不會改變。但實際上，在不同的機器上、甚至在相同的機器上，不同的時間可能也會出現難以預料到的變化，結果使得程式變得不穩定。</p>
<p>這位噗友認為，當我們愈信任一個系統，其實可能是一個災難，因此程式的穩定非絕對而是偶然，它是由一連串的偶然所累積而成的。同人發現這個觀點讓人很難反駁，在我過去程式開發的經驗中，並不乏遇到原來運作正常的程式，在不同時空環境出現問題的現象。正如同這位噗友提到的，作業系統與程式語言它們本身也都是程式，很難確保它們不會出問題。</p>
<p>相信很多人都曾遇到過，程式發生失常通常只因為一個令人難以捉摸的小錯誤，因此似乎真的可以說「穩定的程式是偶然的」。不過，如果穩定的程式真的是偶然的，程式的穩定就只能依賴運氣而不是人為努力，事情真的是這樣嗎？其實這位噗友太過強調<strong>環境變化</strong>的隨機性，卻忽略了適應環境變化，程式開發必然會經歷<strong>複雜演化</strong>的過程。穩定的程式是演化而來的，雖然演化的過程是偶然、但其最後結果卻是必然。換句話說，穩定的程式是<strong>偶然下的必然</strong>。</p>
<p>程式的運作通常需要處在多變的環境中，使得開發者很難確知他的程式有多少 Bugs。因此在本質上，程式開發過程是一個偶然，由一連串的隨機事件所構成的。雖然開發者可以使用與將來應用系統所運作環境相同的硬體及作業系統，但實際上終究並非同一台機器。它們可能會因為使用者的差異，而安裝了無法與應用系統相容的軟體或設定了會使系統出現問題的系統組態。就算是兩台機器能夠完全一模一樣，時空環境的變化也可能造成系統運作出現問題的關鍵，即使兩者的差異相當微小。</p>
<p>以上的現象就是程式開發的「<a href="http://en.wikipedia.org/wiki/Butterfly_effect">蝴蝶效應</a>」，就像天氣很難預測準確的道理一樣。一隻蝴蝶在巴西輕拍翅膀，會使更多蝴蝶跟著一起輕拍翅膀，最後將有數千隻的蝴蝶都跟著那隻蝴蝶一同振翅，其所產生的巨風可以導致一個月後在美國德州發生一場龍捲風。這是「<a href="http://en.wikipedia.org/wiki/Chaos_theory">混沌</a>」理論所討論的現象，未來的狀態對<strong>初始條件敏感</strong>而造成無法預測。在混沌系統中，初始條件十分微小的變化，經過不斷放大，對其未來狀態會造成極其巨大的差別。<sup>[<a href="http://www.lifeparty.idv.tw/blog/archives/421#footnote_0_421" id="identifier_0_421" class="footnote-link footnote-identifier-link" title="維基百科編者 (2008). 混沌理論. Wikipedia, . Retrieved 03:15, 1月 9, 2009.">1</a>]</sup></p>
<p>除非所有 Bugs 都出現為止，否則程式開發者不會知道他的程式有多少 Bugs。因為他無法預測他的機器和實際運作的機器，有那些差異會造成程式運作出現功能失常。而且問題可能不是因為他的程式錯誤，有時候問題是出在作業系統或程式語言本身的 Bugs；它們都是程式，只要程式都有可能會有 Bugs。</p>
<p>現實真是令人沮喪呀，如果不確定因素使得環境變得如此難料，那要如何期待穩定的程式出現呢？我們不能期待環境不再變化，因為軟體開發的現實就是變化無常，唯一不變的真理就是變。我們也不能不去面對問題，進行規劃及管理，因為隨性地處理問題，只會讓問題變得更加混亂而讓情況失去控制。我們應該讓系統可以 <strong>適應環境的變化，發展出兼具穩定與彈性的程式</strong>，這樣我們就能在<a href="http://en.wikipedia.org/wiki/Edge_of_chaos">界於混亂與穩定的過渡地帶</a>中，逐步演進出複雜的行為來維持整體系統的動態平衡。</p>
<p>這就是<a href="http://en.wikipedia.org/wiki/Complexity_Theory">複雜理論</a>的<a href="http://en.wikipedia.org/wiki/Complex_adaptive_system">複雜適應性系統</a>的觀念，系統透過<a href="http://en.wikipedia.org/wiki/Self-organisation">自我組織</a>來穩定自己，不會因為無法控制混亂而崩解為混沌一片，同時也保有足夠變化的彈性來進化系統行為。就像同人在<a href="http://www.lifeparty.idv.tw/blog/archives/87">過去的文章</a>中，曾經提出複雜系統來比喻專案管理的隱喻一樣，複雜適應性系統的演化過程是不斷經歷觀察、假設、調整、再假設的重覆循環過程。</p>
<blockquote><p>整個專案環境所能供給的資源是有限的（成本限制），而演化競爭者會和我們爭奪這些有限資源（時間限制），所以對一個複雜系統而言，必須達成演化的使命而創造對整體系統的價值最大化（規模限制）。要做到這點，複雜系統必須能依現狀來演化系統，用最經濟的方式創造最大的價值（技術限制），演化則是先對環境的做出假設，然後依據適者生存不適者淘汰的原則來進行演化，過程中會不斷地修正對環境的假設並改變系統行為以求適應環境。</p></blockquote>
<p>因此，對於程式開發的專案當然也是相同的道理，演化的過程是無法預料的隨機行為，但演化的結果卻是必然的；發展出最適應環境及產生最大價值的穩定系統。假設的目的是為了做到將廣大的可能性限制在一小部分，這是同人在<a href="http://www.lifeparty.idv.tw/blog/archives/175">另一篇文章</a>引用 Peter Ho 所提到讓系統停留在混沌邊緣必須做的三件事之一。另外兩件事則是必須保持足夠的穩定，即使有輕微的改變，系統仍不會崩解成混沌一片、以及必須在靜止不動的死寂與過度活動的「混亂政體」間達到平衡。</p>
<p>這也就是同人提出先前穩定的程式，最後卻變得不穩定，到底問題是出在什麼地方？問題並不在我們對環境做出假設，而是無法意識到假設所帶來的風險，以讓開發程式停留在混沌邊緣以達成系統的動態穩定。因此，噗友提到假設會有副作用的問題不在假設，而在於開發者面對環境的反應的變化太慢，以致於造成局面的紊亂而使得問題無法收拾。</p>
<p>然而，如何及早反應環境的變化維持程式的穩定與彈性呢？同人推崇測試、重構與整合三項實務，誠如 Peter Ho 所說的「未經<strong>重構</strong>，系統將會沒有空間來容納新功能來適應新的變化驅力；未經<strong>測試</strong>，開發者不會知道系統邊緣在那裡；未經<strong>整合</strong>，生命火光將逐漸消散。」用紀律來維持系統彈性與穩定，雖然環境的變化是無可預料的，但我們仍將體會到穩定的程式是偶然下的必然！</p>
附註
&nbsp;<hr/><ol class="footnotes"><li id="footnote_0_421" class="footnote">維基百科編者 (2008). <a href="http://zh.wikipedia.org/w/index.php?title=混沌理论&amp;oldid=8808028">混沌理論</a>. Wikipedia, . Retrieved 03:15, 1月 9, 2009.</li></ol>]]></content:encoded>
			<wfw:commentRss>http://www.lifeparty.idv.tw/blog/archives/421/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>藏拙</title>
		<link>http://www.lifeparty.idv.tw/blog/archives/407</link>
		<comments>http://www.lifeparty.idv.tw/blog/archives/407#comments</comments>
		<pubDate>Fri, 05 Dec 2008 10:57:27 +0000</pubDate>
		<dc:creator>jim yeh</dc:creator>
				<category><![CDATA[利害關係人]]></category>
		<category><![CDATA[學習]]></category>
		<category><![CDATA[專案團隊]]></category>
		<category><![CDATA[溝通]]></category>
		<category><![CDATA[生活感觸]]></category>
		<category><![CDATA[編程技巧]]></category>
		<category><![CDATA[衝突]]></category>

		<guid isPermaLink="false">http://www.lifeparty.idv.tw/blog/archives/407</guid>
		<description><![CDATA[從事軟體開發的工作中，同人也常觀察到一些開發者不懂藏拙的智慧，意欲表現自己很有能力，但卻總是被人看到他們虛有其表的黔驢之技。我們當然很希望這樣的人，不要出現在工作經驗當中。但很不幸地，世事總是難以如我們的預期，如果不幸在工作碰到這樣的人，我們應該如何自處呢？]]></description>
			<content:encoded><![CDATA[<p>《<a href="http://zh.wikisource.org/w/index.php?title=%E9%BB%94%E4%B9%8B%E9%A9%A2&amp;variant=zh-hant">黔之驢</a>》的寓言故事告訴我們藏拙的智慧。如同故事中老虎先前不知驢子的虛有其表，以為牠的龐然大物而對產生敬畏之心。但當黔驢技窮的底細被老虎發現後，可憐的驢子便喪生於虎口之下，這都是因為那頭驢子不懂藏拙的智慧。</p>
<p>從事軟體開發的工作，同人常觀察到一些開發者不懂藏拙的智慧，意欲表現自己很有能力，但卻總是被人看到他們虛有其表的黔驢之技。通常這種人不懂得用虛心受教來充實能力，而只會把問題的責任推到他人身上。</p>
<p>我們當然很希望這樣的人，不要出現在工作經驗當中。但很不幸地，世事總是難以如我們的預期，如果不幸在工作碰到這樣的人，我們應該如何自處呢？</p>
<p>同人最近就碰到這樣的人，我所參與的專案在訊息交換時出了錯誤，後來有人發現當 <a href="http://en.wikipedia.org/wiki/Xml">XML</a> 訊息中沒有出現某些標籤時，就會產生問題。此時，負責開發接受端程式的開發者表示，XML 訊息中的所有的標籤都必須要存在，即使沒有用到也不能省略，要補一個空的標籤，這是 XML 的標準做法。</p>
<p>他的說法馬上得到其他人的質疑，明明 XML 訊息中不需要用到的標籤可以不用出現，這才是 XML 的標準做法，因此程式產生錯誤的問題，是出在他拆解訊息的程式寫得不夠強固，而不是別人沒有按照標準傳送訊息給他的程式。然而，他對大家的質疑，卻立即用下面這段話來提出反駁。</p>
<blockquote><p>從我這麼多年寫 XML 程式以來，XML 的標準規格就是照我說的；沒有用到的欄位要用空標籤來表示。</p></blockquote>
<p>雖然同人並不認同他的看法，但為了溝通彼此的想法，我還是提醒他這個時候強調自己多有經驗，其實無益於彼此溝通，只會流於各說各話；同時我也提到了 XML 訊息中，使用空標籤並不能代表用不到該標籤對應到的欄位。</p>
<p>同人以為有經驗的開發者應該都知道上面提到的觀念，空標籤代表應對的資料內容是空值，而省略標籤則代表不知該資料的確切內容，或是指該資料在此刻並「不適用」（N/A，Not available）；但沒想到這位堅持己見的開發者不能理性對話，竟然叫我不要說話。</p>
<p>他說他在和負責寫訊息傳送端程式的人在溝通，所以我不應該講話。可是我身為系統分析師，訊息界面規格是我制定的，為什麼我不可以表示意見！不過，既然他如此不可理喻，我也不想生氣浪費我的時間。</p>
<p>我向負責寫訊息傳送端程式的人表示：「不管你決定怎麼做，請堅持專業」、並告訴專案經理說：「如果訊息格式有這樣不合理的限制，只會出現彼此各自為政而整合困難的現象，也會讓問題愈弄愈複雜」。</p>
<p>後來當然是由專案經理出面，「教育」那位自以為是的開發者，要求他必須加強他的程式功能，而非要求別人來配合他的程式。不過，看到那位開發者，在他自以為很有技術權威的外表下，潛藏著其能力的虛有其表，又不願藏拙而想要表現自己什麼都懂，他應該不了解「人外有人，天外有天」的道理吧，這種人還是遠離他一點，就讓他自己去自食惡果吧。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.lifeparty.idv.tw/blog/archives/407/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
