以泛函編程增進功能的可測性

很多人都知道系統開發的功能測試很重要,但有時候我們省略它並不是認為某個功能不需要測試,而是不知道該功能該如果測試。有時候問題的關鍵是卡在環境整合的複雜性,例如牽涉 web 的 session 或是資料庫存取的問題,就讓人很難隔離這些複雜情況。當然理論上我們可以運用 Mock 的技術來隔離它們,然而現實上,即使運用這些技術可能你可能還是無法解決無法測試的問題。這時候,你需要的通常不會是尋求其它更強大的技術,而是需要在編程核心思維上,做一些小改變,來改善功能的可測性。

最近同人就碰到一個實例,讓我體會到以泛函編程(FP)典範增進功能的可測性。其實,以前遠在美國的 Perter Ho 就曾向包括同人在內的幾位點空間的朋友,分享過相同的觀念,而同人則是藉由最近的實例而對這樣的觀念有更深刻的體驗。

這個實例不是困難的問題,而是在向資料庫查詢時,組合 SQL 語句用到 IN 的條件式,但需要符合的集合卻超過 1000 個,因而導致系統的異常發生。這個問題要解決並不困難,只要每 500 個分批下 SQL 查詢,然後把每個 SQL 查詢到的資料加到查詢結果就可以了。這樣程式會寫成:

	int i = 0;
	do {
	    int fromIdx = i;
	    int toIdx = ((list.size() - i) > QUERY_BATCH_SIZE) ?
	    	    QUERY_BATCH_SIZE + i : list.size();

	    List<String> parts = list.subList(fromIdx, toIdx);

	    String sql = sqlExpr.replace(AID_LIST_REPLACEMENT,
	    	    getListToSql(parts));

	    Session session = (Session) getEntityManager().getDelegate();
	    @SuppressWarnings("unchecked")
	    List<Object[]> querylist = session.createSQLQuery(sql).list();
	    resultList.addAll(querylist);
	    i = toIdx;
	} while (list.size() - i > 0);

	return results;

只是上面這段 code 很難被驗證其功能是無誤的。假設以 SQL 向資料庫讀取資料沒問題,那麼讓人關心的問題就會是如何確認分批組合 SQL 語句的演算法會正確無誤,而不是有重覆或遺漏的部分。然而如果要針對它寫測試,那就必須準備有用的測試資料,而且也要面對此功能掛載在主機 context 的問題。我們的系統是採用 Seam Framework 架構,寫測試案例需要承擔一定的負擔及代價,之前沒有人嘗試過,而同人也覺得似乎不該貿然行事。

於是同人試著改變作法,我認為需要測試的並不是讀取資料的環境整合,而是能不能正確沒有重覆和遺漏地分批組合 SQL。所以我應該可以把這段演算抽離出來,並且可以對它做單獨的驗證。同人先用測試案例來表達我對它的願望:

public class PartsOfListTest {

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void test() {
	List<String> list =
		Arrays.asList("1", "2", "3", "4", "5", "6");
	List<List<String>> results =
		new PartsOfList<String>(3).from(list);

	assertEquals(results.get(0).get(0), "1");
	assertEquals(results.get(0).get(1), "2");
	assertEquals(results.get(0).get(2), "3");

	assertEquals(results.get(1).get(0), "4");
	assertEquals(results.get(1).get(1), "5");
	assertEquals(results.get(1).get(2), "6");

	results = new PartsOfList<String>(2).from(list);

	assertEquals(results.get(0).get(0), "1");
	assertEquals(results.get(0).get(1), "2");

	assertEquals(results.get(1).get(0), "3");
	assertEquals(results.get(1).get(1), "4");

	assertEquals(results.get(2).get(0), "5");
	assertEquals(results.get(2).get(1), "6");

    }
}

從測試案例中找到 PartsOfList<T> 的演算邏輯,用來把 List<T> 分批成 List<List<T>> 的串列。這樣我們就可以把一個很長的串列分成多個較短的串列,然後再進行分批的處理。建構 PartsOfList<T> 需要指定每個子串列的最大空間,並且利用 from() 傳入串列來分成多個子串列。

接下來讓測試案例過關,同人實作 PartsOfList<T> 的內容如下所示:

public class PartsOfList<T> {

    private int partSize;

    public PartsOfList(int size) {
	partSize = size;
    }

    public List<List<T>> from(List<T> list) {
	List<List<T>> results = new ArrayList<List<T>>();
	int i = 0;
	do {
	    int endIndex = (list.size() - i) > partSize ? 
	    	    i + partSize : list.size();
	    List<T> part = list.subList(i, endIndex);
	    results.add(part);

	    i = endIndex;
	} while (i < list.size());
	return results;
    }
}

然後最前面的程式片段則會改成如下面的內容:

	List<List<String>> parts = 
		new PartsOfList<String>(QUERY_BATCH_SIZE).from(list);
	for (List<String> aids : parts) {
	    String sql = sqlExpr.replace(AID_LIST_REPLACEMENT, 
	    	    getListToSql(aids));

	    Session session = (Session) getEntityManager().getDelegate();
	    @SuppressWarnings("unchecked")
	    List<Object[]> querylist = session.createSQLQuery(sql).list();
	    resultList.addAll(querylist);
	}
	return resultList;

我們看到這樣程式碼更專注於它們各自的職責,而且 PartOfList 也可以重覆使用在其它適用的情況,更重要的是程式碼的驗證是可行的,連帶程式碼的可讀性、擴充性、以及彈性都得到加強。這又一次地讓人體會到泛函編程宣告式語意的簡潔有力,搭配與物件導向的思維的整合,無疑是提升系統抽象概念堪稱完美的組合。

Please follow and like us:
分類: 分析設計建模, 問題解決, 生活感觸, 編程技巧, 職場, 設計原則。這篇內容的永久連結

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *