前言
4月初在北京的時候,徐昊同學表示我們公司的同事們寫的文章都太簡單,太注重細節,然後撿起了芝麻丟了西瓜,於是我就不再更新部落格(其實根本原因是專案太忙)。上週和其他幾個同事一起參加“Martin Fowler深圳行”的活動,我和同事扎西貢獻了一個《FullStack Language JavaScript》,一起的還有楊雲(江湖人稱大魔頭)的話題是《掌握函數語言程式設計,控制系統複雜度》,李新(江湖人稱新爺)的話題是《併發:前生來世》。
和其他同事預演的時候,突然發現其實我們的主題或多或少都有些關聯,我講的部分也涉及到了基於事件的併發機制和函數語言程式設計。仔細想想,應該與JavaScript本身的特性不無關係:
- 基於事件(Event-Based)的Node.js的正是併發中很典型的一個模型
- 函數語言程式設計使其天然支援回撥,從而非常適合非同步/事件機制
- 函數語言程式設計特性使其非常適合DSL的編寫
下面這個例子來自於實際專案中的場景,不過Domain做了切換,但是絲毫不影響閱讀和理解背後的機制。
一個書籤應用
設想有這樣一個應用:使用者可以看到一個訂閱的RSS的列表。列表中的每一項(稱為一個Feed),包含一個id,一個文章的標題title和一個文章的連結url。
資料模型看起來是這樣的:
var feeds = [ { 'id': 1, 'url': 'http://abruzzi.github.com/2015/03/list-comprehension-in-python/', 'title': 'Python中的 list comprehension 以及 generator' }, { 'id': 2, 'url': 'http://abruzzi.github.com/2015/03/build-monitor-script-based-on-inotify/', 'title': '使用inotify/fswatch構建自動監控指令碼' }, { 'id': 3, 'url': 'http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/', 'title': '使用underscore.js構建前端應用' } ];當這個簡單應用沒有任何使用者相關的資訊時,模型非常簡單。但是很快,應用需要從單機版擴充套件到Web版,也就是說,我們引入了使用者的概念。每個使用者都能看到一個這樣的列表。另外,使用者還可以收藏Feed。當然,收藏之後,使用者還可以檢視收藏的Feed列表。
由於每個使用者可以收藏多個Feed,而每個Feed也可以被多個使用者收藏,因此它們之間的多對多關係如上圖所示。可能你還會想到諸如:
$ curl http://localhost:9999/user/1/feeds來獲取使用者1的所有feed等,但是這些都不重要,真正的問題是,當你拿到了所有Feed之後,在UI上,需要為每個Feed填加一個屬性makred。這個屬性用來標示該feed是否已經被收藏了。對應到介面上,可能是一枚黃色的星星,或者一個紅色的心。
伺服器端聚合
由於關係型資料庫的限制,你需要在伺服器端做一次聚合,比如將feed物件包裝一下,生成一個FeedWrapper之類的物件:
public class FeedWrapper { private Feed feed; private boolean marked; public boolean isMarked() { return marked; } public void setMarked(boolean marked) { this.marked = marked; } public FeedWrapper(Feed feed, boolean marked) { this.feed = feed; this.marked = marked; } }然後定義一個FeedService之類的服務物件:
public ArrayList<FeedWrapper> wrapFeed(List<Feed> markedFeeds, List<Feed> feeds) { return newArrayList(transform(feeds, new Function<Feed, FeedWrapper>() { @Override public FeedWrapper apply(Feed feed) { if (markedFeeds.contains(feed)) { return new FeedWrapper(feed, true); } else { return new FeedWrapper(feed, false); } } })); }好吧,這也算是一個還湊合的實現,但是靜態強型別的Java做這個事兒有點勉強,而且一旦發生新的變化(幾乎肯定會發生),我們還是把這部分邏輯放在JavaScript中,來看看它是如何簡化這一個過程的。
客戶端聚合
快要說到主題了,這篇文章我們會使用lodash作為函數語言程式設計的庫來簡化程式碼的編寫。由於JavaScript是一個動態弱型別的語言,我們可以隨時為一個物件新增屬性,這樣一個簡單的map操作就可以完成上邊的Java對應的程式碼了:
_.map(feeds, function(item) { return _.extend(item, {marked: isMarked(item.id)}); });其中函式isMarked會做這樣一件事兒:
var userMarkedIds = [1, 2]; function isMarked(id) { return _.includes(userMarkedIds, id); }即檢視傳入的引數是否在一個列表userMarkedIds,這個列表可能由下列的請求來獲得:
$ curl http://localhost:9999/user/1/marked-feed-ids之所有隻獲取id是為了減少網路傳輸的資料大小,當然你也可以將全部的/marked-feeds都請求到,然後在本地做_.pluck(feeds, 'id')來抽取所有的id屬性。
嗯,程式碼是精簡了許多。但是如果僅僅能做到這一步的話,也沒有多大的好處嘛。現在需求又有了變化,我們需要在另一個頁面上展示當前使用者的收藏夾(用以展示使用者所有收藏的feed)。作為程式設計師,我們可不願意重新寫一套介面,如果能複用同一套邏輯當然最好了。
比如對於上面這個列表,我們已經有了對應的模板:
{{#each feeds}} <li class="list-item"> <div class="section" data-feed-id="{{this.id}}"> {{#if this.marked}} <span class="marked icon-favorite"></span> {{else}} <span class="unmarked icon-favorite"></span> {{/if}} <a href="/feeds/{{this.url}}"> <div class="detail"> <h3>{{this.title}}</h3> </div> </a> </div> </li> {{/each}}事實上,這段程式碼在收藏夾頁面上完全可以複用,我們只需要把所有的marked屬性都設定為true就行了!簡單,很快我們就可以寫出對應的程式碼:
_.map(feeds, function(item) { return _.extend(item, {marked: true}); });漂亮!而且重要的是,它還可以如正常工作!但是作為程式設計師,你很快就發現了兩處程式碼的相似性:
_.map(feeds, function(item) { return _.extend(item, {marked: isMarked(item.id)}); }); _.map(feeds, function(item) { return _.extend(item, {marked: true}); });消除重複是一個有追求的程式設計師的基本素養,不過要消除這兩處貌似有點困難:位於marked:後邊的,一個是函式呼叫,另一個是值!如果要簡化,我們不得不做一個匿名函式,然後以回撥的方式來簡化:
function wrapFeeds(feeds, predicate) { return _.map(feeds, function(item) { return _.extend(item, {marked: predicate(item.id)}); }); }對於feed列表,我們要呼叫:
wrapFeeds(feeds, isMarked);而對於收藏夾,則需要傳入一個匿名函式:
wrapFeeds(feeds, function(item) {return true});在lodash中,這樣的匿名函式可以用_.wrap來簡化:
wrapFeeds(feeds, _.wrap(true));好了,目前來看,簡化的還不錯,程式碼縮減了,而且也好讀了一些(當然前提是你已經熟悉了函數語言程式設計的讀法)。
更進一步
如果仔細審視isMarked函式,會發現它對外部的依賴不是很漂亮(而且這個外部依賴是從網路非同步請求來的),也就是說,我們需要在請求到markedIds的地方才能定義isMarked函式,這樣就把函式定義繫結到了一個固定的地方,如果該函式的邏輯比較複雜,那麼勢必會影響程式碼的可維護性(或者更糟糕的是,多出維護)。
要將這部分程式碼隔離出去,我們需要將ids作為引數傳遞出去,並得到一個可以當做謂詞(判斷一個id是否在列表中的謂詞)的函式。
簡而言之,我們需要:
var predicate = createFunc(ids); wrapFeeds(feeds, predicate);這裡的createFunc函式接受一個列表作為引數,並返回了一個謂詞函式。而這個謂詞函式就是上邊說的isMarked。這個神奇的過程被稱為柯里化currying,或者偏函式partial。在lodash中,這個很容易實現:
function isMarkedIn(ids) { return _.partial(_.includes, ids); }這個函式會將ids儲存起來,當被呼叫時,它會被展開為:_.includes(ids, <id>)。只不過這個<id>會在實際迭代的時候才傳入:
$('/marked-feed-ids').done(function(ids) { var wrappedFeeds = wrapFeeds(feeds, isMarkedIn(ids)); console.log(wrappedFeeds); });這樣我們的程式碼就被簡化成了:
$('/marked-feed-ids').done(function(ids) { var wrappedFeeds = wrapFeeds(feeds, isMarkedIn(ids)); var markedFeeds = wrapFeeds(feeds, _.wrap(true)); allFeedList.html(template({feeds: wrappedFeeds})); markedFeedList.html(template({feeds: markedFeeds})); });
來自:碼農網
評論(1)