jim yeh on 一月 28th, 2014

寫完〈訊息拆解組合的宣告式語意〉(原篇名:簡化設計的延遲運算-其一)之後,到現在同人才接著寫下第二篇系列文章。其實會隔那麼久才再動筆是有原因的,本來第二篇打算要寫剖析命令列的宣告式語意,不過同人覺得我原先的設計還不夠好,於是就暫時先把它擱置下來。結果一擱就擱了好久,慢慢地也讓我覺得沒有必要重新實作命令剖析列的宣告式語意,因為畢竟會使用到的機會並不多。其實命令列剖析的宣告式語意和前一篇文章的設計原理應該是一致的,因此同人後來決定要跳過它。剛好近看到有朋友討論相關的議題,激發我完成系列文章的動力,這一篇所要探討的主題正是有關於查詢的宣告式語意。

這篇文章同人不想談太多的理論,讓我們先來看一段查詢的宣告式語意的 client code:

list<MS1LineItem> t0101s = ctx
	.from(T010_JOIN_CST_PRD)
	.where(new T010DateEquals(ctx.getToday()))
	.where(new T010OperDateTimeBetween(nextTime, interval))
	.where(new T010PrId2NotEquals(PRID2_0002000))
	.select(T010_SELECTION, ctx, MS1FromT010Selector(
		ctx.getToday(), T010ST_1000));

list<MS1LineItem> t0102s = ctx
	.from(T010_JOIN_CST_PRD)
	.where(new T010DateEquals(ctx.getToday()))
	.where(new T010OperDateTimeBetween(nextTime, interval))
	.where(new T010PrId2Equals(PRID2_0002000))
	.select(T010_SELECTION, ctx, MS1FromT010Selector(
		ctx.getToday(), T010ST_2000));

list<pair<MS1LineItem, MS1LineItem> > t020s = ctx
	.from(T020_JOIN_CST_PRD)
	.where(new T020DateEquals(ctx.getToday()))
	.where(new T020OperDateTimeBetween(nextTime, interval))
	.select(T020_SELECTION, ctx, MS1FromT020Selector(
		ctx.getToday()));

list<MS1LineItem> t040s = ctx
	.from(T040_JOIN_CST_PRD)
	.where(new T040DateEquals(ctx.getToday()))
	.where(new T040OperDateTimeBetween(nextTime, interval))
	.select(T040_SELECTION, ctx, MS1FromT040Selector(
		ctx.getToday()));

MS1Writer writer(nextTime);

writer(t0101s);
writer(t0102s);
writer(t020s);
writer(t040s);
writer.save();

以上是一個轉檔程式的一部分片斷,為了避免牽涉業務邏輯資料的敏感,同人已經稍微做了一點修改。這一段程式碼採用平舖直敍的寫法,應該很容易可以看出這段程式碼的意圖。從這段程式我們看到 ctx 代表系統運作的環境,其中定義了查詢需要使用的參數(例如系統日期),並且負責建立資料庫連線以供查詢使用。

從程式碼的語意我們看到,每一段查詢是藉由 ctx 的 from() 產生查詢的實體,然後再用幾個 where() 來指明查詢條件,最後再用 select() 把查詢到的資料以 list 回傳。其實同人在以前在〈永續物件查詢之設計(其一)〉、〈永續物件查詢之設計(其二)〉有談過類似的表達語意,但這裡和以前運用 Design Pattern 的手法略有不同,在這裡同人是以 C++ template 來實現 Functional Programming 的概念,相信有接觸過 LINQ 的朋友,對這樣的寫法應該不陌生。

呼叫端透過 ctx 的 from() 生成一個查詢的實體,然後呼叫端再利用 where() 來指定查詢條件。where() 會傳回查詢本身,這樣呼叫端又可以再次使用 where() 增添更多的查詢條件(相當於以 and 的關係運算式連結多個查詢條件)。實際上,where() 並未馬上處理指定的查詢條件,而只是建立應對的表示式等到最後呼叫 select() 才會真的根據所有已加入的查詢條件查詢資料而回傳結果。 一個查詢可以複合多個查詢條件,完整的查詢條件我們稱為 criteria,然後把查詢結果轉換成對應的 domain object 的集合(std::list 或 std::map)。這樣查詢就可表示為 Query<T> 的 class template,它的定義如下面這段程式碼所示:

template <typename T, typename R>
class CriteriaExpression {
public:
	virtual R operator() (const T&) = 0;
};

template <typename T>
class Query {
protected:
	DbConnection* _dbconn;
	string _resource;
	shared_ptr<CriteriaExpression<T, string> > _criteria;	

private:
	Query<T>& assign(const Query<T>& other) {
		_dbconn = other._dbconn;
		_resource = other._resource;
		_criteria = other._criteria;
		return *this;
	}

public:
	Query(DbConnection* dbconn, const char* resource) :
		_dbconn(dbconn), _resource(resource) {
		_criteria.reset();
	}

	Query(const Query& other) {
		assign(other);
	}

	virtual ~Query() {
	}

	Query<T>& operator= (const Query<T>& other) {
		return assign(other);
	}

public:
	Query<T>& where(CriteriaExpression<T,
		string>* criteria) {
		shared_ptr<CriteriaExpression<T, string> >
			beforeCriteria = _criteria;
		_criteria = AndOperator<T>() (beforeCriteria,
			criteria);
		return *this;
	}

	Query<T>& where(
		shared_ptr<CriteriaExpression<T,
		string> > criteria) {
		shared_ptr<CriteriaExpression<T, string> >
			beforeCriteria = _criteria;
		_criteria = AndOperator<T>() (beforeCriteria,
			criteria);
		return *this;
	}

	list<QueryResultSet> select(const char* columns,
		const T& param, const char* orderBy = NULL) {
		list<QueryResultSet> rslist;

		ostringstream sql;
		string cs = columns;
		size_t pos = 0, found_pos;
		while ((found_pos = cs.find("%2C", pos)) !=
			string::npos) {
			cs.replace(found_pos, 3, 1, COMMA);
			pos ++;
		}
		sql << SELECT << cs << FROM << _resource;

		if (_criteria) {
			string s = _criteria->operator()(param);

			if (!s.empty()) {
				sql << WHERE << s;
			}
		}

		if (orderBy != NULL) {
			sql << ORDER_BY << orderBy;
		}

		int fetch = FETCH_START;

		rslist.clear();
		if (_dbconn->query(sql.str().c_str(), rslist)) {
			LOG(FileAtLine(__FILE__, __LINE__).c_str(),
				__PRETTY_FUNCTION__,
				"query results count = %u", rslist.size());
		}
		else {
			LOG(FileAtLine(__FILE__, __LINE__).c_str(),
				__PRETTY_FUNCTION__, "query fail!");
		}

		return rslist;
	}

	template <class ElementSelector>
	list<typename ElementSelector::result_type> select(
		const char* columns, const T& param,
		ElementSelector selector, const char* orderBy = NULL) {
		list<QueryResultSet>
			rslist = select(columns, param, orderBy);
		list<typename ElementSelector::result_type> result;
		result.resize(rslist.size());
		transform(rslist.begin(), rslist.end(), result.begin(),
			selector);

		return result;
	}

	template <class ElementSelector>
	list<typename ElementSelector::result_type> select(
		const T& param, ElementSelector selector,
		const char* orderBy = NULL) {
		list<QueryResultSet> rslist = select(
			ALL_COLUMNS_SELECTION, param, orderBy);
		list<typename ElementSelector::result_type> result;
		result.resize(rslist.size());
		transform(rslist.begin(), rslist.end(), result.begin(),
			selector);

		return result;
	}

	template <class ElementSelector, typename KeySelector>
	map<typename KeySelector::result_type,
		list<typename ElementSelector::result_type> >
		select(const char* columns, const T& param,
		ElementSelector elementSelector,
		KeySelector keySelector,
		const char* orderBy = NULL) {
		list<QueryResultSet> rslist = select(
			columns, param, orderBy);
		map<typename KeySelector::result_type,
			list<typename ElementSelector::result_type> >
			result;

		result.clear();
		list<QueryResultSet>::iterator rs = rslist.begin();
		for (; rs != rslist.end(); rs ++) {
			typename KeySelector::result_type
				keyvalue = keySelector(*rs);
			list<typename ElementSelector::result_type>
				valuelist = result[keyvalue];

			typename ElementSelector::result_type
				value = elementSelector(*rs);
			valuelist.push_back(value);
			result[keyvalue] = valuelist;
		}

		return result;
	}

	template <class ElementSelector, typename KeySelector>
	map<typename KeySelector::result_type,
		list<typename ElementSelector::result_type> >
		select(const T& param, ElementSelector elementSelector,
		KeySelector keySelector, const char* orderBy = NULL) {
		list<QueryResultSet> rslist = select(
			ALL_COLUMNS_SELECTION, param, orderBy);
		map<typename KeySelector::result_type,
			list<typename ElementSelector::result_type> >
			result;

		result.clear();
		list<QueryResultSet>::iterator rs = rslist.begin();
		for (; rs != rslist.end(); rs ++) {
			typename KeySelector::result_type
				keyvalue = keySelector(*rs);
			list<typename ElementSelector::result_type>
				valuelist = result[keyvalue];

			typename ElementSelector::result_type
				value = elementSelector(*rs);
			valuelist.push_back(value);
			result[keyvalue] = valuelist;
		}

		return result;
	}
};

Query<T> 是參數化了系統環境類別的查詢泛型,它提供 where() 來接受 criteria 的表示式,它必須是一個一元函數,提供 T 的類型之參數,結果回傳 sql 查詢條件字串、以及 select() 來接受把查詢結果轉換成對應 domain object 的一元函數,以 db 查詢的 query result set 為參數,回傳對應的 domain object。我們可以把傳入 where() 的表示式看成 filter,代表過濾器,把傳入 select() 的表示式看成 projection,代表映射器。Query<T> 並不會馬上運算傳入的表示式,直到呼叫 select() 之後才會一次運算所有的過濾器和映射器,下面這段程式是過濾器和映射器的實作範例:

class MS1FromT010Selector :
	public unary_function<QueryResultSet, MS1LineItem> {
private:
	string _sctToday;
	T010SelectionType _selectionType;

public:
	MS1FromT010Selector(const string& sctToday,
		T010SelectionType selectionType) :
		_sctToday(sctToday), _selectionType(selectionType) {}
	~MS1FromT010Selector() {}

	MS1LineItem operator() (QueryResultSet& rs) {
		MS1LineItem ms1;
		ms1.seqno = SeqnoGeneration(_sctToday,
			MYSYSMS1).getSeqno();
		ms1.operation = ((_selectionType == T010ST_1000) ?
			'1' : '2');
		ms1.prNo1 = rs.getString(T010_MEMBER_ID1, 0, 4);
		ms1.accNo1 = rs.getString(T010_CST_ACNO1);
		ms1.accCode1 = rs.getChar(T010_CST_KIND1);

		// ...

		return ms1;
	}
};

class T010DateEquals :
	public CriteriaExpression<MySysContext, string> {
private:
	Comparsion<MySysContext, Eq> _comparsion;

public:
	T010DateEquals(const string& dt) : _comparsion(
		new Column<MySysContext>(T010_DATE),
		new Constant<MySysContext, string>(dt)) {}
	~T010DateEquals() {}

	string operator() (const MySysContext& ctx) {
        	return _comparsion(ctx);
	}
};

從上面的實作我們可以看到應用 C++ template 如何實現泛函編程的概念,在這裡同人並沒有採用 C++0X 增加的 lambda expression,而是運用 C++ 編程的仿函式 functor 來達到到延遲運算的目的。這樣我們就可以很輕易地把查詢條件和資料轉換的部分抽離出來;過去在命令式的程式語意下,很多時候,我們常因為要思考如何實作查詢條件和資料欄位的轉換而使程式碼變得很雜亂。現在以泛函編程觀念的宣告式語意則是藉著組合多個簡單的小函式,運用延遲運算的概念讓程式碼變得更簡潔、而且更讓人一目了然知道程式的意圖;讓人們只要專注於系統在做什麼事而不是牽扯繁複的實作細節的描述,使程式變的更有彈性且簡明易讀。

不過限於篇輻,同人沒有辦法交待實作 CriteriaExpression<T, string> 的各種表示式,它們包括各種比較運算子、關係運算子、各種類型的常數、參數等表示式,定義它們並不困難,但要用比較精簡的表示方式來表示整個語法樹,也讓同人花了不少工夫。其實這些表示式,我並不是一開始就窮舉所有表示式,而是用到的時候才加入,然後再慢慢調整改善,讓整個查詢的宣告語意變得更簡單一點。這過程讓同人體會泛函編程和物件導向典範的搭配,其樂趣是無窮的呀。



     

2 Responses to “查詢的宣告式語意”

  1. [...] 從〈查詢的宣告式語意〉提到的實作中,我們可以發現有關資料搜尋的一種設計抽象概念,可以用相同的模式應用在不同的資料結構上,例如從 XML 文件中搜尋特定的資料節點。同人曾經用過宣告式語意實作轉換期貨交易 Span 檔案,將 XML 資料格式轉換成某種特定格式的資料檔案,像下面這段程式碼所示,程式碼的寫法比傳統的命令式語意寫法更為精簡而直覺。 [...]

  2. [...] 這種宣告式語意的寫法之前曾經有文章提到過,所以這篇文章就不再花篇輻多加介紹。你應該可以體會到,用以上這樣的模式來重構 legacy code,程式的意圖會很清楚地浮現出來,也會直接具體地關連到問題領域業務邏輯的語言,而不是一大串的 if-then-else 或是 for 迴圈的語法,這有助於不同觀點的相互溝通。你可以從主要流程中具體明白程式在做什麼,而如果需要瞭解實作的細節,則可以進一步去參考那些很簡單的物件化之小函式。由於不同關切點的分離,程式變得更簡單而且容易測試,對後續的程式維護甚至想要增加功能的需求來說,都會得到莫大的助益呀。 附註  博碩文化(2013),Kent Beck 的實作模式,p.103。[↩]       Posted by jim yeh 分析設計建模, 溝通, 生活感觸, 編程技巧, 職場, 設計原則 Subscribe to RSS feed [...]

Leave a Reply

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="">