4月初在北京的時候,徐昊同學表示我們公司的同事們寫的文章都太簡單,太注重細節,然後撿起了芝麻丟了西瓜,於是我就不再更新部落格(其實根本原因是專案太忙)。上週和其他幾位同事一起參加「Martin Fowler深圳行」的活動,我和同事扎西貢獻了一個《FullStack Language JavaScript》,一起的還有楊雲(江湖人稱大魔頭)的話題是《掌握函數式編程,控制系統複雜度》,李新(江湖人稱新爺)的話題是《並發:前生來世》。
和其他同事預演的時候,突然發現其實我們的主題或多或少都有些關聯,我講的部分也涉及到了基於事件的並發機制和函數式程式設計。仔細想想,應該與JavaScript本身的特性不無關:
基於事件(Event-Based)的Node.js的正是並發中很典型的一個模型
函數式程式設計使其天然支援回調,從而非常適合非同步/事件機制
函數式程式設計特性使其非常適合DSL的寫作
會後的第二天,我在專案程式碼裡忽然想要將一個聚合模型用函數式程式設計的方式重寫一下,結果發現思路竟然與NoSQL依稀有些聯繫,進一步發現自己很多不足。
下面這個例子來自於實際專案中的場景,不過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://www.php.cn/: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://www.php.cn/:9999/user/1/marked-feed-ids
之所有隻獲取id是為了減少網路傳輸的資料大小,當然你也可以將全部的/marked-feeds
都請求到,然後在本地做 _.pluck(feeds, 'id')
來抽取所有的id
屬性。
嗯,代码是精简了许多。但是如果仅仅能做到这一步的话,也没有多大的好处嘛。现在需求又有了变化,我们需要在另一个页面上展示当前用户的收藏夹(用以展示用户所有收藏的feed)。作为程序员,我们可不愿意重新写一套界面,如果能复用同一套逻辑当然最好了。
比如对于上面这个列表,我们已经有了对应的模板:
{{#each feeds}} <li class="list-item"> <p 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}}"> <p class="detail"> <h3>{{this.title}}</h3> </p> </a> </p> </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, 53384f78b45ee9f1e3082cf378b9c5b4)
。只不过这个53384f78b45ee9f1e3082cf378b9c5b4
会在实际迭代的时候才传入:
$('/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})); });
以上是淺談JavaScript函數式程式設計教學(圖)的詳細內容。更多資訊請關注PHP中文網其他相關文章!