My favorites | Sign in
Project Home Downloads Wiki Issues Source
Search
for
BmiRefactor  
重構程式
tw, refactor
Updated Nov 12, 2009 by gasolin
偉大的創意少之又少,多數時候只是一些小改進。小的改進也是好的。

什麼是重構

可以運作的程式跟可以維護的程式之間,還有一道難以言說的鴻溝。

一個程式設計之初,是用來解決特定問題。就像在前面章節的學習中,我們也已經寫好了一個可以運作的 BMI 程式。但是對程式設計來說,當我們寫越多程式,我們會希望可以從這些程式之中,找到一個更廣泛適用的法則,讓每個程式都清晰易讀,從而變得更好修改與維護。

讓程式清晰易讀有什麼好處呢?當一段程式被寫出來,之後我們所要做的事,就是修改它與維護它。一旦程式越長越複雜,混亂到無法維護的境界時,就只好砍掉重練。 所以若我們能透過某些方式,例如重新組織或部分改寫程式碼,好讓程式容易維護,那麼我們就可以為自己省下許多時間,以從容迎接新的挑戰。

我們回過頭來看看前面所寫的 Android 程式。Android 平台的開發者已經先依照 MVC 模式,為我們將顯示介面所用的 XML 描述檔、顯示資源所用的 XML 描述檔從程式碼中區隔開來。將與程式流程無關的部份分開來組織,讓程式流程更清楚,相對易於維護。

而在主要程式碼(Bmi.java)方面,雖然程式碼量很少,還算好讀,但整體上並不那麼令人滿意。例如,假使我們要在這段程式碼中再多加上按鍵、適用於多種螢幕顯示模式、或是再加入選單等等內容,很快地程式碼就開始變得複雜,變得不容易閱讀,也開始越來越不容易維護。

因此,在繼續新的主題之前,我們先來重構這個 BMI 應用程式。在重構的過程中,也許我們能學到的東西,比學任何新主題還重要哩。

MVC

我們打算重構 BMI 程式的部份 java 程式碼。既然我們已經照著 Android 平台的作法,套用 MVC 模式在我們的程式組織上,那麼,我們不妨也試著套用同樣的 MVC 模式在 Bmi.java 程式碼上。

如何套用 MVC 模式到Bmi.java 程式碼上哩?

原來的程式片段是這樣的

1  @Override
2  public void onCreate(Bundle savedInstanceState) {
3      super.onCreate(savedInstanceState);
4      setContentView(R.layout.main);
5           
6      //Listen for button clicks
7      Button button = (Button) findViewById(R.id.submit);
8      button.setOnClickListener(calcBMI);
9  }

上面的程式片段中,包含了所有 Android 程式共用的標準內容, 整個程式的大致架構在前面章節中已經講解過,現在我們從中取出我們感興趣的部分來討論:

Button button = (Button) findViewById(R.id.submit);
button.setOnClickListener(calcBMI);

在第7行我們看到一段程式碼來宣告按鈕物件,與針對該按鈕物件作動作的程式碼。 button.setOnClickListener 程式碼的意義是指定一個函式,來負責處理"按下"這個"按鈕"後的動作。

我們可以想像,在同一個畫面中,多加入一些按鈕與欄位後,"onCreate" 這段程式將變得壅腫,我們來試著先對此稍作修改:

首先,我們可以套用 MVC 模式,將宣告介面元件(按鈕、數字欄位)、指定負責函式等動作抽取出來,將 onCreate 函式改寫如下

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    findViews();
    setListeners(); 
}

接著我們將宣告介面元件的部份寫成一個獨立的「findViews」函式:

private Button calcbutton;
private EditText fieldheight;
private EditText fieldweight;

private void findViews()
{
    calcbutton = (Button) findViewById(R.id.submit);
    fieldheight = (EditText) findViewById(R.id.height);
    fieldweight = (EditText) findViewById(R.id.weight);
}

順便將原本很沒個性的按鈕識別參數「button」改名成「calcbutton」,以後在程式中一看到「calcbutton」,就知道是一個按下後將開始處理計算工作的按鈕。

同樣地,我們也將指定特定動作(按按鈕)的負責函式獨立出來:

//Listen for button clicks
private void setListeners() {
    calcbutton.setOnClickListener(calcBMI);
}

如此一來,我們就將程式邏輯與介面元件的宣告分離開來,達成我們重構的目的。

完整程式如下:

package com.demo.android.bmi;

import java.text.DecimalFormat;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class Bmi extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        findViews();
        setListeners();
    }

    private Button button_calc;
    private EditText field_height;
    private EditText field_weight;
    private TextView view_result;
    private TextView view_suggest;

    private void findViews()
    {
    	button_calc = (Button) findViewById(R.id.submit);
    	field_height = (EditText) findViewById(R.id.height);
    	field_weight = (EditText) findViewById(R.id.weight);
    	view_result = (TextView) findViewById(R.id.result);
    	view_suggest = (TextView) findViewById(R.id.suggest);
    }

    //Listen for button clicks
    private void setListeners() {
    	button_calc.setOnClickListener(calcBMI);
    }

    private Button.OnClickListener calcBMI = new Button.OnClickListener()
    {
        public void onClick(View v)
        {
            DecimalFormat nf = new DecimalFormat("0.0");
            double height = Double.parseDouble(field_height.getText().toString())/100;
            double weight = Double.parseDouble(field_weight.getText().toString());
            double BMI = weight / (height * height);
            
            //Present result 
            view_result.setText(getText(R.string.bmi_result) + nf.format(BMI));
 
            //Give health advice 
            if(BMI>25){
                view_suggest.setText(R.string.advice_heavy);
            }else if(BMI<20){
                view_suggest.setText(R.string.advice_light);
            }else{
                view_suggest.setText(R.string.advice_average);
            }
        }
    };
}

同樣是「calcBMI」 函式,在完整程式中,改將「calcBMI」 函式從原本的「OnClickListener」宣告成 「Button.OnClickListener」。這個改變有什麼差別呢?

閱讀原本的程式碼,在匯入(import)的部分可以看到,「OnClickListener」是來自於「android.view.View.OnClickListener」函式:

    import android.view.View.OnClickListener;

改成 「Button.OnClickListener」後,「Button.OnClickListener」就變成來自於「android.widget.Button」中的「OnClickListener」函式,在查閱程式時,整個「Button」與「OnClickListener」之間的關係變得更清晰。

另外,我們偷偷將「OnClickListener」中其他會存取到的介面元件識別參數,也補進 findViews 宣告中:

private void findViews()
{
    button_calc = (Button) findViewById(R.id.submit);
    field_height = (EditText) findViewById(R.id.height);
    field_weight = (EditText) findViewById(R.id.weight);
    view_result = (TextView) findViewById(R.id.result);
    view_suggest = (TextView) findViewById(R.id.suggest);
}

同時,我們也把識別參數的命名方法做了統一:按鈕的識別參數前加上 「button_」前綴,可輸入欄位的識別參數前加上 「field_」前綴,用作顯示的識別參數前則加上「view_」前綴。將變數名稱的命名方法統一有什麼好處呢? 好處在於以後不管是在命名新變數,或是閱讀程式碼時,都能以更快速度命名或理解變數的意義,讓程式變得更好讀。

我們也把原本在程式中直接寫進的字串

TextView result = (TextView) findViewById(R.id.result);
result.setText("Your BMI is "+nf.format(BMI));

改寫成

//Present result
view_result.setText(getText(R.string.bmi_result) + nf.format(BMI));

並將「TextView view_result」宣告改放到 findViews 中一次處理好。

現在,整個程式流程是不是清爽了許多呢?

< 完成 BMI 程式 | 回目錄 | 加入對話框 >


對於本章,您還期望知道什麼樣的內容呢?請在下方提出建議!

Comment by diordna....@gmail.com, Aug 3, 2008

受益匪浅!

期待继续拜读续作,关注中...

Comment by tomch...@gmail.com, Aug 5, 2008

多謝你花時間寫這本書~讓我學到很多呢

希望能就繼續看到你的續作摟

Comment by yeema...@yahoo.com.hk, Sep 4, 2008

十分十分尊敬大大您這偉大無私的貢獻... 請繼續

Comment by iamak...@gmail.com, Nov 24, 2008

獲益良多

Comment by project member gasolin, Dec 31, 2008
Comment by project member gasolin, Dec 31, 2008

Code Conventions for the Java Programming Language http://java.sun.com/docs/codeconv/html/CodeConvTOC.doc.html

Comment by b9507...@gmail.com, Jan 18, 2009

你的介紹非常詳盡,深入淺出。對我這個完全不懂JAVA的人,受益良多。

Comment by lion0...@gmail.com, Feb 18, 2009

看了您的文章,获益良多! 关于重构,我发表一下自己的方法:) 先是 public class Bmi extends Activity implements OnClickListener? 然后 public void setListeners()

{
button_calc.setOnClickListener(this);
}
最后 public void onClick(View v)
{
int viewId = v.getId(); switch(viewId) {
case R.id.submit:
DecimalFormat? nf = new DecimalFormat?("0.00"); double height = Double.parseDouble(field_height.getText().toString())/100; double weight = Double.parseDouble(field_weight.getText().toString()); double bmi = weight/(height*height);
view_result.setText(getText(R.string.bmi_result)+nf.format(bmi));
if(bmi>25)
view_suggest.setText(R.string.advice_heavy);
else if(bmi<20)
view_suggest.setText(R.string.advice_light);
else
view_suggest.setText(R.string.advice_average);
break;
}
}

这样子可以集中根据传入的onClick方法中不同的View参数来调用相应的处理方法:) 冒昧修改,如有不合适,可以删除:)

Comment by project member gasolin, Feb 18, 2009

lion0...., 這方法也很好呀,只要把握住了重構的目的即可。寫書求的是大家都看的懂,跟自己寫 Code 還是會多少有差異的。

Comment by Brooke...@gmail.com, Feb 24, 2009

讲解的浅显易懂,不仅解释了Android应用开发的基本方法, 更是对设计模式进行了探讨,非常感谢!

Comment by littleti...@163.com, Apr 7, 2009

非常感谢你的讲解,文字浅显易懂,让我受益匪浅!!

Comment by big...@gmail.com, Apr 28, 2009

請問view_result.setText(getText(R.string.bmi_result) + nf.format(BMI));這段裡面

getText是哪來的? 是view_result的function?還是別的object?

謝謝

Comment by project member gasolin, Apr 28, 2009

bigDav:

把滑鼠擺到 getString 上面,eclipse 就會告訴你這個 getString 是來自

android.content.Context.getString

作用是讀取資源中的特定字串內容。

Comment by big...@gmail.com, Apr 28, 2009

原來如此 謝謝您的解釋以及分享:)

Comment by sky.andr...@gmail.com, May 3, 2009

非常感谢你的讲解!

Comment by col...@gmail.com, Jul 11, 2009

找到你是我的福气,谢谢

Comment by is0...@gmail.com, Aug 2, 2009

謝謝你的教學!

Comment by KennyWu2...@gmail.com, Sep 10, 2009

抱歉,因為我看書頗慢,加上如果每找到一個疑問就要寫一次問卷的話頗怪,所以選擇在線上版這裡回應。文本書在本章節最末有新增一段解釋getText的段落(11-10頁),裡面有一小段提到〝只要把滑鼠擺到「getText」這個函式上面,Eclipse開發環境就會告訴你「getText」這個函式來自於「android.content.Context.getString」〞,請問在最末的getString這裡是否有誤植?因為我在Eclipse上看到的它是顯示「android.content.Context.getText」。

Comment by project member gasolin, Sep 10, 2009

Kenny, 沒問題, 在哪反應都可以. 當然以您實際看到的為準, 謝謝您的指正

Comment by ysl4s...@gmail.com, Oct 20, 2009

great thanks!

Comment by kuansl...@gmail.com, Oct 26, 2009

您好,我是个JAVA初学者,有一个问题就是: “同樣是「calcBMI」 函式,在完整程式中,改將「calcBMI」 函式從原本的「OnClickListener?」宣告成 「Button.OnClickListener?」。這個改變有什麼差別呢?

閱讀原本的程式碼,在匯入(import)的部分可以看到,「OnClickListener?」是來自於「android.view.View.OnClickListener?」函式:

import android.view.View.OnClickListener?;

改成「Button.OnClickListener?」後,「Button.OnClickListener?」就變成來自於「android.widget.Button」中的「OnClickListener?」函式,在查閱程式時,整個「Button」與「OnClickListener?」之間的關係變得更清晰。” 这段程序在eclipse中运行,当将鼠标放置于Button.OnClickListener?中的OnClickListener?上时还是提示来源于android.view.View.OnClickListener?,为什么不是想文章所说的来自android.widget.Button.OnClickListener?呢?

Comment by project member gasolin, Oct 26, 2009

因為 Button.OnClickListener? 是直接繼承自 android.view.View.OnClickListener? 的函式. 剛開始學的話沒有必要在這種細節裡打轉, 先繼續學下去, 改天再看就能領會.

Comment by clark751...@gmail.com, Nov 19, 2009

Thanks a lot

Comment by Crazy...@gmail.com, Dec 9, 2009

so good .

Comment by mars...@gmail.com, Feb 23, 2010
lion0neo 的方法是不錯,但是如果在同一個view時,要實做不同的listener就會發生問題囉(比方說多個button之類)~ ^^
Comment by project member gasolin, Feb 23, 2010

回應樓上:lion0neo 講到的方法是成立的, 多個 button 可以用 view id 來區別, 例如 case R.id.submit/case R.id.xxx

Comment by mars...@gmail.com, Feb 25, 2010

回應作者:恩,是我回得太急了,並不是不能作,而是會影響到「重構」這件事情,若以C的角度來看,是沒什麼問題的,但以物件導向的方式來看,其實是作者您的寫法比較容易維護、管理喔。(雖然這不是什麼大問題就是了)

Comment by gi9...@gmail.com, Aug 8, 2010

非常感謝您的文章,對我幫助很大;不過以我只讀過1/4本"JAVA由初學邁向認證" 跟1/5本"Android開發入門與實戰"的基礎上來吸收您的文章,說真的還是讀的 有點盲,當然英文單字每次都要翻譯也是困難度之一;最後我還是有些疑惑麻煩您 指點: 問1.findViews(); 我的理解能力是findView="發現視圖";但後面那對"()"用處是啥? 感謝您的指點

Comment by project member gasolin, Aug 8, 2010

您的問題很明顯是對程式語言還不熟悉, 建議先針對程式語言學習一下, 再來進入手持設備開發....

Comment by elson...@gmail.com, Aug 17, 2010

确实是非常好的android学习资料!就算不会java也很容易看懂~THX!

Comment by rmn...@gmail.com, Sep 24, 2010

一个问题:在运行“double height = Double.parseDouble(field_height.getText().toString())/100;”时, field_height处会不会报NullPointerException?

应该不会, 编译时不会有NullPointerException?的事, 而在实际运用时,当执行上面代码时,findViews肯定已经执行过了。

虽然知道自己的提问自己已经解决了,还是写在这里记录下吧。

Comment by dtnj...@gmail.com, Jan 15, 2011

感謝大大的分享,讓我搞清楚了整個Android程式運行架構。

Comment by Rolado.S...@gmail.com, Feb 15, 2011

請問一下 Bmi.java的內容,完全照大大所提供的複製貼上 可是跑模擬時會跑出以下內容 Sorry! The application BMI(processcom.demo.android.bmi)has stopped unexpectedly.Please try again. 這時候我該怎麼解決呢?

Comment by jeiw...@gmail.com, Mar 3, 2011

不好意思,新手想問一下蠢問題..這一章說的: "改成 「Button.OnClickListener?」後,「Button.OnClickListener?」就變成來自於「android.widget.Button」中的「OnClickListener?」函式", 我查Android developer 網頁裡面的 android.widget.Button下面沒有"OnClickListener?"(在android.view.View下面)... 是不是因為Botton繼承自TextView,TextView繼承自View的關係?

Comment by jeiw...@gmail.com, Mar 3, 2011

不好意思,我看到已有人問了..

我是想儘量自己從線上確認這些class跟method..新手對於這些東西感覺很混亂..(一條一條的程式) 也許可以用圖形把物件之間的關係畫出來..會更清晰..

Comment by David870...@gmail.com, Jul 6, 2011

不好意思,我記得android在1.6之後,已經可以不用設onclicklistener了。在按鈕屬性的OnClick?修改值後,在程式中實作同名函數即可。應該可以修正一下。

Comment by project member gasolin, Jul 6, 2011

最近正好碰到這個問題. 我發現不用onclicklistener改設onclick按鈕屬性的話, 在release時用proguard做混淆後, 當按下按鈕時就會出錯. 應該是在progurad處理時對應的名稱被改掉了. 所以我還是建議使用onclicklistener

Comment by songfac...@gmail.com, Aug 19, 2011

非常不错,有幸研读!

Comment by ericyang...@gmail.com, Sep 6, 2011

thanks to your simple and inducive introduction

Comment by Fout...@gmail.com, Oct 12, 2011

您好, 我是個android初學者, 最近在寫的東西就有一堆介面元件, 每次改程式碼時光找那些就找到快暈了 看您這樣配置感覺好簡明易懂, 只是我很好奇一點就是, 元件都宣告成全域變數, 這樣沒關係嗎? 就會不會很占記憶體? 還是其實手機很厲害無所謂呢?

Comment by project member gasolin, Oct 12, 2011

Fout..., 我也不知你問題的答案, 但Android會自動管理記憶體, 目前一般使用狀況下這樣寫沒什麼問題

Comment by project member gasolin, Oct 12, 2011

Fout..., 我也不知你問題的答案, 但Android會自動管理記憶體, 目前一般使用狀況下這樣寫沒什麼問題

Comment by Fout...@gmail.com, Oct 12, 2011

原來如此, 謝謝您回答我, 我還有一個問題(對不起我很煩>"<), 就是觸發事件函式的部分 , private OnClickListener? calcBMI = new OnClickListener?() ... 我看到好多程式都是長這樣, 這和直接讓程式實作OnClickListener?有什麼差別嗎? 如果有很多顆按鈕, 每顆按鈕要執行的動作都不一樣時, 那是就寫很多款觸發事件函式分別給每顆按鈕比較好, 還是寫一款觸發事件函式裡面再判斷是哪顆按鈕比較好, 還是讓程式實作OnClickListener?再慢慢判斷是哪顆按鈕比較好呢?

Comment by project member gasolin, Oct 12, 2011

Fout..., 兩種都可以, 看個人偏好

Comment by wch3...@gmail.com, Apr 25, 2012

請問view_suggest.setText(R.string.advice_heavy);為什麼不用加入getText()? 而view_result.setText(getText(R.string.bmi_result) + nf.format(BMI));就有加入 getText(), 一樣都是TextView,有什麼差別呢?

Comment by project member gasolin, May 9, 2012

可以看API有沒有直接支援從R.string取出字串,沒有的話只好用getText取出。這是Android API不太一致的地方


Sign in to add a comment
Powered by Google Project Hosting