Subscribed unsubscribe Subscribe Subscribe

Hatena Developer Blog

はてな開発者ブログ

GitHubのラベルの変更を通知するreviewkunの紹介 〜Google Apps Script でちょっとしたツールを作る〜

※ GitHub と Slack を利用している人向けの記事になります

こんにちは。はてなでアプリケーションエンジニアをしている id:hatz48 です。 社内で使っているちょっとしたツールの紹介と、それを作るのに Google App Script を利用したら楽だったという話をしようと思います。

どんなツールですか

f:id:hatz48:20170323163324p:plain

github.com

はてなでは現在、コードの管理に GitHub/GitHub Enterprise、コミュニケーションツールに Slack を利用しています。 GitHub と Slack を利用していれば、おそらくその二つの連携機能 も利用しているのではないかと思います。連携はとても便利ですが、現状すべての活動を Slack に流すことが出来るわけではありません。出来ないことのうちの一つが「ラベル変更」の通知です。

はてなではいくつかのチームで、ラベルによるレビューフローの管理をしています。 レビューしてもらいたい状態になったら レビュー依頼、レビューしはじめたら レビュー中 といったラベルを付けて、レビューしてほしいものが放置されてしまったり、複数人で同じ P-R をレビューしていたりということがないようにしています(後者に関しては、別に複数人でレビューすること自体はよいので、目安程度なのですが) レビュー依頼 ラベルをつけたら Slack に通知されてくると便利!なのですが上述の通り GitHub-Slack 連携ではラベルの変更を通知することができません。 これはさほど大きな問題ではなく、 GitHub の webhook と Slack API を使えばラベルの変更を通知することは出来ます。

ただ「その実装をどこに置くのか」がやや面倒ですよね。 本当に「ちょっとしたツール」という感じです。「この為にわざわざサーバーを立てるのか」「複数のチームで利用したいけれどどこがサーバーの管理をするの」という億劫さがあります。そこで元祖サーバーレスアーキテクチャであるところの Google App Script でお手軽 webhook サーバーを構築することにしました。

GitHub webhook --> Google Apps Script(web application として公開) --> Slack

Google Apps Script は書いたコードを「web application として公開」することが出来ます。「web application として公開」すると url が付与され、その url への GET アクセスは doGet メソッドで、POST メソッドは doPost メソッドでハンドルすることが出来ます。Github の webhook にこの url を指定しすれば、doGet メソッドでハンドルすることが出来ます。今回は webhook で飛んできた情報をもとに「どのチャンネルに」「何を」通知するかを計算して通知するようにしました。

使い方

簡単なスクリプトですが GitHub に公開してみたので紹介します。

構築編

  • リポジトリ を clone
  • コードをビルド
    • npm install して npm run build
  • Gooleスプレッドシートの用意
    • 「どのチャンネルに」「何を」通知するかの情報を格納するスプレッドシートを用意します
    • これ をコピーする
  • Google Apps Script にコードアップロード & プロパティ入力
    • Google Apps Script を作成して、ビルドしたコード(reviewkun.js)をコピペする
    • ファイル->プロジェクトのプロパティ->スクリプトのプロパティで
      • SLACK_API_URL というキーで Slack API url を記入
      • spreadsheet_id というキーで上記のスプレッドシートの ID を記入
  • web application として公開する
    • 公開->ウェブアプリケーションとして導入 からスクリプトを公開します
    • 公開するときに、アクセス出来るユーザーの指定を「全員(匿名ユーザーを含む)」にするのを忘れないようにして下さい
      • つまり、公開 url の取り扱いはお気をつけて
    • ここで取得できる url をあとで webhook に指定します

実際に動かす

  • スプレッドシートにリポジトリと通知先チャンネルを記入
    • repository と channel の指定があれば動きます
    • label condition で指定した正規表現にマッチしたラベルがついたときに通知されます。デフォルトは レビュー です
  • Github/Github Enterprise の webhook の設定

一旦構築してしまえばリポジトリや通知先チャンネルの追加などが「Spreadsheet の行を増やして Github で webhook を設定」するだけで出来ます。 利用側のハードルの低さもあってか、社内では今は 30 近いリポジトリ、ほぼすべてのチームが reviewkun を利用しています。

まとめ

とくにすごいことをやっているわけではないですが、Google Apps Script をサーバーレスアーキテクチャとして用いることでちょっとしたツールを煩わしくなく使うことが出来ています。なんでもかんでもサーバーレスで、という訳ではないですが適材適所で使う技術を選択していけると良いですね。

はてなでは課題解決のために最適な技術選択できるエンジニアを募集しています。

hatenacorp.jp

新機能がサービスのパフォーマンスに悪影響を与えないか素早く推定する

 こんにちは、 id:shiba_yu36 です。

 先日、新しい機能や改善を加えようとする時に、それがデータベースに対して悪影響を及ぼさないか、どのように検証すれば良いですかという相談を受けました。つまり、新しく作った機能を導入した瞬間にデータベースが高負荷になりサービス全体に影響を与えたりしないか不安であるという相談です。その相談に対して僕は、簡単な計算式を作り、その機能のデータベースへの影響度をざっくり推定することで、リリース前に時間をかけずにパフォーマンスに悪影響を与えないか推定できるという話をしました。

 この話をしていて、「リリースする前に新機能や改善がサービスのパフォーマンスに悪影響を与えないか素早く推定する」ことは、経験のあるエンジニアは自然とやっているけれど、あまりブログなどで言語化されていないと感じました。そこで、今回はこのことについてブログに書いてみます。


なぜ機能をリリースする前にパフォーマンスに悪影響がないか考える必要があるか

 サービスの新機能を作ったり、何かしらの改善を加えたりした場合、リリース前にある程度それがサービス全体のパフォーマンスに悪影響を与えないか検討すべきです。なぜなら、何も考えずにパフォーマンスに悪影響を与える機能を投入してしまった場合、その機能の影響でサービスの全体のレスポンススピードが悪化したり、最悪の場合サービス自体が落ちるなどといったことが起こりうるためです。

 パフォーマンスに悪影響を与える原因はいろいろあると思いますが、例えばMySQLなどのデータベースで考えるなら

  • 非常に重いクエリを投げてしまっていて、DBが詰まり、他の機能のレスポンスすら返せなくなった
  • クエリ単体は軽かったけど、あまりにも多くクエリを投げすぎていて、DBが詰まってしまった
  • 転送量が多すぎるクエリを投げてしまっていて、ネットワークで詰まってしまった

などといったことが起こらないように、事前に検証しておく必要があります。



ざっくりとパフォーマンスへの影響度を推定する

 上記したとおり、機能リリース前にサービスのパフォーマンスに影響を与えないか検証する必要があります。かといって、はじめから詳細な検証をしようとしてしまい、時間がかかりすぎても問題です。

 そこで僕はパフォーマンスに悪影響がないか心配な機能を作ったときは、詳細な検証に移る前にざっくりとパフォーマンスへの影響度を推定し、そこで問題がありそうと判明したときのみ詳細に調査するということをしています。ざっくり推定までで問題が起こらないということが分かれば、時間をかけずに検証を終えることが出来ます。

 おおよその影響度の推定は以下のように行っています。

  • 影響度を要素分解し計算式を作り、ざっくりした数字を計算する
  • 現状の数字と比較し、影響がどの程度か確かめる

影響度を要素分解し計算式を作り、ざっくりした数字を計算する

 パフォーマンスへの影響度を推定するときにまずやらなければならないのは、その機能によってどの程度数字が増えるかを計算することです。影響度を要素分解し計算式を作り、実際の数値を当てはめれば、ある程度の増分が把握できます。

 例えば「新機能のDBへの影響度」を知りたい時、要素分解し、以下のような計算式を作っています。

 まず新機能のDBへの影響度というのは、つまりその機能のエンドポイント全ての影響度を合計したものと考えます。エンドポイントとは単なるページの表示や、何かしらの更新処理など、その機能を作るための処理全てです。

 次にそれぞれのエンドポイントごとのDBへの影響度を考えます。それぞれのエンドポイントごとの影響というのは、1秒あたりのエンドポイントのアクセス数 * 1アクセスあたりのDBへの影響度です。ここで1秒あたりとしたのは、パフォーマンス測定をする時は秒単位で考えることが多いためです。

 次に1アクセスあたりのDBへの影響度とは何かを考えます。1アクセスあたりのDBへの影響度は、1アクセスでの「それぞれのクエリの影響度」 * 「そのクエリの実行数」を合計したものと考えられます。

 最後に1クエリの影響度ですが、これはEXPLAINを利用したり、実際に実行してみたり、転送量を計算したりして、気になることを調査すると分かるでしょう。


 ここまで分解すれば、アクセス数をアクセスログから計算したり、転送量を計算したりすることで、新機能をリリースすると

  • インデックスが効かず、200msほど時間がかかるクエリが2query/sほど増えそう
  • SELECTのクエリ数が30query/s程度増えそう
  • 転送量が30KB/sほど増えそう

などといったことが計算できるようになります。

現状の数字と比較し、影響がどの程度か確かめる

 新機能をリリースした時の増分が計算できれば、あとは現状の数字と比較すれば影響の大きさが把握できます。例えば「SELECTのクエリ数が30query/s程度増えそう」の場合

  • 元々30000query/s捌いていたのであれば、0.1%増くらいなら問題は少なそう
  • 元々300query/s程度であったのなら、10%増でもしかしたらまずいかもしれない

のように影響の大きさを推定することが出来ます。

 ここまででどう考えても問題が起こらないであろうと判断できれば、詳細な検証をせずにリリースしても良いという判断が出来ます。おおよその推定でこのように判断が出来れば詳細な検証をせずに済むので、素早く検証ができるというわけです。

 逆にここまでで問題が起こる可能性があると判断したら、そこで初めて、詳細な調査や負荷試験を始めれば良いでしょう。

ケーススタディ:新機能のDB転送量を不安に思っているケース

 それでは実際に例を使って考えてみます。

  • 特定のページに表示する要素を増やしたい
  • 表示する要素を増やすためにはDBへのクエリが2つ増える
  • クエリ1は転送量が気になるクエリであり、クエリ2は一瞬で返ってくるようなクエリである

という例を考えてみましょう。この場合心配なのは転送量が多くなりすぎないかです。

大体の転送量を計算する

 今回のケースの場合、エンドポイントは1つだけです。そのため、そのエンドポイントのアクセスでの影響度だけ考えます。

 まず、1秒あたりのエンドポイントのアクセス数を考えます。これはアクセスログやGoogle Analyticsなどで調べられるはずです。例えば日間で30万PVだとすると、秒間なら 30万 / (24 * 60 * 60) = 3.5PVほどとわかります。

 次に1アクセスあたりのDBへの影響度を考えます。これは今回のケースなら、「クエリ1のDBへの影響度 * 1 + クエリ2のDBへの影響度 * 1」で計算できるはずです。さらにクエリ2が何の心配もないなら単に「クエリ1のDBへの影響度 * 1」を考えれば良いでしょう。

 最後に「クエリ1のDBへの影響度」、つまりクエリ1の転送量を考えます。例えばDBのデータ定義的に1行30バイト程度で、平均的に3000行くらい取得するクエリになりそうと考えると、転送量は30 * 3000 = 90KBとなります。


 あとはこのデータを計算式に入れます。

  • 1アクセスあたりクエリの転送量 = クエリ1の転送量 + クエリ2の転送量 ≒ クエリ1の転送量 = 90KB
  • DBへの影響度 = 3.5PV/s * 90KB = 315KB/s

となり、今回の機能改善での影響は315KB/sの増加であるとわかりました。

現状と比べ、影響度を推定する

あとは現状と比べるだけです。

もし現状の転送量が1MB/s程度であれば、今回の機能により30%ほど転送量が増えてしまいます。直感的には影響度は非常に大きいでしょう。この場合、詳細の検討や、別の方法での実装へ移ります。

もし現状の転送量が300MB/sであれば、今回の増分は0.1%ほどです。その場合まあ問題ないだろうと考え、そのままリリースできます。



コツ: 悪い側に倒して考える

 今回のようにざっくり推定する時に、僕が一点だけ気をつけていることは、悪い側に倒して考えることです。

 例えばアクセス数が5万ほどあるページの改善の影響度を考える時に、もしかしたらそのページが一日10万PVになる日があるかもしれません。他にも平均的には転送量が10KBくらいのクエリだけど、もしかしたら転送量が多いクエリが発行されるページが多めにアクセスされ、実際の平均値はもう少し増えるかもしれません。このような時に問題が起こっても困ります。

 そのため僕は悪めのケースを考えて、そんな悪いケースで考えても問題ないなら問題なかろうという考え方をしています。

 例えば先程の例なら、日間30万PVなら今後の成長も見越してとりあえず日間100万PVとして、転送量90KBより増える可能性がありそうなら転送量120KBくらいにとりあえず増やして計算します。その数値で計算すると1.4MB/s程度増えることがわかります。もしこの悪めで考えた数字で問題がなければ、通常時は全く問題がないと判断できます。



まとめ

この記事ではサービスのパフォーマンスに悪影響がないか素早く推定する方法について、データベースへの平均的な影響度がどの程度かを推定する例で説明しました。しかし、このようにひとまずざっくりとした数字を出して現状と比べるという方法は

  • アクセスが一気に集中した場合にどうなるか
  • アプリケーションサーバーのパフォーマンスが問題ないか

など、データベースに関わらず利用できる方法かなと思います。今回書いてみたやり方が参考になればと思います。また、別の方法で考えているなどあれば教えてください。

hatenacorp.jp

YAPC::Kansai 2017 OSAKA はてな社員の感想エントリ/資料まとめ

こんにちは, はてなアプリケーションエンジニアの id:papix です. Perlは大好きです.
さて, YAPC::HokkaidoからスタートしたYAPC::Japanシリーズの第二弾, YAPC::Kansaiが3月4日に開催されました!

developer.hatenastaff.com

先日, こちらのエントリでも紹介しました通り, 今回のYAPC::Kansaiでははてなから4人のエンジニアが登壇し, 前夜祭でも3人のエンジニアが発表を行いました.

加えて, 今回ははてなの本社がある京都に近い, 大阪での開催ということもあり, はてなからもたくさんのエンジニアがYAPC::Kansaiに参加しました.
もしかすると, 会場や懇親会などではてなの社員と話す機会があった! という方も多かったのではないでしょうか.

ということで, こちらのエントリでは, はてな社員の感想エントリや, 登壇者による発表資料などを紹介したいと思います.

はてな社員の感想エントリ/発表資料など

id:Soudai

soudai.hatenablog.com

id:papix

papix.hatenablog.com

id:astj

astj.hatenablog.com

id:hitode909

blog.sushi.money blog.sushi.money

id:Songmu

www.songmu.jp

id:motemen

motemen.hatenablog.com

採用情報

はてなでは, 勉強会やカンファレンスでのアウトプットに意欲的なエンジニアを募集しています!

hatenacorp.jp

DroidKaigi 2017 で「大規模アプリのリノベーション」の発表を行いました

アプリケーションエンジニアの id:funnelbit(北村) です。先日 DroidKaigi 2017 で「大規模アプリのリノベーション」というタイトルで発表させていただきました。

speakerdeck.com

大まかな内容としましては以下のようなものです。

1. ドメイン知識を得る

まずは現状のアプリケーションの解析からスタートします。実装済みの画面を洗い出し、現状を把握します。各画面や View に正しい名前付けを行います。

2. ライブラリと設計を決定する

次にライブラリ、設計を決定します。はてなブックマークの Android 版では MVVM を採用しています。また Repository, Interactor, ViewModel, View といったレイヤー分けを行っています。この設計につきましては様々な議論があるところかと思います。これが唯一無二のものであると主張するつもりは毛頭ありません。ただし現状では、この設計でうまく開発していくことができています。必要なコードはスライドにすべて書いてありますので、興味のある方はぜひご覧ください。

3. サンプルアプリケーションの作成

設計がある程度確定したら、サンプルアプリケーションを作成します。このアプリケーションは、API を叩いてそれをリスト表示するといった流れを表した、小さなアプリケーションです。時間がなくて発表できませんでしたが、実際はログイン状態によって UI を変更したり、Realm を盛り込んでみるなどの試行錯誤もここで行っていました。

サンプルアプリケーションは github 上で管理します。設計に問題があれば、プルリクエストで修正します。その際、ドキュメントの修正も合わせて行います。

4. 設計のドキュメント化

次に設計をドキュメント化します。いつでも最新の設計情報を見れるように配慮します。こうすることで設計をぶれないものとします。また今後引き継ぎがあったとしてもドキュメントがあれば比較的スムーズに対処できるはずです。

5. 着手

ドキュメント化が完了したら、実際にリファクタリングを進めていきます。作業の流れとしてはレイヤーの最も下の者から順番に進めていきます。つまりまずは Repository を一通り作成し、次に Interactor を、その次に ViewModel を、といった具合です。

コードの置き換えは必ずステップごとに行うようにします。例えば Repository が RxJava2 を使っているが、Repository を利用したいクラスの内部が RxJava1 だった場合、一時的に Repository に RxJava1 用メソッドを生やす事が考えられます。Dagger2 の Component を移行したい場合も同様で、いきなり新 Component で inject することはせずに、新 Component にゲッタを生やし、旧 Component 利用クラスは 新 Component のインスタンスを受け取り、ゲッタ経由で必要なインスタンスを受け取れるようにしておきます。

着手する時はまず見積もりを行うようにし、何時までに完了するのかを明確にします。もしも見積もりをオーバした場合、なぜそうなったのかを分析し、改善して次のイテレーションに進みます。

6. リリース

リリースは1〜2週間毎に行うようにします。問題が発生したときに原因究明をやりやすくするためです。タスクの見積もりもこのリリースサイクルを基準に行われます。

まとめ

こうしてみると、準備に相当な労力を割いている事がお分かりいただけるかと思います。実際に準備にはかなりの時間を掛けており、傍から見ると過剰にも映るかもしれません。しかし大きなアプリケーションに手に入れるには、これぐらいの準備が必要なのだと思います。実際に綿密な準備をしたことによって、スムーズにリファクタリングを進めることができています。

ドイメイン知識獲得や、設計の確定をチーム全員で行ったのも正解でした。みんなで模索した結果なので、誰もが自信を持って進めていくことができます。

リノベーションはまだ道半ばなので、完了するまでこの方針が本当に正しかったのかは分かりませんが、きっとやり遂げられるのではと感じています。そして新しいコードベースをもとにして、今後のアプリの成長に繋げれていけたらなと思います。

薄いフレームワーク指向の Web クライアントサイドプログラミング

こんにちは、Web アプリケーションエンジニアの id:nanto_vi です。先日開催された Kyoto.js #12 において、「薄いフレームワーク指向の Web クライアントサイドプログラミング」と題した発表を行いました。とある Web アプリケーションの開発にあたって、JavaScript による GUI プログラミングにどう取り組んだかという話になります。当日のスライドの内容に口頭で伝えた内容を加え、以下にまとめます。


前提


Web アプリケーションを新規開発するにあたり、クライアントサイドをどう実現するか。ここでは開発期間が決まっているというのが大きな要因となり、チームメンバーの経験がある MVP アーキテクチャパターンを採用することにしました。

また、JavaScript を直接記述するのではなく、TypeScript を使います。

MVP アーキテクチャパターン

TypeScript で実現する MVP アーキテクチャパターン - Hatena Developer Blog

View は Presenter にユーザー入力を伝える、Presenter は View に表示更新させる、Presenter は Model を操作する、Presenter は Model の更新を通知される

  • MVVM (Model-View-ViewModel)
    • View と ViewModel が双方向データバインディング機構で暗黙的につながる
  • MVP (Model-View-Presenter)
    • View は Presenter へ明示的にユーザー入力を伝える
    • Presenter は View の表示を明示的に更新する

MVP アーキテクチャパターンは MVVM アーキテクチャパターンと似ています。しかしながら、MVP アーキテクチャパターンには (MVVM アーキテクチャパターンにおける双方向データバインディング機構のような) ブラックボックスとなる部分がないので、既存のフレームワークを用いなくても簡単に実装できます。

ライブラリの使用

  • View
    • jQuery (DOM 操作、イベントハンドリング)
  • Presenter / Model
    • jQuery (HTTP 接続、Promise)

既存のフレームワークを用いないといっても、既存のライブラリも使わないということではありません。ともすれば冗長になりがちな処理を簡潔化するため、jQuery を使っています。とはいっても、jQuery の機能を野放図に使うのではなく、レイヤーごとに特定の機能だけ使うようにしています。

コンポーネントの例

interface IMyView {
    update(): void;
}
interface IMyArgs {
    foo: string;
}

class MyPresenter {
    view: IMyView;
    foo: string;

    constructor(view: IMyView, args: IMyArgs) {
        this.view = view;
        this.foo = args.foo;
    }

    changeFoo(foo: string): void {
        this.foo = foo;
        // (4) Presenter が View に状態変更を知らせる
        this.view.update();
    }
}

class MyView implements IMyView {
    presenter: MyPresenter;
    $element: JQuery;

    // (1) ルーターから View のインスタンスを生成する
    constructor($element: JQuery) {
        this.$element = $element;
        // (2) View から Presenter のインスタンスを生成する
        this.presenter = new MyPresenter(this, {
            foo: this.$element.attr('data-foo'),
        });
        this.$element.on('click', this.onClick.bind(this));
    }

    onClick(event: JQueryEventObject): void {
        // (3) View が Presenter の状態を変更する
        this.presenter.changeFoo(Date());
    }

    update(): void {
        // (5) View が Presenter の状態を表示に反映させる
        this.$element.text(this.presenter.foo);
    }
}

View のインスタンスと Presenter のインスタンスは一対一対応しています。コンポーネントの表示に必要な「状態」は Presenter が持っていますが、それを実際の表示 (HTML 要素) に反映させるのは View の役目です。

View は Presenter の実装を知っていますが、Presenter は View のインターフェイスしか知りません。こうすることで、Presenter をテストする際に View のモックを使うことができ、DOM に触らずプレゼンテーションロジックをテストできます。

コンポーネント間の連携

  • 当初は、HTML 要素のカスタムイベントを通じて View 同士が連携
    • 親子関係にないコンポーネントの連携が大変
      • 共通の親に仲介コンポーネントを設けるなど

当初はコンポーネント間の連携を実現するのに、jQuery のイベント機能を使って View 同士をつなげていました。しかし、それだと親子関係にない HTML 要素それぞれに対するコンポーネント同士を連携させるため、共通の祖先要素に対して仲介役のコンポーネントを設置するといった必要が出てきます。そのままだと連携するコンポーネントの数が増えたときに複雑さが増加しそうなので、後述する Model レイヤーでの連携を試みました。

Model を通じたコンポーネント間の連携

  • 複数の Presenter が Model の同一インスタンスを参照する
    • Multiton パターン (Singleton パターンの拡張)
class MyModel {
    private static instances: { [myId: string]: MyModel };
    static getInstance(myId: string): MyModel {
        let instance = MyModel.instances[myId];
        if (!instance) {
            instance = MyModel.instances[myId] = new MyModel(myId);
        }
        return instance;
    }

    constructor(myId: string) {
        ...
    }
}
  • Model の状態変化を複数の Presenter に通知する
class MyPresenter {
    constructor(...) {
        ...
        this.myModel = MyModel.getInstance(myId);
        this.myModel.on('foo', () => { this.view.update(); });
    }
}
class MyModel {
    on = Emittable.on;
    off = Emittable.off;
    emit = Emittable.emit;

    doSomething(): void {
        ...
        this.emit('foo');
    }
}

Presenter 同士が Model を介して 連携するためには、複数の Presenter のインスタンスが同一の Model のインスタンスを参照する必要があります。きちんとやるならレジストリだ依存性注入だといった話になるのでしょうが、簡単化のため Multiton パターンを使って Model のインスタンスを生成することにしました。

Presenter 同士が Model を介して連携するためには、ある Model のインスタンスの変化を複数の Presenter のインスタンスへ伝える必要があります。ここでは、Observer パターンを使って Model の変化を Presenter へ伝えることにしました。

例: 「いいね!」できるユーザー

class LikePerformer {
    static getInstance(...) { ... }

    userId: string;
    targetId: string;
    isLiking: boolean;

    constructor(userId: string, targetId: string, isLiking: boolean) {
        this.userId = userId;
        this.targetId = targetId;
        this.isLiking = isLiking;
    }

    toggleLike(): JQueryPromise<void> {
        let isLiking = this.isLiking;
        let apiURL = `/users/{ this.userId }/targets/{ this.targetId }/{ isLiking ? 'unlike' : 'like' }`;
        return $.post(apiURL).then(() => {
            this.isLiking = !isLiking
            this.emit('status-change');
        });
    }
}

例として、Facebook の「いいね!」のような機能を実現するための Model を考えてみます。「『いいね!』できるユーザー」を表すクラス LikePerformer は、対象を今現在「いいね!」しているかどうかの状態 isLiking を持っています。isLiking の値が変わるたび status-change イベントを発行し、Presenter に向けて状態の変化を伝えます。

toggleLike メソッドが Promise を返しているのは、アプリケーションを利用するユーザーへのエラー通知のためです。エラーもイベントとして複数の Presenter に通知してしまうと、各コンポーネントがそれぞれエラー通知を出し、その内容が重複してしまうかもしれません。そこで、ユーザーへのエラー通知は toggleLike メソッドを呼び出した Presenter のインスタンスが責任を持つこととします。

例: コメントを促すメッセージ

「いいね!」したがコメントしていないユーザーに対して、「『いいね!』の次はコメントしましょう」というメッセージを表示したい。

class CommentSupportMessagePresenter {
    constructor(...) {
        this.likePerformer = LikePerformer.getInstance(...);
        this.commentAuthor = CommentAuthor.getInstance(...);

        this.likePerformer.on('status-change', () => { this.view.update(); });
        this.commentAuthor.on('status-change', () => { this.view.update(); });
    }

    get isShown(): boolean {
        return this.likePerformer.isLiking && !this.commentAuthor.hasComment;
    }
}

class CommentSupportMessageView {
    update(): void {
        this.$element.toggle(this.presenter.isShown);
    }
}

前述の Model を利用する Presenter の例です。ユーザーが「いいね!」しているかどうかを知りたい Presenter は、LikePerformer のインスタンスを参照し、その状態が変化したときに自らの状態変化も View に伝えます。結果として、別のコンポーネントが引き起こした「いいね!」状態の変更により、このコンポーネントの表示が変化します。

テスト

  • テスト時にできるだけ DOM に触りたくない
    • DOM は状態の塊であり、テストのために用意するのも大変
  • Presenter のテスト時には View のモックを使う
    • DOM を用意せずにプレゼンテーションロジックをテストできる
  • 一から始めるJavaScriptユニットテスト - Hatena Developer Blog
    • Presenter / Model のテストはメソッドごとに
    • View のテストは JSDOM を使い、「ボタンがクリックされたら要素が表示される」といったシナリオに沿って

上で述べたように、Presenter をテストする際には View のモックを用意します。また、Sinon.JS を使って setTimeoutXMLHttpRequest といったブラウザの機能をモックしています。

View と Presenter の分担

  • View に条件分岐が出てきたら黄信号
// ○ OK
class MyView {
    updateVisibility(): void {
        if (this.presenter.isShown) {
            this.element.classList.remove('isHidden');
        } else {
            this.element.classList.add('isHidden');
        }
    }
}
// × NG
class MyView {
    updateVisibility(): void {
        if (this.presenter.currentScore < this.presenter.maxScore) {
            this.element.classList.remove('isHidden');
        } else {
            this.element.classList.add('isHidden');
        }
    }
}
  • イベントのデフォルトアクションをキャンセルする
class MyFormPresenter {
    handleSubmit(): boolean {
        if (!this.shouldHandle) return false;

        this.doSomething();
        return true;
    }
}

class MyFormView {
    onSubmit(event: JQueryEventObject): void {
        let isHandled = this.presenter.handleSubmit();
        if (isHandled) {
            event.preventDefault();
        }
    }
}
  • Presenter → View → Presenter と呼び出してはいけない
// × NG
class MyPresenter {
    doSomething(): void {
        this.updateVisibility();
    }

    hide(): void {
        ...
    }
}

class MyView {
    updateVisibility(): void {
        ...
        // View をモックに置換すると hide メソッドが呼び出されなくなる
        this.presenter.hide();
    }
}
  • 「状態」と「状態遷移のトリガー」は Presenter が持つ
    • View は「状態遷移のトリガー」を呼び出し、「状態」を DOM に写すだけ

MVP アーキテクチャパターンの悩みどころのひとつが、View と Presenter をどう分割するかです。「DOM に触らずプレゼンテーションロジックをテストする」という大前提があるため、個別のコンポーネントに関することは基本的に Presenter に記述し、どうしても DOM (HTML 要素) を扱わざるを得ない部分だけ View に切り出すようにします。

DOM イベントを扱うときも、イベントオブジェクトを直接 View から Presenter に渡すのではなく、DOM に依存しない数値・文字列を取り出してそれを Presenter に渡すようにします。イベントのデフォルトアクションのキャンセルも、するかどうかの判断は Presenter で、実際にキャンセルする (イベントオブジェクトの preventDefault メソッドを呼び出す) のは View でとなります。

複雑になる処理

  • Presenter が View に対して Data Transfer Object を公開する
    • ある View に特化した DTO を用意することで、View を薄くできる
  • リストの項目の追加・削除・並べ替え
    • VirtualDOM が本領を発揮する分野
  • 手続き的か宣言的か
    • UI は宣言的に書けたほうが楽
// 手続き的 (素の DOM API)
let link = document.createElement('a');
link.href = url;
link.textContent = label;
element.appendChild(link);
// 手続き的 (jQuery)
let $link = $('<a>').attr('href', url).text(label);
$element.append($link);
// 宣言的 (JSX)
<a href={url}>{label}</a>
[%- # 宣言的 (Perl の Text::Xslate::Syntax::TTerse) %]
<a href="[% url %]">[% label %]</a>

View はできるだけ薄く保つべきですが、複雑な処理が入ってしまうこともあります。代表的な例がリストの項目の追加・削除・並べ替えでしょう。この部分をうまいことを隠蔽してくれる仮想 DOM ライブラリ (React など) がうらやましくなります。

View での DOM 操作はどうしても手続き的になりがちです。コンポーネントによってはサーバー側で生成した HTML 片を挿入することで宣言的な性質を取り入れているところもありますが、仮想 DOM ライブラリを使って最初から宣言的に記述するほうがわかりやすいことも多いでしょう。

質疑応答

  • 結局開発期間に間に合ったのか?
    • 間に合ったが、今後も同様の方針を継続するには文書整備の必要があり、保守性に不安が残る
  • 別のアーキテクチャパターンを混ぜられるか?
    • 基本的にコンポーネント単位で実装しているので、あるコンポーネントだけ Flux アーキテクチャパターンで実装するといったことも可能
  • MVP パターンで検索すると Web の事例が出てこない。どう教育していくのか?
    • この資料を含め、自分たちで文書を整備するしかない

今後もこの方針を続けていけるかどうかというのは難しい点です。慣れていればまずコンポーネントの状態と状態遷移を洗い出して Presenter から書き始められるのですが、Web クライアントサイドプログラミングに慣れていないと View から書き出し、都度 Web ブラウザで確認することになります。そうなると View にプレゼンテーションロジックが詰め込まれ、View が厚くなってしまいがちです。

Flux / Redux アーキテクチャパターンのほうが文書も豊富で学習しやすく、状態と状態遷移を意識づけることもしやすいかもしれません。


まとめ

以上で見てきたように、クライアントサイドプログラミングが小・中規模に収まるなら、大掛かりなフレームワークを使わずとも開発は可能です。しかし、それにはアプリケーションの状態の把握や DOM 操作の知識が必要であり、一概に学習コストが低いとは言い切れません。

はてなでは AngularJS や React + Redux を導入しているチームもあり、それぞれのメリット・デメリットを共有して今後に役立てていければと思っています。あなたもはてなで一緒に JavaScript (TypeScript) を書いてみませんか?

hatenacorp.jp

YAPC::Kansai 2017 OSAKA にはてなから4人のエンジニアが登壇します

こんにちは。はてなアプリケーションエンジニアの id:astj です。 Perl は5も6も好きです。
昨年12月の YAPC::Hokkaido を皮切りにリブートした YAPC::Japan の第2弾として、来る2017年3月4日に YAPC::Kansai 2017 OSAKA が開催されます。 Perlを軸としたITに関わる全ての人のためのカンファレンスであるこちらのカンファレンスに、はてなのエンジニアが4名(前夜祭を含めると5名!)登壇しますので以下にトークを紹介します!

既にチケットは売り切れてしまっていますが、参加されるみなさまと会場でお会いすることを楽しみにしています。
詳細はカンファレンス公式サイトをご覧下さい。

yapcjapan.org

また、カンファレンスの前日である3月3日にはYAPC::Kansai 2017 OSAKA 前夜祭 in大阪も開催されます。こちらも既にチケットは売り切れてしまっていますが、この前夜祭で開催される「突撃!隣の開発環境!」にも id:hitode909 id:Songmu id:papix の3名がはてなから参加します。前夜祭から参加される方はこちらもよろしくお願いします。

登壇内容

PerlのWebアプリケーションをデプロイする時に僕達が考えたこと

Webアプリケーションを作ったとき, それを実際にユーザーに届けるためには, そのWebアプリケーションを"デプロイ"して, ユーザーが利用可能な状態に設置しなければなりません.
最初は, 開発者自らがコマンドを入力する, 手動のデプロイで十分かもしれません. しかし, その作業を何度も繰り返す場合, アプリケーションの構成上少し手順が複雑な場合, あるいはデプロイを行うサーバーの台数が増えてくると, 手動でコマンドを入力するデプロイだと少し厳しくなってきます.

本トークでは, 主に趣味で開発するような小規模なPerl製Webアプリケーションのデプロイ戦略や, そのために有用な道具類の紹介をします.
具体的には, 以下のような内容が含まれる予定です.

  • デプロイとは?
  • 手動デプロイと自動デプロイ
  • デプロイの自動化ツールについて
  • Push型とPull型
  • Graceful Restart
  • バッチやワーカーのデプロイ
  • デプロイ対象をどのように管理するか?

「PerlでWebアプリケーションを作ってみたけど, どういう感じにデプロイすればいいんだろう?」とお悩みの方には, きっと興味深くお聴きいただけるのではないかと思います.

http://yapcjapan.org/2017kansai/talks.html#talk-29

また id:papix は 12:00-12:40 に C会場で行われる Perl入学式の座談会、"Perl入学式座談会 〜これまでの軌跡を振り返り, そして未来へ〜" にも登壇予定です!

Perl 6 で Web Application Framework をつくる

Web アプリケーションの開発をやっていると、誰もが一度は自分で Web Application Framework を作ってみたくなる物ではないでしょうか。実際の本番で導入するかは別にして、その言語におけるWebアプリケーション開発の仕組みを理解するためには重要な試みだと思っています。今回は、 Perl 6 でシンプルな Web Application Framework を作る話から、 Perl 6 での Web アプリケーション開発についてお話ししようと思います。

http://yapcjapan.org/2017kansai/talks.html#talk-15

はてなシステムの考古学

㈱はてなは今年で設立から16年目を迎えます。人力検索はてなに始まり、はてなブックマーク、はてなブログといったコンシューマ向けサービスから、サーバ管理・監視をおこなうMackerelや他社との協業サービスなど数多くのサービスを手広く展開しています。一部のサービスは昔のまま10年以上も愛されて今も元気に動いている一方、新しいサービスを開発するときには新たな設計を選ぶこともしています。
このトークでは、はてなにおけるウェブサービス開発の軌跡をPerlエンジニアの観点から辿り、生き残ったもの・捨てられたものからポスト現代のサービス開発を探ります。

http://yapcjapan.org/2017kansai/talks.html#talk-27

Webエンジニアに知ってほしいRDBアンチパターン

最初は良かれと思ってした設計が後々大きなボトルネックになることが度々あると思います。
そんなWebエンジニアが陥りやすい罠(アンチパターン)をご紹介し、未来の苦労を一つでも減らせる、そんなセッションをお送りします。
今回は

  • 忘れ去られたバックアップ
  • 使われない制約
  • ロックの功罪

に触れたいと思います。
どれも胸に刺さる内容ですのでご期待ください!!

http://yapcjapan.org/2017kansai/talks.html#talk-5

スポンサーもしています!

はてなは YAPC::Kansai のスポンサー企業でもあります。会場ではノベルティの配布も行いますのでぜひお楽しみに!

採用情報

はてなでは Perl に限らず Web サービス開発に興味のあるエンジニアを募集しています。

hatenacorp.jp

「バックログに入らないタスクを可視化する仕組み」という話を技術勉強会でしました

こんにちは。アプリケーションエンジニアの id:daiksy です。

はてなでは毎週木曜日に技術勉強会を開催しています。

参考: 寿司と勉強会とエンジニア - Hatena Developer Blog

先週、当番が回ってきたので、「バックログに入らないタスクを可視化する仕組み」というトークをしました。

speakerdeck.com

詳細はスライドを見ていただくとして、この発表で定義された「税」などの用語が、さっそく社内でのコミュニケーションでも使われだして、エンジニア同士で、ある概念について共通の認識を持つためにもこういった場は効果があるな、と実感しました。

はてなでは、アプリケーションを構築する技術だけではなく、プロジェクトマネジメントやチームビルディングの知見などもこうして技術勉強会で共有されています。