用 Java 實作語言整合查詢功能

語言整合查詢(LINQ)是非常有用的設計概念,它扭轉我們過去用指令式編程的習慣,轉而以宣告式編程的設計典範,讓程式碼變得簡潔,也提高設計的抽象能力。尤其是對於迴圈當中一再出現類似的條件判斷,卻苦於迴圈的特性讓我們沒有很好的策略來將變動的部分封裝起來,只能任由讓程式碼因應需求改變而日趨複雜,讓開發及維護程式愈來愈困難的時候。但如果我們能夠把指令式編程的開發慣例換成宣告式編程的思維模式,我們就會發現問題將變得很簡單。

如同 .NET Framework 支援 LINQ 的查詢語句可以抽換查詢的條件式(透過 where() 方法)和查詢結果的對應(透過 select()、groupBy()等方法),其實在 Java 1.6 我們也可以用相同的設計概念實作語言整合功能。本來同人一直認為實作它應該會很複雜,但實際動手後來發現,實作它比想像中的來的簡單,也只是運用延遲運算的觀念和一些設計樣式的手法就能夠很輕易的實現了。

我們先來看如何使用語言整合查詢,同人把實作的目標定在集合的查詢,也就是 CollectionQuery,使用它的測試案例如下所示:

public class CollectionQueryTest {

    private List<MyObj> myObjList = Arrays.asList(new MyObj[]{
        new MyObj(123, "a", "abc"),
        new MyObj(246, "a", "ace"),
        new MyObj(369, "b", "bbb"),
        new MyObj(456, "x", "xyz")
    });

    @Test
    public void testSelect() {
        List<MyResult> result = CollectionQuery.from(myObjList)
                .where(new Expression<MyObj, Boolean>() {
                    @Override
                    public Boolean evaluate(MyObj myObj) {
                        return !"x".equals(myObj.getKind());
                    }
                })
                .select(new Expression<MyObj, MyResult>() {
                    @Override
                    public MyResult evaluate(MyObj myObj) {
                        MyResult myResult = new MyResult(myObj.getKind() + ":"
                                + myObj.getName());

                        return myResult;
                    }
                })
                .getResult();

        Assert.assertEquals(3, result.size());
        Assert.assertEquals("a:abc", result.get(0).getResultStr());
        Assert.assertEquals("a:ace", result.get(1).getResultStr());
        Assert.assertEquals("b:bbb", result.get(2).getResultStr());
    }

    @Test
    public void testGrouping() {
        List<MyResult> result =
                CollectionQuery.from(myObjList)
                .groupBy(new Expression<MyObj, String>() {
                    @Override
                    public String evaluate(MyObj myObj) {
                        return myObj.getKind();
                    }
                }, new Expression<MyObj, String>() {

                    @Override
                    public String evaluate(MyObj myObj) {
                        return myObj.getName();
                    }
                }
                )
                .select(new Expression<CollectionGrouping<String, String>, MyResult>() {

                    @Override
                    public MyResult evaluate(CollectionGrouping<String, String> myGrouping) {
                        StringBuilder sb = new StringBuilder()
                                .append(myGrouping.getKey());
                        boolean initiated = false;
                        for (Iterator<String> iterator = myGrouping.getElementsIterator();
                                iterator.hasNext();) {
                            if (initiated) {
                                sb.append(',');
                            }
                            else {
                                sb.append(':');
                                initiated = true;
                            }
                            sb.append(iterator.next());
                        }
                        return new MyResult(sb.toString());
                    }

                })
                .getResult();

        Assert.assertEquals(3, result.size());

        Assert.assertEquals("a:abc,ace", result.get(0).getResultStr());
        Assert.assertEquals("b:bbb", result.get(1).getResultStr());
        Assert.assertEquals("x:xyz", result.get(2).getResultStr());
    }
}

測試案例用到的 MyObj 及 MyResult 則如下所示:

class MyObj {

    private int id;

    private String kind;

    private String name;

    MyObj(int id, String kind, String name) {
        this.id = id;
        this.kind = kind;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getKind() {
        return kind;
    }

    public String getName() {
        return name;
    }

}

public class MyResult {

    private String resultStr;

    MyResult(String resultStr) {
        this.resultStr = resultStr;
    }

    public String getResultStr() {
        return resultStr;
    }    

}

要讓測試案例通過,我們要先定義 CollectionQuery 類別,在這邊同人使用 Composite Pattern。因為查詢過程要處理集合中的元素,我是採用延遲運算的作法,也就是會把某種資料類別的元素轉換成一元表達式 Expression<T, R>,其中 T 代表運算式的參數型態,而 R 代表運算結果的型態。可想而知,CollectionQuery 將會一個套一個,也就是我們可能會用 select() 轉換集合內的元素,或是用 groupBy() 歸類集合內的元素;查詢的每次操作中都會複合我們的 CollectionQuery,於是 CollectionQuery 的定義如下所示:

public abstract class CollectionQuery<T> {

    private List<Expression<T, Boolean>> predicates
            = new ArrayList<Expression<T, Boolean>>();

    public static <R> CollectionQuery<R> from(List<R> list) {
        return new ListQuery(list);
    }

    public CollectionQuery<T> where(Expression<T, Boolean> predicate) {
        predicates.add(predicate);
        return this;
    }

    public <R> CollectionQuery<R> select(Expression<T, R> projection) {
        return new FunctorCollectionQuery<T, R>(projection, this);
    }

    public <K, R> CollectionQuery<CollectionGrouping<K, R>> groupBy(
            Expression<T, K> keyProjection, Expression<T, R> valueProjection) {
        return new GroupingCollectionQuery<T, K, R>(keyProjection,
                valueProjection, this);
    }

    protected abstract Iterator<T> getIterator();

    public List<T> getResult() {
        List<T> result = new ArrayList<T>();       

        for (Iterator<T> iterator = getIterator();iterator.hasNext();) {
            T t = iterator.next();
            boolean broken = false;
            for (Expression<T, Boolean> predicate : predicates) {
                if (!predicate.evaluate(t)) {
                    broken = true;
                    break;
                }
            }

            if (!broken) {
                result.add(t);
            }
        }
        return result;
    }

}

最初的 CollectionQuery 是由 CollectionQuery 的靜態 factory method from() 所產生,爾後對 CollectionQuery 進行操作都會傳回一個 CollectionQuery 以利連續查詢操作,如果操作只是像 where() 的篩選資料,那麼我們只是把篩選資料的 predicate 加入整個 predicates 當中,再回傳整個 CollectionQuery 的實體。以 Functional Programming 的觀念來說,回傳的 CollectionQuery 已經不是原來的 CollectionQuery,只是站在實作的考量,我們沒有必要再 new 一個新的 CollectionQuery 實體造成記憶體的浪費。

然而,至於 select() 和 groupBy() 操作,則回傳的 CollectionQuery 則需要建構新的 FunctorCollectionQuery、以及 GroupingCollectionQuery,所以它們必須繼承自 CollectionQuery,並且實作 getIterator() 供 getResult() 循序處理 Collection 的內存資料或表達式。限於文章的篇輻有限,在這邊我只列出較複雜的 GroupingCollectionQuery 之 getIterator() 實作,相信以此類推,更簡單的 FunctorCollectionQuery 的 getIterator() 實作也是一點都不困難的:

    @Override
    protected Iterator<CollectionGrouping<K, E>> getIterator() {
        Map<K, CollectionGrouping<K, E>> groupings =
                new TreeMap<K, CollectionGrouping<K, E>>();

        for (T t : elementsQuery.getResult()) {
            K key = keyProjection.evaluate(t);
            CollectionGrouping<K, E> grouping = groupings.get(key);
            if (null == grouping) {
                grouping = new ListElementsCollectionGrouping<K, E>(key);
                groupings.put(key, grouping);
            }
            E element = elementProjection.evaluate(t);
            grouping.add(element);
        }

        return new ArrayList<CollectionGrouping<K, E>>(groupings.values())
                .iterator();
    }

在這邊還需要交待一下 CollectionGrouping<K, E>,它是用來表示集合分群的界面;其中 K 代表鍵值類別,E 代表群內元素類別。它還提供 accumulate() 供傳入 Accumlator<K, E, R> 來進行累積的操作,CollectionGrouping<K, E> 及 Accumlator<K, E, R> 的定義如下所示:

public interface CollectionGrouping<K, E> {

    public K getKey();

    public Iterator<E> getElementsIterator();

    public void add(E result);

    public long getCount();

    public E max(Comparator<E> comparator);

    public E min(Comparator<E> comparator);

    public <R> R accumulate(GroupingAccumulator<K, E, R> accumulator);

}

public interface GroupingAccumulator<K, E, R> {
    public void accumulate(K key, E element);

    public R getResult();

}

以上,便是同人用 Java 實作語言整合查詢功能的經驗分享。本來我以前都用 Collections 和 CollectionUtils 來處理集合中內存的元素,但它們的功能都極其有限。也許在指令式編程的領域當中,它們已經很夠用了,但在宣告式編程的領域當中,使用它們實在是略感不足。雖然我現在工作上還沒用到 Java 8,但沒有 lambda expression 仍然可以用匿名類別的方式來達到匿名函式的效果,其實也是棒的呀!

Please follow and like us:
分類: 分析設計建模, 編程技巧, 職場, 設計原則, 軟體開發。這篇內容的永久連結

在〈用 Java 實作語言整合查詢功能〉中有 2 則留言

  1. 自動引用通知: 以延遲運算實現流程模組化 « 同人的生活派對

  2. 自動引用通知: 更簡單的條件查詢設計 « 同人的生活派對

發佈留言

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