jim yeh on 七月 31st, 2007

要有效率地處理資訊系統資料存取,應用系統的實作通常都會需要整合關聯式資料庫。因此,在應用系統之軟體的設計方面,除了業務上的程式邏輯上,也必須考量在系統面上與資料庫技術的整合。而在實務上,為了讓設計儘量簡單及單純,避免業務面需求與系統面技術對軟體功能與品質產生複雜的交互作用。一般而言,會讓業務邏輯的抽象概念與資料庫的實作分隔開來。如此,永續層的抽象概念便應運而生,下圖便是基於此設計想法,所發展出來的設計架構

persistent.PNG

這個設計架構將應用系統分成四個邏輯概念層,包括服務層(ServiceLayer)、業務層(BusinessLayer)、永續層(PersistentLayer)及永續框架(PersistentFramework)。服務層負責接受外界呼叫者的呼叫要求,業務層則負責依據業務邏輯來處理外界的呼叫需求,而永續層則負責處理將物件儲存至永久儲存媒體(通常為資料庫),並呼叫永續框架達成資料存取的功能。

這樣利用分層負責的概念,讓軟體開發的工作變得簡潔而有效率。不過,在設計上要特別注意,對於 BizDataAccessor 界面而言,其 method 簽章不可以與所使用的永續框架產生相依性,否則就沒辦法達到設計概念與實作細節分開的理想。

例如,使用 Hibernate 的永續框架,BizDataAccessor 的 method 中,就不應該用 Hibernate 的 SessionCriteria 來當做參數或傳回值。否則,一旦改用其它的永續框架,BizServiceImpl 將會受到影響而必須改寫,而破壞設計的良好結構。

換句話說,BizDataAccessor 之 method 參數與傳回值型態,應該不能參照到永續框架所特有的類別,像 Hibernate 的物件查詢語言(HQL)或條件式查詢(Criteria)都是不能使用的。

然而,實務上,BizDataAccessor 的呼叫者卻很可能需要用一致的表示方式來代表各種不同查詢條件。舉一個最簡單的例子來說,使用者可能希望在前端勾選不同的欄位來組成查詢條件,系統就能依他所選擇的條件查出符合查詢條件的資料。但要設計出能滿足此需求的功能,在 BizDataAccessorImpl 的實作上將會面臨到兩大難題。

首先,如前所述,Hibernate 現成的 HQL 或 Criteria 不能直接使用;其次,就算可以直接使用它們,對於使用者未勾選的欄位,組成如 ”where selected_attribute = checkValue and not_selected_attribute = null”的條件表示式應該是有問題的,應該組成的條件式理應像”where selected_attribute = checkValue”才對,但不管用 HQL 或 Criteria 都無法直接做到這個功能。

所以,典型的做法會變成把組成條件式語法的邏輯,變成 BizDataAccessorImpl 中的一連串的 if 敍述中,這種做法相當的 dirty 且 ugly,而且條件邏輯一旦變複雜,還很容易出錯,那怎麼呢?同人願意在此分享我在實務上的設計及實作心得,供大家參考。

其實不管我們用那種永續框架,我們都必須用通用的表示式來描述查詢條件表示式,然後再轉換成特定的永續框架的查詢實作方式。所以我們把物件查詢條件表示式視做一個通用的 class,並將之命名為 QueryCriteria。

此舉並非意味著我們要自己造輪子,用 QueryCriteria 取代 Hibernate 的 HQL 或 Criteria,而是要建立一個共通的表述方式,讓使用不同的永續框架都能採用一致的作法來滿足需求。我們的目的是利用永續框架,但卻不受特定的框架所限制。所以,在底層的實作上,還是需要用到永續框架特有的方式來實作物件查詢的。

用 QueryCriteria 封裝查詢條件的主意看起來還不錯,這樣一來,呼叫 BizDataAccessor 的過程,大概會像這樣:

BizDataAccessor dao;
BizObject bo;
ExpressionContext context;
...
context.setValue("sexParm", "male");
List boList = dao.find(
 
QueryCriteria.from(BizObject.class).
  
where(Relation.and()
   .
add(ComparisonExpression.like(
    
new PropertyExpression("name"),
    
ConstantExpression.getString("abc%")))
   .
add(ComparisonExpression.eq(
    
new PropertyExpression.property("sex"),
    
new ParameterExpression("sexParm"))),
  
context));
Iterator itr = boList.iterator();
while(itr.haveNext()) {
 
bo = itr.next();
 
// processing bo
 ...
}

這段程式碼的語意,是要查詢出 BizObject 中,符合 name 屬性前三碼為 abc,且 sex 屬性為 male 的物件實例。我們在 QueryCriteria 中的 where 條件式中建立樹狀語法結構 QueryExpression,用運算表示式的方式表達出通用的查詢條件,並可依據 ExpressionContext 語境的內容,產生出不一樣的查詢條件。例如,在這段程式中,如果 sexParm 的值是設為 female,則所產生的查詢條件式將會變成找出符合 name 屬性前三碼為 abc,且 sex 屬性為 female 的物件實例。

那麼,後端的 BizDataAccessorImpl 要如何實作呢?因為,QueryCriteria 是由查詢呼叫端建立並傳入的,所以,不管後端如何實作查詢的功能,其呼叫界面都應該是一致的。當 BizDataAccessorImpl 在收到 QueryCriteria 之後,必須要依照共通的查詢條件表示方式,轉換成特定的永續框架實作。這代表著相同 QueryCriteria 的結構,可依不同的永續框架而產生不同的實作方式。因此,我們選擇採用 GOFbuilder pattern 來實作 BizDataAccessorImpl,如下段程式碼所示:

public class BizDataAccessorImpl
    
implements BizDataAccessor {
    ...
    
public List find(QueryCriteria criteria) {
        
HqlQueryBuilder builder =
            
new HqlQueryBuilder();
 
        
new QueryCriteriaProcessDirector()
            .
makeProcess(criteria, builder);
        
String hql = builder.getHqlText();
        
// using hql to find objects
        ...
    
}
    ...
}
 
public interface QueryBuilder {
    
public void buildFrom(Class clazz);
    
public void buildWhere(
        
QueryExpression expression,
        
ExpressionContext context);
    ...
}
 
public class QueryCriteriaProcessDirector {
    
public makeProcess(QueryCriteria criteria,
        
QueryBuilder builder) {
        
builder.buildFrom(criteria.getFrom());
        
builder.buildWhere(criteria.getWhere(),
            
criteria.getContext());
        ...
    
}
}
 
public class HqlQueryBuilder {
    ...   
    
public void buildFrom(Class clazz) {
        
queryClass = clazz;
    
}
 
    
public void buildWhere(
        
QueryExpression expression) {
        ...
        
whereClause = ...
    
}
 
    
public String getHqlText() {
        
return FROM + queryClass +
            
WHERE + whereClause + ...
    
}
}

上段程式碼中,QueryCriteriaProcessDirector 負責處理條件查詢表示式,它提供了 makeProcess,使 BizDataAccessor 可以傳入 QueryCriteria及特定永續框架的 QueryBuilder 的實作來產生應對的查詢實作,而此處我們是以 Hibernate 的 HQL 為例,這樣便可以巧妙地去除呼叫者與物件查詢的實作者之間的相依關係了。

當然,在處理 where 條件式的部分,同人並沒有明確交待該如何實作。其實,這一段的處理正是物件查詢條件表示式處理最關鍵之處,物件查詢的條件表示式是一種樹狀結構語法樹,當然理應運用 GOF 的 interpreter pattern 來予以解析,不過,QueryExpression 並不適合加入任何有關的語法解析的操作,於是,採用 GOF 的 visitor pattern,使相同語法樹可以接受不同的語法解析器來解析條件表示式是個可行的想法,但要怎麼做呢?限於篇幅,請容同人賣個關子,欲知詳情,請待下回分解;^)。



     

7 Responses to “永續物件查詢之設計(其一)”

  1. 哈米尼斯 說道:

    我有一個疑問是:
    這樣的架構是base on 完整而通盤的規劃之下吧
    但就現實問題而言,一家公司裡,可能許多套系統都是由不同的廠商開發,若以這樣的角度來看,這一個架構似乎有點問題。
    再來則是效能考量的問題,我認同這一個架構下,可以讓開發和維護變得簡潔,但若是遇到某些特定的系統,如下單系統,它強調的重點會在反應時間及大量資料處理,若是以此為考量,這樣的架構似乎又繞了太多層?

    一點小小看法,請您多多指教

  2. [...] 前文提到在查詢永續物件的後端實作上,在處理 where 條件式的部分,對於物件查詢的樹狀結構條件表示式而言,可以運用 GOF 的 interpreter pattern 的觀念,藉由尋訪語法樹的過程中來予以解析同時加以處理。 [...]

  3. jim yeh 說道:

    這是個實際運作的架構,應用在銀行的付款系統,所以對績效的要求是非常嚴苛的,然而實際上它的效能卻未見哈米尼斯您所顧慮的問題。

    認為系統繞太多層是我們的想法,也是一種猜測,如果擔心的話,應該去驗證,這才是軟體開發者應有的積極態度。而事實上,系統並不在意我們切多少個 class,我們只是把原來放在一起的程式碼區分好責任,去掉重覆性而已,所以對於系統而言,繞太多層只是對我們的理解而言,而不是對系統的運作,如果真的擔心的話,那就做設計概念驗證吧,讓事實或數據說話,而不是經驗的推論,因為後者往往會有個人徧見出現。

    供參考。

  4. [...] 我想到哈米尼斯對同人分享永續物件設計所提出的意見,他提到: 就現實問題而言,一家公司裡,可能許多套系統都是由不同的廠商開發,若以這樣的角度來看,這一個架構似乎有點問題。 [...]

  5. [...] 上面這段程式用到同人過去分享的「永續物件查詢的設計」來一般化查詢商業物件的方法,讓我們不會受到 ORM 的框架而影響影響 DataAccossor 的操作界面。同人建立一個共通的表述語法稱為 QueryExpression,讓查詢商業物件的實作方式不會因為永續框架的改變而受到影響。 [...]

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

  7. [...] 在五年前,同人曾經發表過〈永續物件查詢之設計〉。當年我分別以其一和其二兩篇分享應用在 Hibernate 框架的動態物件條件查詢的實作,而最近同人在工作上應用 Hibernate-Jpa 又實作了另一套動態條件查詢的元件。這兩者不一樣的是,不像五年前需要定義查詢條件表示式的語法結構,現在我應用的設計樣式更簡單,捨棄了較複雜的 interpreter pattern,而單單利用延遲運算的概念來結合 builder pattern 與 strategy pattern。 [...]

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="">