前文提到在查詢永續物件的後端實作上,在處理 where 條件式的部分,對於物件查詢的樹狀結構條件表示式而言,可以運用 GOF 的 interpreter pattern 的觀念,藉由尋訪語法樹的過程中來予以解析同時加以處理。
但在實作的考量上,相同的條件表示式在解析處理上卻必須隨著採用的永續框架而有所變化。因此,解析處理的實作並不能放在 QueryExpression 中,而是必須將解析處理的概念封裝為尋訪器的界面,讓 QueryExpression 可接受不同尋訪器,以容納可針對採用不同永續框架而有不同解析處理的功能。
此種做法其實就是利用 GOF 的 visitor pattern 來增加系統設計的彈性,但限於篇幅,前文並未交待要如何做,本文就是要接續這個問題來做後續探討。首先,我們定義用來解析處理樹狀結構之條件表示式的尋訪器界面,即 QueryExpressionVistior,它知道如何尋訪條件表示式中的各種資料型態。然後讓 QueryExpression 可以接受不同的尋訪器來解析處理條件表示式:
public interface QueryExpression {
public void visitPropertyExpression(
PropertyExpression expression,
ExpressionContext context);
public void visitScalarExpression(
ScalarExpression expression,
ExpressionContext context);
public void visitScalarComparisonExpression(
ScalarComparisonExpression expression,
ExpressionContext context);
public void visitRelationExpression(
RelationExpression expression,
ExpressionContext context);
…
}
public interface QueryExpression {
public void accept(
QueryExpressionVisitor visitor);
}
public interface ScalarExpression
extends QueryExpression {
public Object getValue(
ExpressionContext context);
}
…
這段程式,同人用 ScalarExpression 來當成 ParameterExpression、ConstantExpression 的一般化抽象概念,在解析處理語法樹的過程當中,可透過 getValue method,依據傳入的語境(記錄表示式中參數之值) ExpressionContext 來取得變數的實際值,以供代入條件式中。
在此只簡單展示在前文的程式片段中曾列出條件表示式。在實務上,程式開發可能需要更多種類的 QueryExpression,例如在比較式中,亦常見 in、between、not、is (not) null 等運算子,或在比較表示式中有關永續物件屬性的相互比較等,限於篇幅,本文無法全部提到。但依本篇所提到的設計觀點,其實很輕易地,可以自行舉一反三。於是,在前文的 HqlQueryBuilder 的 buildWhere 就會像這段程式所示:
…
public void buildWhere(
QueryExpression expression,
ExpressionContext context) {
HqlQueryExpressionVisitor visitor =
new HqlQueryExpressionVisitor(
context);
expression.accept(visitor);
whereClause = visitor.getWhereClause();
}
…
public class HqlQueryExpressionVisitor
implements QueryExpressionVisitor {
…
public void visitPropertyExpression(
PropertyExpression expression,
ExpressionContext context) {
hqlText = expression.getName();
}
public void visitScalarExpression(
ScalarExpression expression,
ExpressionContext context) {
hqlText = expression.getValue(context);
}
public void visitScalarComparisonExpression(
ScalarComparisonExpression expression,
ExpressionContext context) {
HqlQueryExpressionVisitor leftVisitor =
new HqlQueryExpressionVisitor(
context);
HqlQueryExpressionVisitor rightVisitor =
new HqlQueryExpressionVisitor(
context);
expression.getProperty()
.accept(leftVisitor);
expression.getScalar()
.accept(rightVisitor);
hqlText = leftVisitor.getHqlText() +
expression.operatorText() +
rightVisitor.getHqlText();
}
public void visitRelationExpression(
RelationExpression expression) {
int count = expression.getCount();
QueryExpression [] expressions =
expression.getExpressions;
if (count > 1) {
List hqlList = new ArrayList();
for (int i = 0; i < count; i++) {
HqlQueryExpressionVisitor v =
new HqlQueryExpressionVisitor(
context);
expressions[i].accept(v);
String itemHqlText =
v.getHqlText();
hqlList.add(itemHqlText);
}
StringBuffer sb = new StringBuffer();
for (i = 0; i < hqlList.size(); i++) {
if (i > 0) {
sb.append(SPACE);
sb.append(
expression.getOperator());
}
sb.append(LEFT_PARENTHESIS);
sb.append(hqlList.get(i));
sb.append(RIGHT_PARENTHESIS);
}
hqlText = sb.toString();
}
else if (count > 0) {
expressions[0].accept(this);
}
}
…
}
…
從這一段程式,不難理解尋訪器解析處理條件表示式的過程,首先由解析者建立語法解析尋訪器,並設定好要條件表示式的語境,為了程式的簡潔性考量,我們將HQL的語法解析尋訪器的建構子加上語境 ExpressionContext 當參數。尋訪器在解析處理完成後,會將解析處理結果存在尋訪器內部的結構,可供解析者取回,這種運作方式其實和 builder pattern 如出一轍。
而在 visitXXX 的實作方面,如果 QueryExpression 有子節點時,則先建立新的語法解析尋訪器來解析處理子節點,然後再整合完整的 HQL 表示式,這樣就可輕易地處理複合條件式的語法,如上段程式中的 RelationExpression 及 ComparisonExpression,其運作原理並不複雜且充滿彈性。
以上便是同人在永續物件查詢的主要設計想法,同時也概述了其實現的過程,這過程其實不難體會到有關 builder pattern 及 visitor pattern 的不同運用。builder pattern 與 visitor pattern 可用來解除實作與資料結構的耦合度。這兩者都可以依據相同的資料結構來對資料做不同的處理,它們都會保留處理過程的狀態,等到資料處理完成後再由呼叫者自行取回。
然而,兩者較大的不同的是 builder pattern 適用較單純的資料結構,運作過程多以反覆方式處理資料。所以,可以把通用性的控制流程抽取成 Director,把差異性的資料處理封裝成 Builder 界面,以實現不同的 Builder 實作,而達到擴充功能的目的。
visitor pattern 則是適用於較複雜的樹狀結構,運作過程中,大部份是用階層方式處理資料。因此,它並不需要 Director 來主導處理資料的流程,而是將具差異性的資料處理封裝成 Visitor 界面,讓它同時負有尋訪及處理資料的任務,而實作者也可藉由實現不同處理的 Visitor 實作,來達到擴充功能的目的。
Design pattern 的運用常常是容易讓開發者產生疑惑及混淆的地方。雖然 design pattern 可以讓我們在面對不確定時,為所設計的系統預先留下彈性,以容納新功能以符合需求的變化,但不適當地運用 design pattern,卻為讓設計產生不必要的複雜度而增加開發的負擔。因此,學習 design pattern 的困難之處並不在於 design pattern 如何實作,而是在於察覺使用 design pattern 的時機。
就拿本文的例子來說,一開始我們並不著眼於 design pattern 上,而是隨著設計的不同考量上,藉由逐步發現與解決設計問題的過程中,讓 design pattern 躍然成形。讓問題來主導設計,而非讓設計使問題複雜化。當然,這也有賴於開發者在平常就要不斷地練習與思考,畢竟簡潔而富有彈性的設計不是一蹴可幾的呀。
自動引用通知: 查詢的宣告式語意 « 同人的生活派對
自動引用通知: 更簡單的條件查詢設計 « 同人的生活派對