[ Ruby on Rails ] 重構經驗談

會寫這篇文章的初衷是因為公司當初聘請我最重要的任務就是把網站從 Rails 2.3 升級到 Rails 3.2 並且把整個網站重構。公司網站的規模大概落在40~50個controller和model上下,說大不大說小不小。在整理的過程中,看到了各式各樣慘不忍睹甚至令人噁心的程式碼,加上完全沒有spec或document的狀況下,導致我必須不斷去揣摩、拆解當初原作者這段程式的想法,同時,還必須靠"想像"原本的Spec重新寫出好維護的程式碼。回憶起這段不堪回首的往事真的是非常辛苦,但在把後端整理到可以維護的狀況和把前端的工作切割拋出去給F2E以後,回頭想想收穫也不少。所以想藉由這次的經驗做點記錄與分享。

在文章正式進入主題之前我想要針對接下來的內容先預設立場:

1. 我是前後端完全分離的支持者。

術業有專攻,我認為前端和後端本來就是不同世界的東西,跨過平衡的界線只會讓問題萌芽。

2. 我只相信Better Practice,而非Best Practice!

事事無絕對,文章中的技巧要如何運用及拿捏是否適用需要由客官自行掌握。

– 本文開始 –

身為一個Rails developer,身兼前後端工程師是件很正常的事,在開發的過程中往往會因為方便、懶惰、趕時間、腦袋不清楚……等因素養成了不好的習慣,這些習慣在養成之前,一定都會有個"之後有空再整理"的自我辯解,等到真的有空的時候,通常為時已晚!因為要改的東西太多,想改也改不動!養成好的習慣對於建構一個好維護的網站有很大的幫助!至於好維護這件事我個人對他的定義是好上手、好新增、好修改、好刪除,至於要怎麼做到以上四點呢?

  • 增加程式可讀性
  • 檔案架構化
  • 降低前後端的相依性

增加程式可讀性

很多人(包括以前的我在內)都認為,良好的可讀性就是要讓程式越精簡越好、看起來越乾淨越好,通常的作法:

  • 盡可能刪除程式內的空白(斷行)。
  • 命名縮寫。
  • 用一些tricky的技巧。
  • 毫無限制的拆partial讓view變得非常簡短。

從開始寫Rails開始,陸續接觸了十幾個案子,從event site、中型網站、到較大型的網站,這些經驗告訴我,以上的方法反而都是造成可讀性低落的因子!

  • 適當的空白可以幫助閱讀。
  • 完整的命名可以增進功能上的理解。
  • tricky的技巧時常會讓人閱讀起來毫無頭緒。
  • 無限制的拆partial會導致為了改一個view,可能必須比對N個file才有辦法完成一件簡單的事。

在可讀性方面的建議就是盡可能把自己的team member當做白痴,把code寫得清清楚楚,寫完後再當做程式不是自己寫的詳讀一遍。若程式真的過於複雜,加上幾行註解也無妨。千萬不要把team member當天才,即使真的是,他們也會因此多花很多時間去trace code。

檔案架構化

雖然Rails已經提供了非常好的結構來因應Web Application的需求,但當檔案數目過多,還是需要針對各個部分做更進一步的結構化。Rails的Controller和View配上Router的Namespace、Model配上Scope,都可以輕易的把相關的檔案集中至各別的資料夾。

Controller和View配上Namspace:

app/controllers/group/members_controllers.rb
app/controllers/group/users_controllers.rb

app/views/group/members/
app/views/group/users/
# routes.rb

namespace do
 resources :members
 resources :users
end
# app/controllers/group/members_controllers.rb

class Group::MembersController < ActionController::Base
...
end
# app/controllers/group/users_controllers.rb

class Group::UsersController < ActionController::Base
...
end

Model搭配Scope:

app/models/group/member.rb
app/models/group/user.rb
# app/models/group/member.rb

class Group::Member < ActiveRecord::Base
...
end
# app/models/group/user.rb

class Group::User < ActiveRecord::Base
...
end

前端的部份,針對各別layout開各別的CSS和Javascript folder,並且把自己寫的檔案和plugin的檔案再次切成兩個資料夾。至於圖片,也是如法炮製。

舉個例子來說,假設我有兩個layout:

app/views/layout/application.html.erb
app/views/layout/admin.html.erb

css的部份把它切成:

app/assets/stylesheets/application.css
app/assets/stylesheets/application/
app/assets/stylesheets/application/plugin/
app/assets/stylesheets/admin.css
app/assets/stylesheets/admin/
app/assets/stylesheets/admin/plugin/

javascript的部份把它切成:

app/assets/javascripts/application.js
app/assets/javascripts/application/main.js
app/assets/javascripts/application/app/
app/assets/javascripts/application/plugin/
app/assets/javascripts/admin.js
app/assets/javascripts/admin/main.js
app/assets/javascripts/admin/app/
app/assets/javascripts/admin/plugin/

這邊必須提到的是,main.js主要用來集中管理所有js的啟動,至於實際作法在後面會詳細說明。

image的部份把它切成:

app/assets/images/application/
app/assets/images/admin/

在網站規模小的時候,這些舉動看似拖褲子放屁,但試想當有十幾個layout的情形會是如何?

相信一定有人會問,那重複的檔案怎麼辦?就如同我前面所提到,為了好刪除這個特性,所以每個layout的folder都各自擺一份。

降低前後端的相依性

這部份我覺得是Rails developer最常見的問題,就如同我前面提到的,身為一個Rails developer,身兼前後端工程師是件很正常的事,但Rails developer終究還是Rails developer,要把前端寫好還是需要花一定時間的磨練,在半之半解的狀況下很容易把前端拿到後端寫,RJS就是一個很悲慘的案例!網站規模到達一定程度,專業分工是必要的,Rails developer不需要強求HTML、CSS和Javascript強到什麼程度,但最重要的是要把前後端盡可能的切割乾淨,讓F2E接手的時後可以在明白檔案架構的狀況下,即使不會Rails也可以快速上手。

1. 避免使用inline CSS和inline Javascript、使用content_for或是直接把CSS和Javascript寫在檔案中

這點非常常見,最常見的原因就除了懶還是懶,排除懶之外的原因常常是為了使用Rails的變數,關於這種狀況,我會把變數塞在DOM裡面,用data-"attribute"去處理。

2. 使用SCSS

SCSS對於會寫CSS的人來說幾乎沒有門檻,不但可以加速工作效率,還可以更容易的讓CSS更加模組化。

3. 使用Compass

Compass不但讓SCSS的使用更方便,還有大量的module、helper可以使用,解決cross broswer的問題並且減少重複的程式碼,增加可讀性。

4. 把版面區塊化和功能化,減少CSS及Javascript的相依性

這和我第二點提到main.js的部分息息相關。

Web Application快速變更是很正常的一件事,但增加功能容易,刪除卻很難!程式難維護的一個很重要的原因常常是在於刪除功能時刪不完全或因為有相依性不敢貿然刪除,導致不需要的垃圾程式碼越長越多,干擾到正常程式碼的閱讀!為了解決這個困擾,我們可以使用版面區塊化和功能化的方式,在每個頁面、區塊或功能設定id,以id作為最上層的selector,依照功能或區塊切割檔案,在javascript和css file裡以id作為最上層的判斷依據。

舉個例來說:

假設我現在有個功能是tab,存在app/view/home/index.html.erb

CSS

app/assets/stylesheets/application.css
app/assets/stylesheets/application/tabs.css.scss

Javascript

app/assets/javascripts/application.js
app/assets/javascripts/application/main.js
app/assets/javascripts/application/app/tab.js

View

app/views/home/index.html.erb
# app/views/home/index.html.erb

<div id="tab">
...
</div>
# app/assets/stylesheets/application/tabs.css.scss

#tab {
  ...
}
# app/assets/stylesheets/application.css

/*
 *= require ./application/tabs
*/
# app/assets/javascripts/application/app/tab.js

var Tab = {
  initialize: function(){
    ...
  }
  ...
};
# app/assets/javascripts/application/main.js

if($("#tab").length > 0) {
  Tab.initialize();
}
# app/assets/javascripts/application.js

//= require jquery
//= require jquery_ujs
//= require ./application/app/tab.js

main.js這時候也產生了索引的作用,可以利用Object Name找到檔名,輕鬆新增修改刪除,同時也可以把"髒東西"完全切割,丟給F2E去整理。

6. 使用上Compass的variavle、mixin和helper取代原本傳統共用class。

舉個例來說,原本一個按鈕:

<a href="xxx" class="button red warnning delete">Delete</a>

簡簡單單一個按鈕卻必須套用這麼多class讓view看起來很雜亂,Javascript和CSS都會使用到id和class,如果不能簡單化對trace code也會有一定的阻礙!如果改成:

<a href="xxx" class="delete-button">Delete</a>

改用SCSS的variable、mixin和helper去處理delete-button class,不但可以減少id和class的使用,還可以集中管理網站共用style。

(缺點大概會是因為用了mixni取代原本通用的class,precompile以後會產生重複的style,造成檔案變大。)

7. Ajax的return data使用json替代html,並以前端template取代手動組成html

為了將工作切割更明確及增加View的維護性,我會建議ajax call的return data直接回傳json取代原本Rails developer常用的回傳html,讓F2E直接用javascript的template,一方面是減少action和view複雜度,另一方面在於可以大幅提昇傳輸速率。而template的部份我非常推薦handlebars_assets,此Gem將handlebars和precompile緊密結合,直接寫類似html的語法在precompile的時候產生template的js file,template也可以直接整理到asset的folder,不用寫在view裡面,也不需要從backend傳輸,非常方便。

重構經驗談 (二)

本篇發表於 ruby on rails, 網站開發。將永久鏈結加入書籤。

[ Ruby on Rails ] 重構經驗談 有 20 則回應

  1. easoncxz 說道:

    『寫完後再當做程式不是自己寫的詳讀一遍』
    ——高难度啊 呵呵

  2. virusswb 說道:

    可以显示简体中文吗

    • hellolucky 說道:

      我看簡體的東西都是用chrome的自動翻譯轉成繁體,不知道有沒有相對應的功能?我不會打簡體字,只能用繁體交流,sorry阿!這兩天比較忙,找時間找找看有沒有解決方案 :)

  3. golden05 說道:

    最好可以做一个重构gem可以自动把前面的类转移到子目录内,手动的调整不理想

  4. BenZhang 說道:

    你好, 谢谢你的分享. 我对于其中一条看得不太明白
    1. 避免使用inline CSS和inline Javascript、使用content_for或是直接把CSS和Javascript寫在檔案中
    我来自中国大陆, 我不太明白你说得把 javascript写在档案是什么意思? 你的意思是否把javascript代码写在类似app/view/home/index.html.erb的文件里?

  5. halida 說道:

    请问F2E是什么? 前端工程师?

  6. lulalala 說道:

    > 7. Ajax的return data使用json替代html
    這個的特例是,如果要回傳的 fragment 跟 server 要顯示的是一模一樣,那還是直接回傳 cache 好 render 成果的即可。JSON有時候中文字一多反而比較大,而且 client-side render 會多花時間。這樣也可以避免重複在 js 重刻一樣的 template。

    • hellolucky 說道:

      恩恩,還是要視狀況而定,如果只是簡單的application,也沒必要用template自找麻煩!

      不過JSON中文字多反而比較大是甚麼樣的情況?有點想像不到,不知可否舉個例解釋一下。

      至於client-side render會多花時間,我倒覺得大部分的情況下接收html的時間會比接收json加上client-side render的時間多很多!

  7. kkp 說道:

    你好,请问Controller和View配上Namspace, Model配上scope后,View中的form_for怎么写?
    form_for [:group, @member],会报出未定义group_group_members_path方法。

  8. ask main.js 說道:

    請教main.js是拿來放全站有使用到的視覺元件的檔案嗎?

    因為我看到文章 Tab.initialize();這行 有包 if
    if($("#tab").length > 0) {
    Tab.initialize();
    }
    故猜測您的main.js也會放別的UI,最後變成
    if($("#tab").length > 0) {
    Tab.initialize();
    }
    if( … ){
    myUI1.initialize();
    }
    if( … ){
    myUI2.initialize();
    }
    若是,想討論一下~
    這樣的javascript code對於中站、小站還算ok,對於大站或超大站,可能不ok?

    若不是,請忽略我的提問^^"

    Thanks!

    • hellolucky 說道:

      應該不能說視覺元件,而是用來宣告各個頁面的JS。當這個頁面有這個dom元素的話就宣告他!

      這種寫法的優點是整個網站的使用到的js一目了然,不怕刪錯檔案,缺點是每個頁面都需要跑很多if判斷式。

      我當初是評估這個網站並非js互動非常多的網站,功能不斷會變更,為了維護性(更容易判斷檔案或是功能是否有用),所以用這種方式。

      如果大網站的話,會建議用require.js去區分不同頁面的js也許會更好 :)

  9. 小黑 說道:

    請問版主,若有 module(html+javascript+css) ,該如何放置?

發表迴響

您的電子郵件位址並不會被公開。 必要欄位標記為 *

*

您可以使用這些 HTML 標籤與屬性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>