TWJUG《重構專題》學習材料導讀
- notes refactoring twjug
為什麼想做重構練習呢?
因為發現了適合的材料啊!
又是新的一年啊!(2022)
每年的新開始,就會想要有一種 reset 的感覺。先不要追逐最新最潮的技術,回頭盤點一些其實該牢牢握在手上的小工具。(詸之聲:這聽起來適合在年底做吧!?)
至於,要選擇什麼樣的知識來討論這讓我苦惱了很久,最終是選擇了「重構」作為主要想面對的主題。因為,過去幾年與一些試著轉職的網友們聊過,也看過了一些人在培訓機構實作的作品。而他們的作品,不少是公開放在網路上的,這些東西都可以成為練習重構的材料。儘管,在少數幾個專案中,並沒有辦法涵蓋重構名錄上的各種問題,但常見的 bad smell 與新手常見實作型式,反倒是更適合用來討論的例子。
我們選用的例子會是在 GitHub 用 資策會
能搜尋到的 Java Web 專案,畢竟這是培訓的大宗。在幾年前開始接觸到這類的「作品」,去年和網友討論他的專題內容時又再看了一下程式,突然發現同樣的結構,同樣的初學者「樣式」反覆出現在不同屆的專題原始碼內。突然意識到,這是一種 legacy code 的世代複製。這是當下能力所及的實作,若是在入行幾年代有所成長,再回頭看看原來的專題,是不是會有不同想法呢?
所以,我們可以想像著在有了經驗後,怎麼去給「當時的自己」改善的建議。用這個出發點,來用網路上公開的專案進行「重構」的練習。我們得強調,是取得材料然後練習重構所需的「觀點」與「技法」。在過程中,我們會試著用不同的「觀點」來點出問題,並練習可以把程式碼變得更好的方法。
學習材料
透過 GitHub 的搜尋功能,加上 Java
語言的過濾條件。我們可以找出需要的材料:
由於這是個 Eclipse 專案,我們配合它使用 Eclipse 匯入查看:
你可以看到專案中有相當多的 package,並且都是呈對的:
- xxx.yyy.
controller
- xxx.yyy.
model
這是典型的資策會專案的組織型式。通常 xxx.yyy
package 會是由同一個人實作,而 xxx.yyy
內的 Controller 會是一隻 Servlet 與 URL Pattern 的搭配,多數使用傳統的 Deployment Descriptor (也就是 web.xml
) 註冊,在少量的 Servlet 內會看以 @WebServlet
註冊的型式。
依先前訪談的經驗,一個小組會有若干人組成。看似 最沒問題的,大概會是前端頁面的部分。因為,這專題的主要目標是在展現身為 Java Web 應用程式的初級開發者能力所及的範圍,前端部分多為傳統的非前後端分離的實作方式,搭配 jQuery 進行 AJAX 開發,只有特地為了 AJAX 準備的 Request 部分能沾上前後端分離的邊角。剩下的部分,就依頁面或是「模組」來均攤到各組員身上,而這裡的模組就是我們前面看到的一組 xxx.yyy
package。
這樣的 Project Layout 只有方便「圈地」,沒有太大實質的意義。因為,多半是用在寫履歷時,描述合作專案中哪些「模組」是由自身實作的,而不用零散地指出哪些 class 是由自身實作的。可是,這樣的圈地並不會起太大的效用,因為如唐伯虎點秋香提到的:
紅花需要有綠葉陪襯
一眼望去都是綠葉是無法吸引人的注意力的。
我會試著整理它,也許會有一些評批,要針對的並不是實作者,因為那就是培訓單位在有限的時間內,配合整個班級能達到的進度去實作出來的「實力公約數」。去 GitHub 上多查不同的組別,有些也是有進入到 Spring Boot 與 JPA 時代的,但多數都還在很初級的階段。在這個初級的階段,反而是在社群推廣中能協助的,畢竟,我們樂於作為新手的墊腳石,即便新手不知道如何求助於人。
目標與限制
我們的目標是利用這些程式,由他的現狀透過重構、設計模式或脫離初級開發者後的經驗來改善它。大致上,我會先做個簡單地限制:
先不要進入框架的時代
例如:
- 採用 Spring Framework 或 Spring Boot
- 使用 Hibernate 或 JPA (Java Persistence API)
儘可能使用標準函式庫提供的工具,但輔助一些 Open Source 的 Library 為主。
除此之外,我們會大量鎖定在各種該「提示」的 Bad Smell 上,特別是新手容易寫出的 Duplicated Code,並且特別容易在各種型式上重複:
- 重複的程式碼區塊
- 重複的程式結構
- 重複的瑣碎實作
- …
重複會造成諸多的維護問題,他必需被適當地收納。而學習這些「收納」的技巧,也不太需要經年累月的累積才行。單純地,看過了、知道了、用上了、學到了的流程罷了。
PS. 其他太微小的 Coding Style 問題並不會有主題的型式去討論,多數情況會去忽略它或是以碎唸的型式出現在文章或影片中,那些是開發者自己需要養成的習慣。
基本問題列管
在這先粗略地列一些顯而易見的問題,但這並不包含全部的問題。可以隨著有興趣一起參與討論的朋友,自行列出可以提出「改善建議」的項目來追加。
Controller (servlet)
- 重複的 encoding 設定,這其實可以是 Filter 去處理的
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
res.setContentType("text/html; charset=UTF-8");
// ...
}
- 使用者認證採用設計不良,採「明碼」存放在資料庫內
- 肥大的商業邏輯都擠在 Servlet 內,取其中一隻 Servlet 簡化後的結構如下:
public class XXXYYY extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
doPost(req, res);
}
public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
String action = req.getParameter("action");
if ("getAllMemMat_record_Display".equals(action)) { // 來自select_page.jsp的請求
}
if ("insert".equals(action)) { // 來自addMat_record.jsp的請求
}
if ("delete".equals(action)) { // 來自listAllMem.jsp
}
if ("ajaxInsert".equals(action)) { // 來自addMat_record.jsp的請求
}
if ("ajaxDelete".equals(action)) { // 來自listAllMem.jsp
}
}
}
相似的結構,重複了整個專案。這每一個 condition 的 block 內都有著不少行數的實作,夾雜了 Business Logic 與 DAO 的操作。
- (承 3.) 由於缺乏適當的分層,讓程式看起來不好維護,閱讀時的心裡負擔比較大。至少能切出三層來,讓 Business Logic 放到 Service 內去實作,Controller 就純粹負責 Persentation (
收參數 -> 委派 service -> 輸出結果
):
controller -> service -> dao
更進一步,若能實作一個小巧的 Dispatcher 去收納「海量」Controller 的 Routing 問題,那會讓程式更加簡潔。
Data Access Object
-
無意義地抽象化,特別是每一個 Dao 都有自己的 interface,分別給 JDBC 實作與 JNDI 實作使用。除了 Connection 來源不同之外,沒必要維護 2 份一樣的 Dao。還會其中一個修改了,另 一個忘了修改。應該抽象化的對象是
ConnectionFactory
。(可以由 YouTube 影片參考比較的結果) -
重複而瑣碎的 JDBC API
@Override
public void insertWithLespic(LessonVO lessonVO, List<LespicVO> list) {
Connection con = null;
PreparedStatement pstmt = null;
try {
try {
Class.forName(driver);
con = DriverManager.getConnection(url, userid, passwd);
// 新增課程
String cols[] = { "LES_NO" };
pstmt = con.prepareStatement(INSERT_STMT, cols);
pstmt.setString(1, lessonVO.getDs_no());
pstmt.setString(2, lessonVO.getDs_name());
pstmt.setString(3, lessonVO.getLes_name());
pstmt.setString(4, lessonVO.getLes_info());
pstmt.setString(5, lessonVO.getCoach());
pstmt.setInt(6, lessonVO.getCost());
pstmt.setInt(7, lessonVO.getDays());
pstmt.setDate(8, lessonVO.getSignup_startdate());
pstmt.setDate(9, lessonVO.getSignup_enddate());
pstmt.setString(10, lessonVO.getLes_state());
pstmt.setInt(11, lessonVO.getLes_max());
pstmt.setInt(12, lessonVO.getLes_limit());
pstmt.setDate(13, lessonVO.getLes_startdate());
pstmt.setDate(14, lessonVO.getLes_enddate());
pstmt.setString(15, lessonVO.getLess_state());
pstmt.executeUpdate();
// 擷取對應的自增值主鍵
String next_les_no = null;
ResultSet rs = pstmt.getGeneratedKeys();
if (rs.next()) {
next_les_no = rs.getString(1);
System.out.println("自增主鍵值=" + next_les_no + "(剛新增成功的課程編號)");
} else {
System.out.println("未取得自增值主鍵");
}
rs.close();
// 在同時新增課程照片
LespicJDBCDAO dao = new LespicJDBCDAO();
System.out.println("list.size()-A=" + list.size());
for (LespicVO lespics : list) {
lespics.setLes_no(next_les_no);
dao.insert2(lespics, con);
}
// 設定於pstmt.excuteUpdate()之後
con.commit();
con.setAutoCommit(true);
System.out.println("list.size()-B=" + list.size());
System.out.println("新增課程編號" + next_les_no + "時,共有" + list.size() + "張照片同時被新增");
} catch (SQLException se) {
if (con != null) {
try {
// 3●設定於當有exception發生時之catch區塊內
System.err.print("Transaction is being ");
System.err.println("rolled back-由-dept");
con.rollback();
} catch (SQLException excep) {
throw new RuntimeException("rollback error occured. " + excep.getMessage());
}
}
throw new RuntimeException("A database error occured. " + se.getMessage());
// Clean up JDBC resources
}
} catch (ClassNotFoundException e) {
throw new RuntimeException("Couldn't load database driver. " + e.getMessage());
} finally {
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException se) {
se.printStackTrace(System.err);
}
}
if (con != null) {
try {
con.close();
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
}
}
- (承 2.) 對於 Connection、Statement 與 ResultSet 的管理,需要更加簡化。而不是各別手擼
try-catch-finally
- (承 2.) 對於資料狀態的轉移,必需要更加容易。而非人工去呼叫 set 與 get。
結語
上述列出來的問題,是一些顯而易見的問題。同時,也可能是「拿著作品去面試」會被挑出來討論的問題。希望這個系列,能告訴「小時候不懂事」的自己,用成熟的方法去處理它們。
後續,我們會在 TWJUG 線上聚會的時間,逐步抓一點出來討論研究。