2011年11月30日 星期三

阿班私房菜-重構(Refactoring)的小技巧(1)


最近在看一本書"The Pragmatic Programmer",裡有不少與重構相關的範例,值得參考,有與趣人可以買來看看。因此書,筆者心血來潮,分享一下自己的開發經驗。


在專案開發程式的過程,常會因時間壓力或市場考量,不得不加入很多非計畫中的程式碼。
開發時間越久,就漸漸難以維護,增加新功能更為秏時;當人力不足以達成出貨時程,便開始增加人力,最後日積月累,專案程式變成一個大怪物;直至某一日砍掉重練。


而筆者近年來,使用重構方法,使得自己不用一直為解專案bug而加班。

重構(Refactoring) 英文是一個現在進行式,表示這個動作需要一直不斷發生。


其中,"重構"的最重要的基石就是自動化測試程式,如每當重構程式碼中重覆的部分時,執行一下自動化測試程式,便可很快的確認程式修改後的正確性。

接下來我們來看一些筆者常用的小技巧,以下範例使用Python語言。

def show_animal_info0(animal):
    if animal == 'dog':
        print("The dog has four legs")
    elif animal == 'chick':
        print("The chick has two legs")
    else:
        print("There is an unknown animal")
show_animal_info0("dog")
show_animal_info0("chick")
show_animal_info0("cat")


重構if-else

通常if-else會越加越長,慢慢變得難以除錯。
animal_info_dict = {
        "dog":   "four legs",
        "chick": "two legs"
        }
def show_animal_info1(animal):
    try:
        info = animal_info_dict[animal]
        print("The %s has %s." % (animal, info))
    except:
        print("There is unknown animal")
用dict結構表格取代if-else, 也可用來取代switch


重構重覆呼叫函式

這種函式大部分由copy and paste而來。
show_animal_info0("dog")
show_animal_info0("chick")
show_animal_info0("cat")
def main():
    animals = ["dog", "chick", "cat"]
    for name in animals:
        show_animal_info1(name)
if __name__== "__main__":
    main()

重構屬性
紅色legs部分也是重覆程式碼的狀況之一
animal_info_dict = {
        "dog":   "four legs",
        "chick": "two legs"
        }
def show_animal_info1(animal):
    try:
        info = animal_info_dict[animal]
        print("The %s has %s." % (animal, info))
    except:
        print("There is unknown animal")
animal_info_dict = {
        "dog":   {"leg": "four"},
        "chick": {"leg": "two"},
        "cat":   {"leg": "four"}
        }
def show_animal_info1(animal):
    try:
        info = animal_info_dict[animal]
        legs = info['leg']
        print("The %s has %s legs." % (animal, legs))
    except:
        print("There is unknown animal.")


自動化測試程式

要進行自動化測試,以本範例為例:

1. 將邏輯與資料顯示動作分離

def show_animal_info1(animal):
    try:
        info = animal_info_dict[animal]
        legs = info['leg']
        print("The %s has %s legs." % (animal, legs))
    except:
        print("There is unknown animal.")
def get_animal_info1(animal):
    try:
        info = animal_info_dict[animal]
        legs = info['leg']
        msg = "The %s has %s legs." % (animal, legs)
    except:
        msg = "There is unknown animal."
    return msg

2. 比對預期函式回傳值結果

使用pyunit 來實作自動測試
import unittest
from code_for_automation import *
class DefaultTestCase(unittest.TestCase):
    def testTestDog(self):
        sample="The dog has four legs."
        msg = get_animal_info1('dog')
        assert msg == sample , 'test dog message'
    def testTestChick(self):
        sample="The chick has two legs."
        msg = get_animal_info1('chick')
        assert msg == sample , 'test chick message'
    def testTestCat(self):
        sample="The cat has four legs."
        msg = get_animal_info1('cat')
        assert msg == sample , 'test cat message'
if __name__ == '__main__':
    myTestSuite = unittest.makeSuite(DefaultTestCase,'test')
    runner = unittest.TextTestRunner()
    runner.run(myTestSuite)

3. 統計測試結果
$ python 05_automation.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
以上範例程式均可由tip1下載

結論

當有了自動化測試程式之後,可提高自行重構的信心,又不用花費手動一個個輸入測試的時間。
本文雖以python為例,但觀念是相通的,其他語言也有其自動測試函式庫可供使用,歡迎不吝指教。


django i18n 實戰 (二)


續上篇django i18n 實戰,筆者作了一些小實驗,以方便中文翻譯的進行。

整段文章中文化


在前篇提到的使用django.po 來編修翻譯的方法,在此處便不太適用。
因為這類文字時常修修補補,非常不易維護。
目前使用的解決方法,使用不同locale目錄來放置不同語言版本template。
en test.html
test2.html
zh-tw test.html
test2.html

取得目錄及template名稱
from django.utils import translation
def getLocaleResourcePathName(templateName):
    cur_language = None
    try:
        cur_language = translation.get_language()
    except:
        logger.exception('getLocaleResourcePath')
    if cur_language == 'zh-tw':
        return "%s/%s" % (cur_language, templateName)
    return “en/%s”  % templateName
其中translate.get_language() ,傳會目前要求取用此頁瀏灠器所使用的語系。


在javascript中需翻譯的文字

解法1: 為將瀏灠器目前所使用的語系,存入cookie中,以便javascript實作中文化。
def setLangCookie(request):
    params = {}
    c = Context(params)
    t = loader.get_template(pageForJS)
    response = HttpResponse(t.render( c ))
    try:
        cur_language = translation.get_language()
        response.set_cookie('lang', cur_language)
    except:
        logger.exception('set_cookie')
    return response

解法2: 同上一解法為將所需要翻譯文字集中至同一檔案,依瀏灠器目前所使用的語系,代換為i18n.js所在路徑。

簡易bash腳本檔案i18n.sh


續前篇,將相關命令包裝為腳本檔以方便使用。

如: sh  i18n.sh  u
 (即更新django.po檔案,附加新增文字)

#!/bin/sh


ACTION="$1"
TARGET="$2"

usage() {
        echo "syntax: |update|compile>"
}

case $ACTION in
initial)
        django-admin.py makemessages -l zh_TW "$TARGET"
        ret=$?
;;
u*|update)
        django-admin.py makemessages -a
        ret=$?
;;
c*|compile)
        django-admin.py compilemessages
        ret=$?
;;
*)
        usage
        ret=1
;;
esac
exit $ret

備註:其中django-admin.py,如使用環境在Mac OS X 使用brew install gettext卻找不到xgettext 和 msgfmt相關檔案時,請使用下列命令產生正確連結:
 brew link gettext


2011年11月29日 星期二

django i18n 實戰

image

最近在為的上線前專案做中文化的處理,在此留下一心得記錄。
就django 中文化相關工具而言,主要是將gettext中的兩個命令:
  • xgettext包裝成django-admin.py makemessages, 用以將程式及腳本檔中所定義的Key字串取出放入至django.po
  • msgfmt包裝成django-admin.py compilemessages, 用來把django.po 轉為二進位的django.mo檔案。
Key 字串
以底線(_)為常見的gettext key字串
在下列命令為常使用的兩種:
from django.utils.translation import gettext_lazy as _
筆者使用下列語法:
from django.utils.translation import ugettext as _
為什麼筆者不使用 gettext_lazy呢?
django 在更新po檔案時,會將相近字詞直接複製到附加的詞組中,時常牛頭不對馬嘴,不如手動編修。

demo 專案為例
請進入與settings.py同層的專案目錄
$ django-admin.py makemessages –l zh_TW
processing language zh_TW
正確產生自動下列檔案django.po
locale/zh_TW/LC_MESSAGES/django.po
如出現下列錯誤:
$ django-admin.py makemessages -l zh_TW
Error: This script should be run from the Django SVN tree or your project or app tree. If you did indeed run it from the SVN checkout or your project or application, maybe you are just missing the conf/locale (in the django tree) or locale (for project and application) directory? It is not created automatically, you have to create it by hand if you want to enable i18n for your project or application.
請建立locale目錄,再試一次。

更新po檔案
每當新增一些個需要中文化字串,並不需要將它們動手加入django.po
可使用下列指令,直接將新字串附加至原有的django.po:
$ django-admin.py makemessages –a
編譯mo
$ django-admin.py compilemessages
註:在測試時,如有重新mo檔案,請重新執行django。

附錄
  • 在使用 blocktrans 及 endblocktrans時,因為採用python 內建dict 變數代換功能,所以只能使用-層。範例如下:
{% blocktrans %} {{user.username}} 進行統
統操作中,… {% endblocktrans %}
其中 {{user.username}}將無法正確代換,需改為 {{username}} 一層dict 結構。對照python 原字串展開範例:
userinfo={'username': _('Bill')}
"%(user.username)s 進行統
統操作中,…" % userinfo


結論


gettext 廣泛使用在開源軟體之中,而django-admin加以包裝,讓它在使用上更加方便。
若對如何包裝gettext有興趣,可參閱python腳本檔如下:
/usr/lib/python2.7/site-packages/django/core/management/commands/makemessages.py
/usr/lib/python2.7/site-packages/django/core/management/commands/compilemessages.py

2011年11月3日 星期四

Vagrant+virtualbox (2): 實戰封裝自制package.box

image

續上篇執行lucid32.box 使用 jRuby + vagrant在Windows XP
Vagrantfile 範例
Vagrant::Config.run do |config|
  config.vm.box = "gold"
  # config.vm.forward_port("web", 80, 8080)
end


添加所需應用程式及設定,如安裝django套件:
sudo easy_install pip
sudo pip install django
使用此檔案執行 vagrant up後,使用 putty (參考執行lucid32.box 使用 jRuby + vagrant在Windows XP


若要附加自己的說明檔案,請使用—include
vagrant package --vagrantfile .vagrant --include README.txt


若要附加自己的說明檔案,請使用--
vagrant package --vagrantfile .vagrant --


執行結果
C:\Documents and Settings\vagrant\SRC\vagrant\gold>vagrant package --vagrantfile .vagrant --
[default] Attempting graceful shutdown of linux...
[default] Clearing any previously set forwarded ports...
[default] Cleaning previously set shared folders...
[default] Creating temporary directory for export...
[default] Exporting VM...
Progress: 0%Progress: 1%Progress: 4%Progress: 9%Progress: 10%Progress: 14%Progre
ss: 19%Progress: 24%Progress: 28%Progress: 34%Progress: 39%Progress: 42%Progress
: 44%Progress: 49%Progress: 54%Progress: 69%
[default] Compressing package to: C:/Documents and Settings/vagrant/SRC/vagrant/gold/package.box[default] Packaging additional file: .vagrant


加入本機檔案package.box 為mygold box
vagrant box add mygold package.box


執行結果
C:\Documents and Settings\vagrant\SRC\vagrant\gold>vagrant box add mygold package.box[vagrant] Downloading with Vagrant::Downloaders::File...
[vagrant] Copying box to temporary location...
[vagrant] Extracting box...
[vagrant] Verifying box...
[vagrant] Cleaning up downloaded box...

在新增之後,看看目前有的boxes:
C:\Documents and Settings\vagrant\SRC\vagrant\mygold>vagrant box list
gold
lucid32
mygold


修改新的Vagrantfile 來使用 mygold
Vagrant::Config.run do |config|
  config.vm.box = "mygold"
  # config.vm.forward_port("web", 80, 8080)
end

執行vagrant up 啟動
C:\Documents and Settings\vagrant\SRC\vagrant\mygold>vagrant up
There is a syntax error in the following Vagrantfile. The syntax error
message is reproduced below for convenience:
C:/Documents and Settings/vagrant/.vagrant.d/boxes/mygold/include/_Vagrantfile:1: syntax error, unexpected ':'
{"active":{"default":"f5a2c996-a70b-4c2c-8928-a85127a26863"}}

發生錯誤,移除include/_Vagrantfile ,再執行一次,啟動成功。
C:\Documents and Settings\vagrant\SRC\vagrant\mygold>vagrant up
[default] Importing base box 'mygold'...
[default] The guest additions on this VM do not match the install version of
VirtualBox! This may cause things such as forwarded ports, shared
folders, and more to not work properly. If any of those things fail on
this machine, please update the guest additions and repackage the
box.
Guest Additions Version: 4.1.2
VirtualBox Version: 4.1.4
[default] Matching MAC address for NAT networking...
[default] Clearing any previously set forwarded ports...
[default] Forwarding ports...
[default] -- ssh: 22 => 2222 (adapter 1)
[default] Creating shared folders metadata...
[default] Running any VM customizations...
[default] Booting VM...
[default] Waiting for VM to boot. This can take a few minutes.
[default] VM booted and ready for use!
[default] Mounting shared folders...
[default] -- v-root: /vagrant
C:\Documents and Settings\vagrant\SRC\vagrant\mygold>vagrant ssh
`vagrant ssh` isn't available on the Windows platform. The
vagrant.ppk file for use with Putty is available at:
  C:/jruby-1.6.5/lib/ruby/gems/1.8/gems/vagrant-0.8.7/keys/vagrant.ppk
To use this create a new Putty session for `vagrant@localhost`
on port `2222`, in the Connection>SSH>Auth
configuration section navigate to the vagrant.ppk file,
select it, save the session for later use, and connect.
For a more detailed guide please consult:
http://vagrantup.com/docs/getting-started/setup/windows.html
C:\Documents and Settings\vagrant\SRC\vagrant\mygold>

使用 putty連入,檢查之前在gold box所產生的測試用檔案[my.box],OK。
image


停止啟動mygold box

vagrant halt


刪除啟動後的mygold box
C:\Documents and Settings\vagrant\SRC\vagrant\mygold>vagrant destroy
[default] Forcing shutdown of VM...
[default] Destroying VM and associated drives...


結論
利用封裝已安裝及設定好的軟體的Box,可大大減少啟動後所需的安裝時間。
且可使用不同版本的gold box 來進行軟體自動化測試,並方便管理。


附註
多人共用mygold.box
可將編寫腳本檔案 mygold_update.bat 中加入如下設定:
@echo off
REM 刪除已存在的mygold_xxxx, 視情況決定是否執行destory, 或由使用者自行決定
vagrant destroy
REM 移除舊的mygold
vagrant box remove mygold
REM 加入新的mygold
vagrant box add mygold http://yourwebserver:port/mygold.box
註:請記得將所產生的package.box檔案更改並放到自行架設的web server 上。

可與ssh 搭配使用
vagrant ssh 等於
ssh -i <vagrant private key> vagrant@localhost –p <vagrant port>
(ex: ssh -i /cygdrive/c/jruby-1.6.5/lib/ruby/gems/1.8/gems/vagrant-0.8.7/keys/vagrant vagrant@localhost -p 2222)

下列為使用cygwin + ssh + vagrant key
$ ssh -i /cygdrive/c/jruby-1.6.5/lib/ruby/gems/1.8/gems/vagrant-0.8.7/keys/vagrant vagrant@localhost -p 2222
Could not create directory '/home/vagrant/.ssh'.
The authenticity of host '[localhost]:2222 ([127.0.0.1]:2222)' can't be established.
ECDSA key fingerprint is 36:28:64:97:6c:a4:dd:2a:4e:1a:0e:db:3a:db:d4:66.
Are you sure you want to continue connecting (yes/no)? yes
Failed to add the host to the list of known hosts (/home/vagrant/.ssh/known_hosts).
Welcome to Ubuntu 11.04 (GNU/Linux 2.6.38-8-server x86_64)
* Documentation:  http://www.ubuntu.com/server/doc
  System information as of Thu Nov  3 01:22:58 CST 2011
  System load:  0.0               Processes:           71
  Usage of /:   3.1% of 38.15GB   Users logged in:     0
  Memory usage: 6%                IP address for eth0: 10.0.2.15
  Swap usage:   0%
  Graph this data and manage this system at https://landscape.canonical.com/
New release 'oneiric' available.
Run 'do-release-upgrade' to upgrade to it.
Last login: Thu Nov  3 00:49:19 2011 from 10.0.2.2
vagrant@vagrant-ubuntu-natty:~$