What have you found for these years?

2009-01-18

Don't Repeat Yourself

在 ruby 社群被朗朗上口的一句標語,縮寫為 DRY,
後來因為 rails 而變得眾所皆知。講白話一點,其實就只是一個概念:

「不要寫出任何重複的程式。」

基本上這也是我一直所在意的一件事。如果開始重複了,就應該抽出來。
如果我寫:

puts 'Hi!'
puts 'Hi!'
puts 'Hi!'

就應該改成:

3.times{ puts 'Hi!' }

如果又寫成:

3.times{ puts 'Hi!' }
3.times{ puts 'Hi!' }
3.times{ puts 'Hi!' }

就應該寫成:

def greet; 3.times{ puts 'Hi!' }; end
3.times{ greet }

這裡不是:

9.times{ puts 'Hi!' }

是因為平常我們很難找到這種簡化的方式,
所以只能再拉出一層,從更高的一個角度去說需要重複三次。
當然能做到 9.times 是最好,但真正的程式裡很難找到這種情況。

不過事實上,在強力遵從這種準則的同時,我也開始發現不對勁。
不對勁的地方在於,或許我縮寫完畢之後會變得相當簡潔,
但日後這邊頻頻需要修改的動作,使得當初的 abstraction
變得毫無意義。因為後來要打掉重練的機率很高。

可我也不覺得怎麼樣,只是覺得不對勁而已。

剛剛看到 Sam Smoot 的一篇回復,讓我頗有所感。
Sam Smoot 是 DataMapper 最早的開發者,
後來由於變得較為忙碌,改由 Dan Kubb 領導開發。
同時 DataObject 也是由 Sam Smoot 發起的,
我是不了解 java, 不過他說 data object 的地位就有如 jdbc,
是個一直都應該要有,卻沒有人做的東西。他個人對此感到很失望,
ruby 社群似乎不怎麼關心這樣東西的存在......

anyway, 在 datamapper 的 mailing list, 看過不少 Sam 的發言,
基本上他所說的很多東西我都相當贊同。也算是在網路上看到少數頗為佩服的
programmer 之一吧。

那一篇連結我放在最後面。

其實在用 datamapper 就一直讓我有這個疑問。
首先,ActiveRecord (後稱 AR) 不用定義 primary key (後稱 PK),
如果不表明,那就表示用 id 當 PK. 然 DataMapper (後稱 DM),
卻沒有這個假設,你一定要指出要用什麼名字當作 PK. 一般的作法,
就是 property :id, Serial. 幾乎在任何範例都可以看到這個模式。

還有如果我希望每個欄位都不能有 NULL 呢?

在 AR 沒這個困擾,因為 AR 根本沒這個功能。但是在 DM 裡,
他假設的東西非常少,很多常常要用的東西,你都必須親自一個個點出。
為此,我寫了幾個小 module 幫我做這些假設,一共是:

}.compact.each{ |model|
model.send :include, Abstract::NoNil
model.send :include, Abstract::DateTimeDefaultZero
model.send :include, Abstract::NumberDefaultZero
model.send :include, Abstract::StringDefaultBlank
model.send :include, Abstract::ForeignKeyIndex
}

先不要管上面 model.send :include 那些重複的部份。
剛開始我覺得這個動作是很理所當然的,但事實上,程式變得更大的時候,
這反而變成一種困擾:「錯誤的假設」。當然我可以再用例外的方式去處理,
像是 foreign key index 那個部份,我居然在裡面寫了:
if !property.serial? &&
property.name.to_s !~ /permission_id$/ &&
property.name.to_s !~ /license_id$/ &&
property.name.to_s !~ /icon_id$/ &&
property.name.to_s !~ /role_id$/

property.instance_variable_set '@index', true
end

這實在很蠢!我先假設所有的東西都要,然後舉出有些東西其實不要。
日後如果我又碰上更多假設錯誤怎麼辦?

於是就像 Sam 所說的,其實 DRY 應該是針對 domain knowledge,
而非 keystrokes! 十個一樣名字的東西,他們應該被簡化成一個嗎?

rails 有 polymorphic association, 讓你在一個 posts table 下
存入 photo posts, article posts, user posts, etc posts,
乍看之下是簡化,實際上呢?這些東西真的是一樣的,需要合併的?

在不同 context 下,本來同一個名字就會指著不同的東西。
如果他們只是名字一樣,實際上則不一樣,那麼這個 DRY 也只是少打點字,
實際上根本就沒有簡化任何東西。事實上反而應該是增加了複雜度。
因為你就必須去搞懂他簡化這些 keystrokes 的演算法。
這不是簡化,反而是複雜化,過度的 meta-programming, 過度的 magic.

rails 這個毛病非常嚴重,而 merb 也確實有!

回到他們的討論主題,以下程式碼:
with_default_options :auto_validation => false do

with_default_options :size => 50 do
property :firstname, String
property :lastname, String
end

with_default_options :precision => 10, :scale => 4 do
property :minimum, BigDecimal, :nullable => false
property :lastname, BigDecimal
end

end

真的有比這個好?
opts         = {           :auto_validation => false }
opts_string = opts.merge( :size => 50 )
opts_decimal = opts.merge( :precision => 10, :scale => 4 )

property :firstname, String, opts_string
property :lastname, String, opts_string

property :minimum, BigDecimal, opts_decimal.merge( :nullable => false )
property :maximum, BigDecimal, opts_decimal

前者看起來比較漂亮,但是你怎麼知道 with_default_options 是在做什麼?
你怎麼確保多呼叫一個 method, 執行速度不會變慢?你怎麼確保執行結果是正確的?
兩者的差異有大到需要多包一層嗎?更有甚者,如果是:
property :firstname, String, :auto_validation => false,
:size => 50

property :lastname, String, :auto_validation => false,
:size => 50

property :minimum, BigDecimal, :auto_validation => false,
:precision => 10,
:scale => 4,
:nullable => false

property :maximum, BigDecimal, :auto_validation => false,
:precision => 10,
:scale => 4

這樣呢?如果我 firstname 跟 lastname 長度忽然需要不一樣呢?
哪個比較好維護?哪個比較好懂?

如果他們只是看起來一樣,實際上不一樣,這時候的簡化,只是造成麻煩而已。
我想我之前應該犯了不少這樣的錯......唉

[quote="Sam Smoot"]
Sorry, pet-peeve of mine, but DRY has nothing to do with how much
you're typing, or whether you're copying-and-pasting and everything to
do with Domain knowledge. If two different String properties that just
happen to share :nullable => false can't reasonably be framed as
representing the same knowledge, then eliminating the duplication in
typing isn't an example of DRYness, it's an example of minimizing
keystrokes, which is often at odds with readability and counter to the
goal of DRY which is simply to excise fragility and lower the risk and
effort required to accommodate change.
[/quote]

ref:
Re: Ability to disable all auto_validations and |
or have dynamically evaluated messages

Adam French:
http://groups.google.com/group/datamapper/msg/f940810fcd983bee

Sam Smoot:
http://groups.google.com/group/datamapper/msg/e7d874adf381c2cc

http://en.wikipedia.org/wiki/Don't_repeat_yourself

0 retries:

Post a Comment

Note: Only a member of this blog may post a comment.



All texts are licensed under CC Attribution 3.0