jim yeh on 一月 31st, 2015

語言整合查詢(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 仍然可以用匿名類別的方式來達到匿名函式的效果,其實也是棒的呀!



     

2 Responses to “用 Java 實作語言整合查詢功能”

  1. [...] 從以上以延遲運算實現流程模組化讓我們看到,這個設計樣式和同人先前分享過的語言整合查詢有一些不一樣的地方。語言整合查詢是運用表示式的套疊運算的原理;而這個設計則是引用 Context 的概念,讓各個流程模組元件可以存取並分享處理過程中的資料變化,並且運用 Exception Handler 的機制,來達到流程控制的一致性。我們發現 Context 的本質正是 GOF 的建造者樣式,有別於以 Value Object 組合回傳結果的的方式,採用 Builder 維持我們的抽象概念,也不會違反 Value Object 應該是不變物件的原則。       Posted by jim yeh 分析設計建模, 編程技巧, 設計原則 Subscribe to RSS feed [...]

  2. [...] 對其它我們會碰到沒那麼複雜的情況,同人的經驗顯示有二種不同解決方式:第一種就是上一篇流程元件化提到的應用 builder pattern 或是更早提到訊息拆解組合應用 visitor pattern,建立一個解決問題過程的 context 脈絡,把答案組合出來,有時候問題比較簡單時,也可能只需要像本篇文章提到只需要應用 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="">