ラベル NewsPicks の投稿を表示しています。 すべての投稿を表示
ラベル NewsPicks の投稿を表示しています。 すべての投稿を表示

2014年12月16日火曜日

NewsPicks(iOS)の設計思想

NewsPicksチームインターン生の保田です。
主にiOSアプリ開発のお手伝いをしております。
iOSアプリを作っていて悩ましいな、と思うのが、同じ機能を持つがiPadとiPhoneで見た目が違うページをどう設計していくか、ということです。

ちょうど僕が関わった、NewsPicksの特徴的な機能である「ニュース中間ページ」をどのように設計したかについてお届けいたします。



NewsPicks上でニュースをみるとき、上図の左から右の流れで画面を切り替えます。
この中の真ん中の画面(ニュース中間ページ)の設計についてご説明していきます。


Season1. 単一仕様時代

当初は、次のような単純な仕様でした 。
 - 「フォローしているユーザー」セクションで、コメントを表示
 - 「その他のユーザー」セクションで、コメントを表示


ただし、並び順に関してはサーバー側で『時系列順』『Like順』でソートされたものを受信して、そのまま表示していました。

当時は次のようなクラス図になっていました(メソッドは省略)。


ControllerにはiPhone, iPadで共通の処理が双方に書かれていたため、メンテナンス性が低いものになっていました。また、NewsTableViewにはコメントが表示されるのですが、コメントの有無の判定、データの保持などの処理も行っており、ViewとLogicが切り分けられていない典型的なアンチパターンになってしまっていました。


Season2. 人気コメントの誕生

次に、人気ニュース一覧からニュース中間ページに遷移した場合、Likeがたくさん付いているコメントを「人気コメント」というセクションで上位に表示するという仕様が産声を上げました。


仕様を整理すると、下記のようになります。
そこで、以下のような設計に変更しました。


Commentsというクラスを作成し、NewsTableViewから表示するコメントの表示内容、コメントのソートなどを切り離しました。これで、ViewからLogicを少し切り離すことができました。
CommentsWithTrendingCommentsはCommentsを継承した、人気コメントを表示するためのクラスです。NewsTableViewがどうCommentsを生成すれば良いか知っていて、それをControllerが知っているという設計です。実装の詳細を知らないと使えないクラスはイケてません。


Season3. 検索機能の実装と、連載企画のスタート

NewsPicksオリジナル連載企画がはじまり、中間ページに以下のようなページが仲間入りしました。

上のほうに連載名が書いていたり、「記事に登場するユーザー」というカテゴ リが追加されたりしました。
また、検索機能が追加され、コメント検索の結果から遷移すると「ヒットした コメント」というカテゴリを表示することとなりました。
仕様を整理すると、下記になります。
それに伴い大幅な設計変更を行い、以下の様な設計になりました。

大工事です。
5個しか無かったクラスが
20 個近くになりました。緑色のクラスが実際に呼び出されるクラスです。
共通部分は
Abstract に集約し、コメントのロジックは PickerCommentsCategorizeLogic が責務を持 ち 、 データはCategorizedComments で授受されそのまま表示すると正しいセクションが表示されます。
また、
Factory Method パターンを導入したことによって呼び出し側は Abstract など実装の詳細は一切知りません。 しかし、まだ改善の余地があります。『AbstractNewsSummaryController』の 親クラスが iPad, iPhone によって分かれてしまっていて、全く同じ処理が iPhone, iPad のそれぞれに書かれてしまっています。 
Season4. 更なるリファクタリング Season3 で残ってしまった重複コードを削除すべく、リファクタリングを 行いました。iPad だけが継承している TransitionController は、Controller の遷移方法に関する責務を持っていました。これをを Helper として存在さ せ、標準の UIViewController を継承できるようにしました。
こうすることで共通部分を抽象クラスへ追いやることができ、具象クラスの重複コードが撲滅できました。
ここで、season3でFactory Method パターンを導入していたため、呼び出し側では全くコードの変更が必要ありませんでした。GoF さまさまです。
まとめ
NewsPicks では、同じ機能をもつが iPad と iPhone で見た目が違うページを、下記のような戦略でメンテナンス性の高い設計で実装しています。 - View, Logic, Controller を切り分ける
- 具象クラスに点在する共通処理を抽象クラスに追いやる
- Factory Method パターンでインスタンスの生成方法を隠すことにより、後々の変更に強くする
サービスローンチ時に、様々な試行錯誤を行いながら徐々にサービスを成長させる過程で、負債コードをためてしまうのは致し方ないことです。しかし、適切な手順で設計を変更すれば必ずコードは綺麗になります!
NewsPicks では更なるグロースに向けて、一緒に戦ってくれるエンジニアを募集しております!
インターン生でも設計から携わらせていただけるのでとても勉強になります!
興味をお持ちいただいた方は Wantedly などからご応募ください!!


2014年12月8日月曜日

NewsPicks の Chrome 拡張を作った話


こんにちは。NewsPicks の開発を担当している文字(もんじ)です。本日 NewsPicks の Chrome 拡張をリリースしました。


幸いユーザーの皆様にもご好評頂いているようで嬉しいです。ということで今回は NewsPicks の Chrome 拡張を作った話をします。アジェンダは以下の通りです。
  1. NewsPicks の Chrome 拡張が提供する機能について
  2. なぜ Chrome 拡張を作ったのか?
  3. どうやって Chrome 拡張を作ったのか?
  4. まとめ

1. NewsPicks の Chrome 拡張が提供する機能について

Chrome 拡張が提供する機能については NewsPicks のブログをご覧下さい。主に 2 つの機能を提供しています。

現在開いているページを NewsPicks に Pick する機能




アドレスバー(Omnibox)を利用して NewsPicks 内の記事やコメントを検索する機能





2. なぜ Chrome 拡張を作ったのか?

NewsPicks は基本的にスマホファーストな方針で開発しており、Web 版は今年の夏にリリースされたばかりです。しかしヘビーユーザー ── 特に NewsPicks がターゲットとしているビジネスマン ── は、スマホ以外から NewsPicks を利用することも多いと考えられます。実際に 25 % のユーザーが Chrome から NewsPicks にアクセスしていますし、そもそも会社でスマホを使ってニュースを見ていたら印象が悪いでしょう。

また、過去記事を参照しながら長文のコメントを書いて下さるユーザーは、スマホではなく PC から書きたいと考えている方が多いとも感じていました。私自身も NewsPicks でコメントを書く場合は、スマホではなく PC を利用することが多いため、業務の隙間時間(ビルド時間)や休日の隙間時間に業務外で Chrome 拡張を開発することにしました。


3. どうやって Chrome 拡張を作ったのか?

さて本題の開発方法ですが、それほど特別なことはしていません。Chrome 拡張は必要なツールキットも揃っており、JavaScript と HTML で書けるため、実開発工数は 10-20 時間程度だと思います(気楽に作ることが出来るのが HTML + JavaScript の良いところですね)。ただ、私自身は Chrome 拡張を作ったことが無かったため、以下の順に調査をしました。
  1. Google のドキュメントを読む
  2. 今回開発しようとしている拡張機能に似た Extension のソースコードを読む
1. については、Google の API ドキュメントは良く整備されており、必要最低限の情報は十分に記載されていると感じました。ただ幾つか実際の使用例を見たかったので、2. のソースコードで知識を補完しました。ここでは具体的には以下を参考にしました。
前者は主に Pick 機能(ポップアップ)の実装について、後者はポップアップを常駐型のサイドバーにしようとしたときに読み込みました(Omnibox を操作する拡張のソースコードも幾つか読んだのですが、失念していまいました)。

ちなみにポップアップではなく常駐型のサイドバーにするのは途中で断念しました。理由は幾つかあるのですが、サイドバーを実装しようとすると content script もしくは executeScript と insertCSS によって表示しているページにスクリプトと CSS をインジェクトする必要があるのですが、後者が表示しているページの CSS とコンフリクトするためです。今回は手間を省くために CSS Framework を利用したかったのと、すべてのウェブページに対応しようと考えていたため、コンフリクトを回避するのはコストが高くつきそうだなぁと判断して取りやめた次第です。また、その他の理由としては、後述する Yeoman の generator との相性が悪かったというのもあります。JS ファイル内で依存スクリプト / CSS をインジェクトするため、grunt-usemin を使った Yeoman のビルドフローに乗せるのが面倒でした。

さて、幾つか下準備をしたあと、具体的な開発に入りました。最初は自分でちまちまビルドスクリプトを書こうと思っていたのですが、丁度 Yeoman の generator があることに気付いたので、こちらを使うことにしました。この generator を使うと extension のパッケージングまで含めたビルドフロー全般、また livereload を使ったデバッグ環境まで一式整えてくれるため、Chrome 拡張の開発に慣れていない人にとってはなかなか便利だと思います。ディレクトリ一式も良い感じに作ってくれるので、今回の拡張ではこの generator を使ってこんな感じの構成にしています。


具体的なソースコードについては特に複雑なことはしておらず、NewsPicks のサーバーが提供する REST API を叩いているだけです。あえて工夫した点を挙げると以下になるでしょうか。

まず popup については MVC で開発しました。今回はそれほど複雑な画面ではないので Backbone を利用していますが、フォームのモデルとのバインディングだけ面倒だったので backbone.stickit を利用しています。

backbone.stickit は Backbone にバインディング機能を提供してくれるライブラリです。これを Marionette と組み合わせて使う場合は、次のような Behavior を定義しておくと便利です。

class StickitBindingBehavior extends Backbone.Marionette.Behavior

  createBindings: ($root, attr="name", ignores={}) ->
    bindings = Backbone.$.extend true, {}, @options.bindings
    $root.find("[#{attr}]").each ->
      $el = $(@)
      attribute = $el.attr attr
      return if bindings[attribute]
      for ignore in ignores
        if _.isString ignore
          return if ignore is attribute
        else if _.isObject ignore
          return if ignore.test and ignore.test attribute
      selector = "[#{attr}='#{attribute}']"
      tag = $el.prop("tagName").toLowerCase()
      key = "#{tag}#{selector}"
      return if bindings[key]
      bindings[key] = "observe": attribute
    bindings

  onRender: ->
    @view.bindings = @createBindings @$el, @options.attr, @options.ignores
    @view.stickit()

  onDestroy: ->
    @view.unstickit()

Backbone.Marionette.Behaviors.behaviorsLookup = ->
  stickit: StickitBindingBehavior

これを使うと基本的には何も書かなくてもそれっぽくバインドしてくれるようになります。

class FormView extends Backbone.Marionette.ItemView

  template: "#form"
  behaviors:
    stickit: {}

ちなみに Backbone.Marionette + backbone.stickit + Browserify を使ったテンプレートを以前自分で作って github に公開しているので、良ければこちらをご参照下さい。
→ backbone.marionette.example

Omnibox については Chrome の extension と API を組み合わせているだけですが、操作感については「.」を打つことでページを切り替えられるような UI にしました。また、Chrome の Omnibox は一度に表示出来る候補リストのサイズが少ないため、サーバーの負荷を減らすためにサーバー側から取得した検索結果のバッファをページングし、終端まで辿り着いた段階で API を叩いてリモートから次のページを取得するようにしています。また、当然ですがキーボード入力は適当に間引いて負荷を減らしています。


4. まとめ

以上が今回開発した内容の概要になります。
Chrome 拡張の開発についてのまとめです。

  • Chrome の API ドキュメントは良く整備されており、Chrome extension のソースコードも GitHub などに沢山公開されているため、キャッチアップは比較的容易(但し GitHub に公開されている extension のコードは玉石混淆なため、安易に引用するのはオススメしません)
  • Yeoman の generator を使うと開発環境は何も考えずにセットアップできる
  • popup や omnibox の実装は、それほど Chrome 固有の知識を要求されるものではなく、HTML と JavaScript の知識があれば十分

「Chrome 拡張の開発」と言うとなんとなく敷居が高く感じてしまいますが、それほど難しい概念があるわけでもないので、皆さんも気楽に開発してみると楽しいのではないでしょうか。


NewsPicks では一緒に開発してくれるエンジニアを募集しています!様々なバックグラウンドを持つエンジニアや編集部の皆と世界一の経済メディアをつくりましょう!興味の在る方は是非 Wantedly などでお気軽にオフィスまでお越し下さい!


2014年11月18日火曜日

NewsPicks × D3.js


NewsPicksの開発をしている板倉です。


NewsPicksではニュースを見る画面とは別に、
どの記事がどれくらい読まれているかという画面の開発を進めています。
直感的にわかる画面がほしいということで、 D3.jsを使って画面を開発することになりました。
D3.jsを使うにあたって勉強するつもりで何か作ろうと思い書いたのが今回のエントリーになります。

今回の開発環境

Mac OS X(10.10)
D3.js(3.4.13)

D3.jsについて

まずは、D3について少しだけ。
Githubの人気リポジトリに入っている人気のJavaScriptライブラリです。(2014/11 時点)


APIリファレンスを見ると、たくさんの機能があるのがわかります。

地図を書いてみよう

APIリファレンスを見ていて気になったのがGeography。
ということで日本地図を書いてみました。




参考にしたサイト

地図上に何か表示してみよう


地図を描いただけだと面白くないので、地震のデータを使って地図上に表示してみました。
まずは、データの取得。
以下のサイトで日本の緯度経度、地震の大きさと期間を入力してデータをダウンロードしました。

USGS


ダウンロードしたデータを0.1秒ずつずらして描画してみました。
円をそのままにしておくと画面が円だらけになるので、描画して2秒後に消しています。

d3.csv('geo/eq.csv', function(d) {
    // データが降順だったので反転
    d = d.reverse();
    // 大きさ
    var rScale = d3.scale.linear().domain(d3.extent(d, function(e){
         return e.mag;
    })).range([3, 81]);
    // 深さ
    var colorScale = d3.scale.linear().domain(d3.extent(d, function(e){
         return e.depth;
    })).range(['yellow', 'red']);
    g.selectAll('circle')
         .data(d)
         .enter().append('circle')
         .attr('fill', function(d) { 
              return colorScale(d.depth);
         })
         .attr('fill-opacity', 0.5)
         .attr('stroke-width', '1')
         .attr('stroke', function(d) { 
              return colorScale(d.depth);
         })
         .attr('cx', function(d){
             return projection([Number(d.longitude), Number(d.latitude)])[0];
         })
         .attr('cy', function(d){
             return projection([Number(d.longitude), Number(d.latitude)])[1];
         })
         .transition().delay(function(d, i) { 
              return i * 100;
         })
        .attr('r', function(d) {
             return rScale(d.mag)
         })
        .each('start', function(d){
             dateText.text(df(new Date(d.time)));
             mag.text('M ' + d.mag);
             depth.text('Depth: ' + d.depth + ' km');
         })
        .transition().delay(function(d, i) {
             return (i * 100) + 2000
         })
        .each('end', function(d){
             d3.select(this).remove();
         });
})


実装したものはこちら。

数字が並んでるデータを見るだけだと気付きにくいことも
図形で表現してみると新しい発見がありますね。

Uzabaseではデータの視覚化を進めるエンジニアを募集しております。
興味をお持ちいただいた方はWantedlyなどからご連絡ください!

2014年11月5日水曜日

荒ぶるRedisとNewsPicks


NewsPicks の開発を担当している杉浦です。

NewsPicksはおかげさまでユーザ数が20万を突破しました。
サービスが順調に成長するということは大変にうれしいことなのですが、エンジニアとしては負荷との戦いになったりします。我々も例に漏れず日々、負荷との戦いを強いられています。


NewsPicksの機能面の特長として次の2つがあります。
・フォローしているユーザのPickが自分のタイムラインに集約される
・各カテゴリで話題になった記事を閲覧できる

これらの機能を高速に処理・実現するためにRedisを採用しているのですが、
ユーザ数の増加による負荷増加によって問題が発生するようになりました。

本記事では、
・ユーザ数が増える中でRedisにどのような問題が発生したか
・ソースコードを読みながら問題の原因を考える
・そして、どのような対応を行っているか
を共有したいと思います。

どのような問題が発生したか

主に2つの問題が発生しました。

ピーク時のレスポンスタイムが遅くなる

NewsPicksは朝の8時にその日の注目ニュースをPush通知で送信します。その直後が最大のピークタイムになり、平常時の10倍程度のリクエストを処理する必要があります。ピークタイムに画面表示のレスポンスタイムが悪化するという問題が発生しました。

夜間バッチ実行時にslaveとのレプリケーションが切れる

夜間にRedisに保持している古いタイムラインデータを削除するというバッチが実行されるのですが、バッチによる大量のデータ更新が発生しレプリケーションのタイムアウト値を越えてしまい、レプリケーションが切れるという障害が発生しました。


ソースコードを読みながら問題の原因を考える

なぜこのような問題が発生するようになったのでしょうか。
原因を知るにはRedisのアーキテクチャを理解する必要があります。そして、ソフトウェアのアーキテクチャを理解する近道はいつの時代もソースコードを読むことです。ということで、redis-2.8.17をダウンロード して読んでみました。


ファイル数が少なくシンプルなソフトウェアということがわかります。
理解をしやすくするために、おおまかな機能ごとに整理します。


メインプログラムの redis.c から ae.c を読み進めると、RedisはI/O戦略としてイベントループモデルを採用していることがわかります。
イベントの発火時に各コマンドハンドラーが呼ばれて処理が進められています。

イベントループの詳細については TheC10kProblem を参照してください。
2006年の古い記事ですがよくまとまっていて、今読んでも本質的な部分は変わっていません。

イベントループモデルのメリットは以下が挙げられます。
  • 処理ごとにスレッド/プロセスを用意しないので使用メモリ量を抑えられる
  • スレッド/プロセス切り替えのコンテキストスイッチのオーバヘッドが少ない
  • ノンブロッキングI/Oを合わせて採用することで、I/O待ち時間に他の処理が行われる
逆に、デメリットは以下のようなものがあります。
  • ループの中に遅い処理が入ると、後続の処理が遅れる
  • (Redisの場合は) 1スレッドでループ処理を行うので、マルチコアのサーバの場合に全てのCPUリソースを使い切らない

Redisはデータを全てメモリ中に持つため、高速に処理を行いますが、
それでもRedisの利用度が上がってくると、Redisの能力を越える多量の読み書きが実行される時がやってきます。
そして、Redisはイベントループモデルの特性から、閾値を越えると一気に処理遅延が発生します。

NewsPicksが遭遇した問題はまさにこの事象で、あるタイミングを境に突然問題が発生するようになりました。
システムのアーキテクチャを考える際に負荷に合わせてタイミング良くスケールアウトしていくことができる仕組みを作ることが大切です。
なお、Nginx、Node.jsも同様にイベントループを採用しているので、同じ考え方を適用できると思います。

NewsPicksではどのような対応を行っているか

現在、データのシャーディングと、垂直・水平に処理リクエストを分散をするように対応を進めています。
タイムラインデータについては、一定のユーザ数毎にデータ保存するサーバを分離しています。一般的にユーザパーティショニングによる垂直分散と呼ばれる手法です。
ランキングデータについては読み取りリクエストをslaveに分散させるといった水平分散の対応を行っています。


NewsPicksでは一緒に負荷と戦ってくれるエンジニアを募集しています!
サービスの成長を肌で感じられるやりがいのある仕事です(笑
興味をお持ちいただいた方はWantedlyなどからご連絡ください!