Google Code Prettify

2015年11月8日 星期日

web development

項目日期
*
  Environment
1 Tomcat 7 安裝 SSL 憑證
2014/08/30
2 install GlassFish Server
2015/06/22
3 在 WebSphere 8.5.5.x 上遇到的 jar 檔版本和衝突問題
2017/03/30
*
  Programming (front-end)
1 struts2 jQuery tag (1) - TabbedPanel & DatePicker
2014/10/18
2 struts2 jQuery tag (2) - Grid
2014/10/19
3 jQuery: 處理 json 資料
2016/05/11
4 看見 Angular 的車尾燈
2017/03/26
5 第一支 Angular 程式
2017/04/06
6 在 WebSphere 上 Angular 中文存入資料庫變亂碼!
2017/06/20
7 Updating Angular CLI
2017/07/06
8 Spring Boot Angular Websocket
2018/07/20
*
  Programming (back end)
1 spring 4.x + struts 2.x + mybatis 3.x: getting started
2014/10/11
2 unit test on spring + struts2
2015/07/02
3 spring db connection
2015/12/22
4 spring MVC: getting started
2016/04/30
5 spring MVC + jQuery ajax
2016/05/02
6 spring MVC: 在 view 和 controller 間傳遞多列資料
2016/06/19
7 Spring Application Event
2017/07/23
8 變更 property 檔裡的設定值
2017/07/27
9 第一個 spring boot web 程式 (使用 embedded tomcat)
2017/12/31
10 application.properties@spring boot
2018/06/03
11 web server (https)
2023/06/11
*
  Programming (web services)
1 CXF - getting started
2016/07/04
2 spring boot RESTful 程式的單元測試
2018/03/23

2015年11月6日 星期五

重構: Duplicated Code (重複的程式碼)

Martin Fowler 大師於將近 20 年前出版的名著「Refactoring: Improving The Design of Existing Code」(重構 - 改善既有程式的設計),提出許多重構的手法,這些方法在當時是相當新穎的觀念與技術,現在已經是許多程式員每天寫程式一定會用到的。

在書中 3.1 節是談論重複的程式碼,這當然是程式的壞味道,是重構的標的物,底下將舉出我實際遇到的例子,以及我在重重限制下進行的重構。

這是一個有上百個 tables 的資料庫 web 系統,用 java 開發,沒有採用任何 MVC 的 framework,程式也沒有將 view、model 等區隔開,所以整個系統絕大多數的程式碼是寫在 jsp 裡,在這些 jsp 裡會看到大量的 html、javascript、jquery、css、java 摻雜,連接資料庫直接使用 JDBC API,底下的例子就是一個將 JDBC API 相關的程式,經過重構減少重複程式碼的例子。
PreparedStatement pstmt;
String sql;

sql = "UPDATE  FoudItem "  
    + " SET "
    + " name = NVL(?,' '),"
    + " type = ?, "
    + " rate = ? , "
    + " empNo = NVL(?,' '), "
    + "    mtime = SYSTIMESTAMP "
    + " WHERE fundNo =  RPAD(?,3) " 
    ;

pstmt = conn.prepareStatement(sql);
pstmt.setString(1, fundName);
pstmt.setString(2, fundType);
pstmt.setDouble(3, rate);
pstmt.setString(4, empNo);
pstmt.setString(5, fundNo);
pstmt.executeUpdate();
像這樣存取資料庫的程式碼在整個系統中有數百個,全部是同樣邏輯,先有一段用 string 一直相加產生的 sql statement,在產生 PreparedStatement 後,傳入參數並執行。我是後續接手維護的人,也被告知不能動大架構,所以我只簡單的如下重構,以去除重複的程式碼。




  • 將 sql statement 移到 xml 檔
<?xml version="1.0" encoding="UTF-8"?>

<sql>
    <statement id="update">
    <![CDATA[
    UPDATE  FoudItem "  
    SET "
        name = NVL(?,' '),"
        type = ?, "
        rate = ? , "
        empno = NVL(?,' '), "
        mtime = SYSTIMESTAMP "
    WHERE fundno =  RPAD(?,3)
    ]]>
    </statement>
</sql>
  • 自 xml 中讀取 sql statement
private static String getSqlStatement(String name, String id) {
        File file = new File(path + name + ".xml");

        SAXReader reader = new SAXReader();
        Document document;
        
        try {
            document = reader.read(file);
            Element sqlElmt = document.getRootElement();
            Iterator<Element> iter = sqlElmt.elementIterator("statement");
            while (iter.hasNext()) {
                Element elmt = iter.next();
                if (id.equals(elmt.attributeValue("id"))) {
                    return elmt.getText();
                }
            }
        } catch (DocumentException e) {
            logger.error(e.getMessage(), e);
        }
        
        return StringUtils.EMPTY;
    }
這段程式碼很簡單的讀取指定目錄下 xml 檔,以取得程式需要的 sql statement。
  • 傳入參數到 PreparedStatement 並執行
public static int execute(Connection conn, String name, String id, Object... params) throws SQLException {
        String sql = getSqlStatement(name, id);
        PreparedStatement pstmt = conn.prepareStatement(sql);        
        for(int i=1; i<=params.length; i++) {
            Object param = params[i-1];
            
            if (param instanceof String) {
                pstmt.setString(i, (String) param);
            }
            else if (param instanceof Long) {
                pstmt.setLong(i, (Long) param);
            }
            else if (param instanceof Double) {
                pstmt.setDouble(i, (Double) param);
            }
            else if (param instanceof BigDecimal) {
                pstmt.setBigDecimal(i, (BigDecimal) param);
            }
            else {
                throw new SQLException("field's type is invalid");
            }
        }
        
        return pstmt.execute();
    }
  • 改寫原 JDBC 相關的程式
上面的三個片斷,除了存放 sql 的 xml 檔,每次新增程式都要增加外,另外兩個片斷是共用程式,這些共用程式放在命名為 SQLExecutor 的類別裡,利用這些程式改寫原本的程式如下: 
SQLExecutor.execute(
    conn, "fund", "update",
    fundName, fundType, rate, empNo, fundNo
);
改寫完了之後,一大段的程式最後只剩一行指令! (從命名為 fund.xml 的檔案裡,讀取 id = update 的 sql statement,產生 PreparedStatement 後,將相關參數傳入執行。)

底下是重構一書 3.1 節的原文,有興趣的可以看看,沒看過這本書的人,建議買一本,絕對值得!
Duplicated Code
Number one in the stink parade is duplicated code. If you see the same code structure in more than one place, you can be sure that your program will be better if you find a way to unify them. 
The simplest duplicated code problem is when you have the same expression in two methods of the same class. Then all you have to do is Extract Method and invoke the code from both places. 
Another common duplication problem is when you have the same expression in two sibling subclasses. You can eliminate this duplication by using Extract Method in both classes then Pull Up Field. If the code is similar but not the same, you need to use Extract Method to separate the similar bits from the different bits. You may then find you can use Form Template Method. If the methods do the same thing with a different algorithm, you can choose the clearer of the two algorithms and use Substitute Algorithm. 
If you have duplicated code in two unrelated classes, consider using Extract Class in one class and then use the new component in the other. Another possibility is that the method really belongs only in one of the classes and should be invoked by the other class or that the method belongs in a third class that should be referred to by both of the original classes. You have to decide where the method makes sense and ensure it is there and nowhere else.