読者です 読者をやめる 読者になる 読者になる

コネヒト開発者ブログ

コネヒト開発者ブログ

JavaScript(React+Flux)のディレクトリ構成がガタガタになりそうだったので反省して改善する

f:id:dachi023:20170524174143p:plain

こんにちは。フロントエンジニアの@ry0_adachiです。

ここのところ、React + Flux Utils *1 の構成を選択することが多く、それ以外を選択することが少なくなってきました。特に大きな不満はないし、慣れてきたので良いかもな〜と思っていたのですが、開発期間が伸びてきて、アプリ自体の規模がそこそこになってくるとFacebook公式のサンプルコードのようにシンプルな構成だけではカバーしきれない状況が生まれることもあります。運用し始めてだんだんとその辺の辛さみたいなものが見えてきたのと、継ぎ接ぎな構成になるのは嫌だったので、このタイミングで1回整理して改善してみようと思いました。

Fluxについて

Facebook提唱のアーキテクチャ、またはフレームワークです。データの流れを一方向にして、シンプルに状態管理が行えることがメリットとしてよく挙げられています。Fluxについてより詳しく知りたい方は以前書いた記事があるので良ければそちらも合わせて読んでみてください。

f:id:dachi023:20170523163305p:plain GitHub - facebook/flux: Application Architecture for Building User Interfaces

本題に入る前に

GitHubで参考になりそうなFlux Utils製のアプリケーションのディレクトリやファイルの構成を見ていると各プロジェクトに最適化されている(しようとしている)ものが多く、参考になる部分とならない部分があります。この辺はフレームワーク側で制限しない限り、それぞれが使いやすいものが生まれ続けるでしょう *2 。この記事の内容についてもハマるものもあるだろうしハマらないものもあるはずなので、こうした方が良い!というわけではなくて、あくまで参考程度に捉えてもらえると幸いです。

最初に考えた構成

最初に作り始めた時は公式のサンプルコードにあったチャットアプリをベースにして、そのあと最低限必要なものだけを足して下記の図のような構成にしました。

チャットアプリ *3 : flux/examples/flux-chat at 3.1.0 · facebook/flux · GitHub

js-root/
  ├ actions/
  │   ├ api/
  │   │   └ TodoApi.js
  │   └ TodoActions.js
  │
  ├ components/
  │   ├ TodoList.js
  │   └ TodoListItem.js
  │
  ├ constants/
  │   └ ActionTypes.js
  │
  ├ containers/
  │   └ AppContainer.js
  │
  ├ dispatcher/    
  │   └ AppDispatcher.js
  │
  ├ stores/
  │   ├ TodoStore.js
  │   └ records/
  │       └ Todo.js
  │
  └ utils/
  • 補足1. CSSは別ファイルに出力、別で読み込んでいるのでこの図では考慮しない
  • 補足2. ActionCreatorにAPIコールの責務を持たせるのでactions/api/を作成する
  • 補足3. APIクライアント以外で各レイヤーに収まらないものはutils/に集約する

この構成にして反省したこと

役割を階層で表現できていると思っていた

actions/api/が悪い例で、下記が理由です。

  1. ActionCreator以外からは呼んではいけません、を階層で表現しようとしていた
  2. 呼んではいけなさそうだな!という雰囲気を醸しているだけで呼ぼうと思えば呼べる

ガチガチにやるなら、チェックツールなどで「Action以外がAPIクライアントのメソッドをコールしているか」といった処理をするべきで、わざわざディレクトリの構造を複雑にしてまでやることではありませんでした。今は普通にコーディング規約として文字に起こしてチームメンバーに共有しておく、くらいで良いかなと思います。

Componentの中でのレイアウトとコンポーネントの切り分け

Componentの種類を「パーツを組み合わせたもの」と「パーツ」の2つではっきり分けるべきでした。参考にしていた公式のサンプルコードにはMessageSection.react.jsThreadSection.react.jsがComponentとして用意されていて、こういう風にすれば良かったのかと後から気づきました。

共通で使えるComponentなのかどうかが分からない

components/に置かれたものが「プロジェクト全体で共通的に使えるもの」なのか「特定の画面のみで使われるもの」なのかの判断をぱっと見で付けられなくなってしまったのでもう1つディレクトリを切ったほうがよかったです。

より実用的なディレクトリ構成を考える

反省点を改善できるように修正をしたのが下記の図になります。

js-root/
  ├ actions/
  │   └ TodoActions.js
  │
  ├ components/
  │   ├ common/
  │   │   ├ TextField.js
  │   │   └ Button.js
  │   └ todo/
  │       ├ Section.js
  │       ├ TodoList.js
  │       └ TodoListItem.js
  │
  ├ constants/
  │   └ ActionTypes.js
  │
  ├ containers/
  │   └ AppContainer.js
  │
  ├ dispatcher/    
  │   └ AppDispatcher.js
  │
  ├ stores/
  │   ├ TodoStore.js
  │   └ records/
  │       └ Todo.js
  │
  └ utils/
      └ api/
          └ TodoApi.js

改善されたこと

actionsの中はActionCreatorだけになった

actions/api/utils/api/にしたのでactions/にはActionCreatorだけが入ることになりました。API関連はActionCreatorから切り出されたような形になったので、例えばAPIクライアントを別パッケージにします〜となった場合でも切り出すイメージが湧きやすいかもなぁ、とそんなことも思いつきました。

ComponentにSectionを設ける

Sectionを設けたことでレイアウトやパーツの集合体を定義するクラスが明確になり迷うことがなくなりました。Sectionがレイアウトやパーツの集合体でそれ以外はただのパーツである、という認識をしっかり持てたことで前に比べるとチーム開発もしやすくなったかもしれません(今は私1人でJS書いてるのでまだ試してませんが…)。

共通なComponentとそうでないものはディレクトリを分ける

プロジェクト内で使い回しの効くComponentを切り出したことで「前にこんなの作ったけ?」と考える時間が減ったのと、共通化しようという意図が見えやすくなりました。この辺はUIを考えてくれているデザイナーさんに相談しながら少しずつ共通化して切り出していくようにしています。

余談ですが、Componentが共通化される条件は「ロジック的に使い回せるか?」よりも「UIとして使い回せるか?」を判断軸にしています。これは、CSSとComponentの組み方の乖離 *4 が大きくなると実装が後々大変になるだろう、と判断してそうしています。

まとめ

事前に想定できるケースを洗い出しておくのは前提として、最初に決めた構成のまま突き進むのではなく状況に合わせて適宜変更していく・変更しやすい状態を保つことが重要です。また、途中で開発チームの規模が大きくなるかもしれないので複数人でも開発しやすい状態などを考慮しておくことも重要です。開発者が考える時間を減らしてあげることは開発効率の向上にも繋がるのでしっかりとメンテナンスしていきたいですね。

脚注

*1:Flux UtilsはFluxのリポジトリ(facebook/flux)に含まれています。

*2:現状は決定版となる構成が特にない状況だと認識しています。

*3:2017/05/25: masterにはなかったので3.1.0のタグから引っ張ってきています。

*4:CSSは共通化しているのにComponentはされていない、といったズレが生じている状態。

CakePHP3.4.6にアップデートしました!

f:id:itosho525:20170430222524j:plain

こんにちは。 GWはずっと欅坂46の『不協和音』を聴いていたサーバーサイドエンジニアの@itoshoです。 前回のエントリーからCakePHP3アップデートの連載?を担当しています。

そして、早速GW中に新しいバージョンである3.4.6がリリースされましたので、今回も簡単にではありますがアップデート内容をまとめさせていただきました。

CakePHP3.4.6の変更点まとめ

今回は以下の変更がありました。

  • CSRFとセキュリティトークン用のフィールドのautocomplete属性を明示的に無効にした
    • 新しいSafariのバックボタンの挙動で、blackholedエラーが発生する問題を解決しています。
  • SQLServerの接続ドライバーに多数のオプションが追加
    • 具体的にはコネクションプーリングやログインタイムアウト等のオプションが追加されました
  • APIドキュメントの改善
  • FixtureのDB接続のコネクションが常に別名を使うように修正
    • これによりテスト時に利用するDBが定義されていない時に、誤って生(≒本番)データに接続する危険が回避されます。
  • 翻訳パッケージ(Aura.Intl)が、Cake3.3で生成されたキャッシュファイルからinitializeする時に発生していた不具合を修正。
  • and*()メソッド及びor*()メソッドに追加される条件句(conditions)をいずれも後ろに追加するように修正
    • 以前は and*()メソッドは条件句を後ろに追加、or*()メソッドは条件句を前に追加(prepend)していました。
    • 生成されるSQL文としては順番が逆でも影響はありませんが、混乱するので統一したようです。

詳細は公式のリリースノートをご覧ください。

また、リリースノートには記載されていませんが、GitHubのREADMEにCakeのロゴが追加されたようです!

アップデート時にやったこと

3.4.5へのアップデート同様、今回も軽微な変更のみだったので、composer updateコマンドを実行後、CIが通っていればOKとして、リリースを行いました。

おわりに

日々開発しているシステムに不協和音が起きないようにフレームワークのアップデートは早め早めに実施していきましょう。(自戒も込めて)

CakePHP3.4.5にアップデートしました!

f:id:itosho525:20170430222524j:plain

こんにちは! 3月からコネヒトで頑張っているアイドル大好きエンジニアの@itoshoと申します。 最近はBiSHさんの『プロミスザスター』をヘビロテしています。

いきなりですが、皆さんは普段Webアプリケーションフレームワークのバージョンアップをどういうタイミングで実施していますか? 影響が少なそうなマイナーバージョンアップであっても、ついつい先延ばしにしてはいないでしょうか。

しかし、そうやって先延ばしにしていると、いざバージョンアップしようと思った時に影響範囲のチェックや動作確認のコストが大きくなりがちで「まだ暫くこのままでいいか…」と二の足を踏んで、気付けばいつの間にか最新バージョンに追従出来ないレガシーなシステムを生み出してしまうことになりかねません。(僕も過去にそのような苦い経験がございます…)

コネヒトではWeb APIの開発をCakePHP3を利用して行っているのですが、そのようなシステムを生み出さないように、新しいバージョンが出た際は積極的にバージョンアップするように努めていまして、今日はつい先日実施した3.4.4から3.4.5へのバージョンアップの内容について書きたいと思います。

ちなみに、CakePHPのHTTPリクエスト / レスポンスのインターフェイスが3.4系からPSR-7準拠に生まれ変わっており(素晴らしい!)、今後deprecatedなメソッドが増えることが予想されるので、まだ3.3以下のバージョンを利用されている方はお早めのバージョンアップをオススメします!

CakePHP3.4.5の変更点まとめ

詳細な情報は公式のリリース情報をご覧いただければと思いますが、今回は以下の変更がありました。

  • PaginationHelperで複数の値が設定されたソートリンクがデフォルト以外のモデルだと正しく生成されない不具合の修正
  • CSRFエラーとMissing Controllerが発生した際のエラーメッセージの改善
  • Diactorosというライブラリ起因で発生するHttp\Client\Requestクラス内でのエラー修正

また、公式ページに記載されているのは上記の3つなのですが、changelogをみてみると.travis.ymlからHHVMの記述が消えているので、興味ある方は実際のコードを読んでみると思わぬ発見があるかもしれません。

アップデート時にやったこと

3.4.4からのアップデートで、今回は軽微な不具合の修正のみだったので、composer updateコマンドを実行後、CIが通っていればOKとして、リリースを行いました。(アップデート後も特に不具合は起こっていません)

こういう時、きちんとテストコードを書いておくと安心ですね!

おわりに

今回はマイクロバージョンアップなので、問題なく最新バージョンにアップデートすることが出来ましたが、これからCakePHPをバージョンアップした際はこのブログでご報告させていただいて、微力ながらCakePHP界隈を盛り上げていくことをプロミスしますので、今後ともよろしくお願いいたします!

追伸

この記事をしたためている間に3.4.6がリリースされたので、それについても近日中にまとめたいと思います!

チームでのAPI開発の強い味方!!REST APIクライアント「Paw」と「Insomnia」を比較してみた

f:id:supermanner:20170502152129j:plain

こんにちは!今年もコナン映画にいってきました、コナンでは服部派のエンジニア結城(@super_manner)です(*´ڡ`●) さて、今回はAPIをチームで開発するうえでつよーい味方になるツールを2つ使い比べた結果をご紹介しようと思います!!

そもそもPawとInsomniaとは?

双方ともREST APIクライアントです。

Paw paw.cloud Insomnia insomnia.rest

APIを作成していると、POSTする必要があったり、User-AgentやRequestHeaderによる制約を受けたりで プラグイン追加が加速したりしますよね。
うっかりそのまま他のサイトを閲覧して全部がxmlで表示されたりすることもしばしば。
そんな煩わしさも、これらのクライアントを使うことで開放されるのです!!
APIをメインに開発されている方にはもはや必需品になっているかもしれませんね。 百聞は一見にしかずなので、試しに国土交通省のAPIを叩いた画面を見てみましょう。 *1

PawでGETした様子 f:id:supermanner:20170427205919p:plain InsomniaでGETした様子 f:id:supermanner:20170427210037p:plain

このように、各種設定をしてあげることで簡単に何が返ってくるかを確認することができます。

共有ツールとしてのおすすめポイント

わかりやすいUIなので、初めて使う方でも迷うことなく使っていただけるのがいいところです。
以下、参考画像はInsomniaのものですが、Pawも同じ機能がついています。(詳しい比較は後述します)
このように、階層構造でendpointを管理できたり、パラメータやヘッダ情報もまるっと共有できます! f:id:supermanner:20170501183349p:plain f:id:supermanner:20170501192159p:plain こちらは、環境変数を管理できる機能です。共通して使う変数と、そうでないものを切り分けできて便利です。 f:id:supermanner:20170501205120g:plain

また、snippetを自動生成してくれるのでPull Request等に貼り付けると、レビュワーに優しいですね\(^^)/ f:id:supermanner:20170501195328g:plain

2つのツールの機能比較

さて、ツールの紹介が終わったところで本題です。
コネヒトのAPI開発陣はトライアル期間を経て2つのツールの選択をせまられました… ツールの選択はいついかなるときも難しいものですよね。
ということで、2つのツールを比較し、できること、できないことを表にしてみましたので御覧ください。

Paw Insomnia 備考
RequestHeaderの設定
パラメータの設定
cookieの設定
環境ごとの変数の設定 例えばですが, {local/dev/stg/prod} 等で別途環境変数をマネージする機能があります
snippet発行 ただし双方で発行できるsnippetの種類に違いがあります
設定のimport/export
複数のapiを同時に叩く
teamアカウントを発行できる teamを作成することで、登録したapiの設定を共有できます
ブランチ機能がある ブランチ機能については後述します
料金 個人プラン:$49.99
チームプラン:月額$10
制限付き$0
有料プラン:月額$5
チームプラン月額:$8

Pawのほうが少し料金が高めですが、その分機能が多いですね。
InsomniaでできることはPawですべて網羅していると言っても過言ではないでしょう。(もちろんsnippetの発行形式など細かな違いはあります)
また、個人でcloudにプロジェクトをもつことのできる個人プランはいいお値段です!
そんな多機能なPawですが、中でも特筆すべき機能としてブランチ機能があります。

Pawのブランチ機能について

公式のドキュメントが非常に丁寧に書いてあるので、基本的な使い方はこちらを参照していただけると良いかと思います。 さぁ、セクションタイトルを見てみましょう!

Manage your workflow with branches

何処かで見たような機能ですね。そうです!我々も馴染みが深いgitと同じです。 開発中のapiなどはいきなりみんなに共有しても「これ開発中です!」と宣言して回る訳にはいきません。
そのような場合は自分用にブランチを切って、そこで登録していけばmasterはスッキリと余分なものが登録されないまま保たれます! チームにとっても、自分にとっても常に動くものが簡単に手に入りますb

Insomniaのデータ共有はどうなっているの?

先程の表で述べた通りInsomniaにもteam機能があり、workspaceとよばれる箱でデータを共有することができます。
が、現時点では手元のデータとremoteのworkspaceの同期だけで、git pullのような機能は実装されていません
つまり、手元で変更してしまうとその時点で残念ながらremoteのデータとはズレが生じてしまい、復旧の術がないということです。
dropboxをイメージしていただけるとわかりやすいかと思います。 cloud上のworkspaceと完全一致していない場合は下図のようにどれだけズレているのかが%で表示されています。
f:id:supermanner:20170429174502p:plain

英語ドキュメントの読み逃しがこわかったため、運営チームに問い合わせをさせていただいたところ、下記の返答を頂いたので将来的には実装されるかもしれませんね!!期待して待ちましょう。

Hi Mana,

Thanks for reaching out :)

There is no official way to do a pull master currently. For now, I recommend duplicating the request (or folder) that you want to edit so that you do not modify the existing data.

I will be sure to consider this use case as I improve the team features in the future.

Let me know if that works for you.

まとめ

2つのツールの機能比較をして、現在私のチームではInsomniaにチーム課金して使用しています。
なぜなら、いまのところREST Clientの使用理由が

  • 実装したAPIの各環境での実行確認をするため
  • テストで使用したrequest情報を完全再現してレビュー時にレビュワーにわかりやすくするため

の2つであるからです。
ですので、機能としてはシンプルで、料金もお財布に優しいInsomniaを採用し日々の開発に役立てています。 しかし今回記事を書くにあたって改めて調べてみるとPawの機能はまだ全然つかいこなせていなかったなぁと思ったので、 今後も使って有用だった機能があれば引き続き紹介していきたいと思います!*2

双方ともまだまだ開発は進んでいるようなので、みなさんもよければぜひ活用して素敵なAPIライフを送ってください!

*1:http://www.land.mlit.go.jp/webland/api.html

*2:pawのdocの厚さがものすごい

デザイナーの生産性を高める3つの改善策

f:id:connehito:20170331142331p:plain

お疲れ様です!デザイナーのきよえし(@kiyoe_furuichi)です。 今回は「生産性」というテーマと戦って得た学びについて書いてみたいと思います。 少し長いので、お茶でも飲みながらゆっくり読んでくださいね。

生産性とは

インプット(成果を生み出すためのヒト・時間・情報など)に対するアウトプットの比率のことを「生産性」と言います。 生産性は、少ないインプットで大きなアウトプットを得られると生産性が高いと評価され、大きなインプットの割にアウトプットが小さいと生産性は低いとされます。

デザイナーのお仕事で例えると、バナーを1日で作成してリリースするのと、半日で作成してリリースをするのとを比較して、成果物は同じものだとすると後者の方が「生産性が高い」ということになります。

きっかけ

「忙しい」を理由に思考時間を減らすのをやめたいと思ったことがきっかけでした。 サービスの成長と共にチャレンジできることが増え、好きな領域で活躍できることが嬉しい一方、目の前のissueに追われ本質からモノゴトを考える余裕がなくなっていました。 そんな状況だと質の高いアウトプットは出せない・精神衛生上良くない = 生産性が低下するという問題意識から、自分の仕事のやり方を見直すことにしました。

実践

生産性を高めるために実践したことは以下の3つです。

  1. RescueTimeで消費時間を可視化する
  2. issueを細かく分解する
  3. 情報を断捨離する

補足すると、今回は特に問題意識の高い「時間の使い方」にフォーカスして実践しました。時間の使い方が上手になると本質的な作業への消費できる時間が増えるため、デザインの質が上がり、生産性が高まると考えたためです。

1. RescueTimeで消費時間を可視化する

RescueTime(以下レスキュータイム)というツールを使って、1日の作業内容のうち、本質的な作業にどれだけ時間を消費できたかどうかを計測します。

例えばこのログをご覧ください。私の記念すべき初レスキュータイムの結果です。

f:id:connehito:20170330201240p:plain (本質的な作業 = Software Development + Design & Composition)

グラフを見ると、1日の作業時間の約30%がCommunication (to Slack)に消費されていることがわかります。本質的な作業よりもコミュニケーションの時間が掛かっているのは健全ではありません。 レスキュータイムのログを毎日退社前に見ながら、1日を振り返って改善ポイントを洗い出します。 ちなみにこの日は以下のような反省をしました。

Slackの閲覧時間が多くなってしまう原因として、モニター上に表示しっぱなしで新着の投稿があると気になって開いてしまうことがあげられる。必要なとき以外は目に止まらないようにウィンドウを非表示にしたり不要なチャンネルをミュートするなどで工夫をしよう。

2. issueを細かく分解する

(このやり方はgithub/ZenHubを利用した進捗管理を行なっている方に限定されます)

issueの粒度を細かくすることでアウトプットのゴールを明確にします。 そうすることで1作業に対しどのくらい時間を消費したかどうかが見やすく振り返りもしやすくなります。

例えば「新しい機能のUI設計」というissueがあるとします。細分化するとそのissueをepic(親)としてUI設計に必要な工程ごとにこのように分けることができます。

  • 【epic】 新しい機能のUI設計
    1. 【issue】 アイデア出し
      • ゴール:頭の中でイメージを固める
    2. 【issue】 ワイヤー作成
      • ゴール:ディレクターにOKをもらう
    3. 【issue】 Sketchデータ作成__iOS
      • ゴール:prott確認ができる状態に落とす
    4. 【issue】 Sketchデータ作成__android
      • ゴール:prott確認ができる状態に落とす
    5. 【issue】 確認・修正
      • ゴール:ディレクター・エンジニアと確認を行い、OKをもらう
    6. 【issue】 Zeplinデータ納品
      • ゴール:ZeplinデータをiOS/androidエンジニア両方に納品する

細分化すると一覧として見たときにすごい量になり管理が難しくなりますが、ZenHub(ゼンハブ)というプロジェクト管理ツールでissue管理を行い、次やるべきissueや進捗が明確になるようにしています。

f:id:connehito:20170330201327p:plain

in Progress が現在着手しているissueで、in Reviewがレビュー中、そしてDesign には1スプリント内に着手するissueが順番に並んでいます。

3. 情報を断捨離する

1に記載したレスキュータイムを始めてから、コミュニケーションで時間を消費しすぎているという課題を発見したため、見るもの・見ないものを分別して情報の断捨離を行いました。 主に以下の3つを意識して時間の節約を行なっています。

① タイムラインは閲覧を控える

Slackやカレンダー、SNSなどタイムラインを追うツールはマインドシェアを取られがちなので必要以上には見ないようにしています。
特にSlackの分報の使い方には注意しています。気軽に気づきやつらみを共有することを目的とするチャンネルですが、ついおしゃべりをしてしまうので時間を区切って見るなど常駐しないように気をつけています。

② 大事なことを見逃さないように工夫する

タイムラインの閲覧を控えると、タイムラインが流れてしまい大事な要件を見逃してしまうことがあります。うっかりしないよう、以下のSlackの設定で予防できるようにしました。

  • Slackの /remind機能を利用し、タイムラインを見る頻度を減らす
  • 必ず見るChannelは通知が来るように設定し、見逃しを予防する
  • 定期的に見ないChannelはミュートし、見るべきものだけにフォーカスできるようにする

③ 1次情報をなるべくチェックする

タイムラインの閲覧を制限すると得られる情報に偏りがでてしまうため、議事録やメモなど、まとめられた資料を読んでキャッチアップしています。 コネヒトではDocBase(ドックベース)というドキュメント共有ツールを利用しているので、そちらに投稿されたものは毎日数分時間を取って読んでいます。

f:id:connehito:20170331142058p:plain

成果

上記3つを3ヶ月間実践した結果、3つの改善をすることができました。(333!)

1. 本質的な作業への消費時間が増えた

RescueTimeを見ると時間の使い方が大分良くなったことがわかります。
1日の作業時間のうち、本質的な作業に消費する時間が40%から64%と約1.5倍の改善を達成することができました。

f:id:connehito:20170330201425p:plain (本質的な作業 = Software Development + Design & Composition)

2. デザインの質が上がった

1issueに掛ける時間を増やしたことで、制作後の手戻り回数を減らすことができました。さらに 1スプリント内に消化できるストーリーポイントの合計が22ptから48ptと2倍近く増え、着手できるissueをかなり増やすことができました。(まぐれかもしれないので、引き続き邁進します)

f:id:connehito:20170330201435p:plain

3. 気持ちに適度な余裕ができた

時間に余白をつくることができたため、納品が近い成果物があった場合も気合いで解決することがなくなりました。切羽詰まるような不安要素がなくなり、健全な気持ちでモノゴトに立ち向かえるようになりました。

終わりに

生産性というテーマと向き合う中で、デザイナーとして一歩成長することができました。サービスのことを俯瞰して考える時間が増え、自分自身が納得感を持てるアウトプットを出せるようになったからです。
とはいえ、今回ご紹介した改善策は、"マイナスをゼロに戻すためのもの"だと考えています。
「生産性を高める」ということにおいて、良いアウトプットを出すためにはやっぱりスキル面を磨かないといけません。そのために、今後はゼロからプラスにするための改善に取り組んでいこうと考えています。最後まで読んでいただき、ありがとうございました!

参考

Kotlin1.1の新機能について

f:id:tommy_kw:20161106193050p:plain

こんにちは!@tommykwです。先日のDroidKaigiとても楽しかったですね。2日間参加させていただき、どのセッションも素敵なセッションで学びのある時間を過ごせました。スタッフ、スピーカー、参加者の皆さん、ありがとうございました!

さて、Kotlin1.1が3/1にリリースされました。早いものでもう3週間ほど経ちますが、すでに利用されている方も多くいらっしゃるのではないでしょうか。Kotlin1.1では多くの機能がリリースされましたが、今回はKotlin1.1で導入された便利な新機能をいくつか情報共有したいと思います。

今回紹介する新機能

  • Alt+Enter
  • _キーワード
  • 型エイリアス
  • ::キーワード
  • データクラス継承

Alt+Enter

Alt+EnterとはIntentionと呼ばれるもので、Android Studio上でAlt+Enterすることによって簡単にクイックフィックスできる機能です。 高速コーディングを支える技術で、Kotlin1.1で新しく追加されたクイックフィックスについて紹介したいと思います。

  • Add Android View constructors using @JvmOverloads

Android開発でカスタムViewを作る場合はコンストラクタを複数定義する必要があり、とても冗長になります。 それをKotlinの@JvmOverloadsを使うことによって簡潔に記述できます。 f:id:tommy_kw:20170317080730g:plain

簡単ですね。しかし、以下のように既にコンストラクタを定義した場合はクイックフィックスできません。 この状態からコード最適化できるとリファクタリングが容易です。

class CustomView: View {
    constructor(context: Context) : this(context, null) {
    }
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    }
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
    }
}
  • Convert to range check

範囲指定しているif文をRange型に変換します。 f:id:tommy_kw:20170317082139g:plain

  • Merge ‘if’s

ネストしたif文をマージします。 f:id:tommy_kw:20170317082603g:plain

  • Use destructuring declaration

ラムダで分解宣言を使うことによって、オブジェクトからデータを取り出して別の変数に代入します。 f:id:tommy_kw:20170317224853g:plain

よく見ると、fun main(args: Array<String>) {} と記述するとKotlinアイコンが表示されますね。 エラー、警告表示があった時に取り敢えずこのショートカットを使えば役に立ちますし、特にカスタムViewのクイックフィックスは Kotlinっぽく記述できてコーディングが捗りそうです。

_キーワード

Swiftでもお馴染み_キーワードで、パラメータを省略するためのキーワードです。 ママリQにて、Kotlin1.1にアップグレードした際にLintで未使用な変数である旨の警告が大量に表示されました。そんな時に利用したのが_キーワードになります。使用用途としては以下の通り、ラムダの引数、無名関数の引数、分解引数で利用できます。

  • ラムダの引数
// v, hasFocusパラメータを定義する
view.setOnFocusChangeListener { v, hasFocus ->
}

// v, hasFocusパラメータを省略する
view.setOnFocusChangeListener { _, _ ->
}
  • 無名関数の引数
// xパラメータを定義する
val function = fun(x: Int) {}

// xパラメータを省略する
val function = fun(_: Int) {}
  • 分解引数 その1
// a, b, cパラメータを定義する
val (a, b, c) = listOf(1,2,3)

// bパラメータを省略する
val (a, _, c) = listOf(1,2,3)
  • 分解引数 その2
// a, b, cパラメータを定義する
listOf(Triple(1, 2, 3)).forEach { (a, b, c) -> }

// a, cパラメータを省略する
listOf(Triple(1, 2, 3)).forEach { (_, b, _) -> }
  • onClickイベントのunusedなView引数

AndroidでよくあるパターンのonClickイベントのView引数がunusedになる問題ですが、

fun onPhotoClick(@Suppress("UNUSED_PARAMETER") view: View) {
    // do something
}

残念ながら以下の様にメソッドの引数には適用できません。unusedを回避するには@Suppress(“UNUSED_PARAMETER”) アノテーションをつけるのが良さそうです。

fun onPhotoClick(_: View) {
    // do something
}
  • _キーワードに型を明示的に定義

Function型の引数が複数ある場合に{}{ _ -> }で記述するか悩むケースがあります。 以下はRxJavaを用いたサンプルになります。

// RxJava1の非同期Extension
fun <T> Observable<T>.async(onNext: (T) -> Unit, onError: (Throwable) -> Unit) {
    this.subscribeOn(Schedulers.newThread())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(onNext, onError)
}

// APIクライアントからメンバーカウントを取得する
apiClient.memberCount()
    .async(
        { result ->
            if (result.count > 0) {
                // do something
            }
        }, 
        { e ->  } // 未使用のeが未使用となる
    )

上記の{ e -> }{}に変えても良いですし、{ _: Thowable -> }と型指定した宣言をしても良いのではないでしょうか。 多少冗長になりますが、個人的には可読性が上がる場合もあるかなと思います。

apiClient.memberCount()
    .async(
        { result ->
            if (result.count > 0) {
                // do something
            }
        }, 
        { _: Thowable ->  }
    )

型エイリアス

型エイリアスは既存の型を別の型として提供でき、関数、コレクション、コンパニオンオブジェクト、ネストしたクラスなどに適用できます。 トップレベル宣言、メンバー宣言、ローカル宣言をすることができますが、Kotlin1.1ではトップレベル宣言のみサポートされています。

以下はKotlinのstdlibのコード内で型エイリアスを利用しているケースですが、本来importする必要があるライブラリを型エイリアスを使うことによって、利用する側はimport不要で利用出来るのが良いです。

TypeAliases.kt

@file:kotlin.jvm.JvmVersion

package kotlin.collections

@SinceKotlin("1.1") public typealias RandomAccess = java.util.RandomAccess


@SinceKotlin("1.1") public typealias ArrayList<E> = java.util.ArrayList<E>
@SinceKotlin("1.1") public typealias LinkedHashMap<K, V> = java.util.LinkedHashMap<K, V>
@SinceKotlin("1.1") public typealias HashMap<K, V> = java.util.HashMap<K, V>
@SinceKotlin("1.1") public typealias LinkedHashSet<E> = java.util.LinkedHashSet<E>
@SinceKotlin("1.1") public typealias HashSet<E> = java.util.HashSet<E>


// also @SinceKotlin("1.1")
internal typealias SortedSet<E> = java.util.SortedSet<E>
internal typealias TreeSet<E> = java.util.TreeSet<E>

さらに、以下のAnnotations.ktを見ていただくといくつかのアノテーションをサポートしていることがわかります。 型エイリアスは@Deprecated, @SinceKotlin, @Suppressアノテーションを許可されていて、別名の型として利用する機会はあると思いますが、アノテーション付きで定義できるところが良いなという印象です。

Annotations.kt


@Target(CLASS, FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, PROPERTY_SETTER, PROPERTY_GETTER, TYPEALIAS)
@MustBeDocumented
public annotation class Deprecated(
        val message: String,
        val replaceWith: ReplaceWith = ReplaceWith(""),
        val level: DeprecationLevel = DeprecationLevel.WARNING
)

@Target(CLASS, ANNOTATION_CLASS, PROPERTY, FIELD, LOCAL_VARIABLE, VALUE_PARAMETER,
        CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, TYPE, EXPRESSION, FILE, TYPEALIAS)
@Retention(SOURCE)
public annotation class Suppress(vararg val names: String)

@Target(CLASS, PROPERTY, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, TYPEALIAS)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public annotation class SinceKotlin(val version: String)

このような未定義を表したカスタムViewクラスも定義できますし、@Suppressアノテーションもうまく利用できるかもしれません。

@Deprecated("unused view")
typealias DeprecatedView = View

::キーワード

::キーワードが追加されました。これはJavaの<expression>::classメソッド参照と<expression>::<member name>メンバー参照をサポートしています。 毎回ラムダで少し冗長に記述するのではなく、::キーワードを使ってより簡潔に記述できます。

// ラムダ
listOf("a","b","c").map { it.toUpperCase() }

// メソッド参照
listOf("a","b","c").map(String::toUpperCase)
data class Member(val id: Int)
// ラムダ
intArrayOf(1,2,3).map { Member(it) }

// メソッド参照
intArrayOf(1,2,3).map(::Member)

また、KClassを取得するのに、.javaClass.kotlinがありましたが、::classを使ったほうがよさそうです。 以下のコードからもわかるように、.javaClass.kotlin::classでは動作が異なるため、.javaClassはDeprecatedになっていく予定のようです。

class Foo {
   fun bar() {
      val a: KClass = this.javaClass.kotlin
      val b: KClass = this::class
 
      a.memberProperties.forEach { property: KProperty1<Foo, *> ->
          property.get(this)
      }
 
      b.memberProperties.forEach { property: KProperty1<out Foo, *> ->
          property.get(this) //Error: Kotlin: Out-projected type 'KProperty1<out Foo,*>' prohibits the use of 'public abstract fun get(receiver: T): R defined in kotlin.reflect.KProperty1'
      }
   }
}

データクラス継承

データを保持するクラスとして便利なdata classがありますが、これは継承することができませんでした。以下の通り、Kotlin1.1からsealed classやopen classを継承できるようになり、柔軟なデータクラス設計ができるようになりました。

open class Base(
    open val id: Int, 
    open val name: String
)

data class Member(
    override val id: Int,
    override val name: String,
    val age: Int
) : Base(id, name)

val member = Member(1, "foo", 20)

今までは生成されるコードの一貫性を保つために、クラスを継承できない仕様になっていました。インターフェースは実装することができましたが、 sealed classやopen classを継承できるのはとても便利です。

Kotlin 1.0.7、1.1.1リリース

早いですね!1.0.7は現時点でバージョン1.1にアップグレードできないユーザが利用できるように、Gradleおよびアノテーション処理に関連する修正をバックポートできるバージョンです。1.1.1は1.1.0のバグ修正がメインで一部のIDE、Compiler、Librariesの修正を行っています。特に問題ないのであれば、1.1.1の利用をオススメします。

まとめ

Kotlin1.1の新機能について簡単に説明しましたが、いかがでしょうか。本来であれば、Kotlin1.1のメイン機能であるCorutinesについて情報共有できれば良かったのですが、正直まだ使いどころがわかっていません。こちらは引き続き理解を深めていきます。また今回紹介していない新しいスコープ関数provideDelegateオペレータなどすぐに利用出来る便利な機能がたくさんリリースされました。これらをうまく利用することで、Kotlinの言語機能を最大限に活かせそうです。それでは楽しいKotlinライフをお過ごしください!

Dangerで始めるPull Requestチェック自動化

f:id:Utmrer:20170228201418p:plain

こんにちはー!こねひとちほーのえんじにあのフレンズ@Utmrerだよー!

今回はPull Requestを自動でチェックしてくれるDangerについて紹介します。

Pull Requestでのコミュニケーション

Pull Requestのレビューは不具合の指摘やコーディングスタイルの統一、より良いコードのための提案などのために行われます。 ですが、次のようなコミュニケーションをしたことはありませんか…?

  • タイトルにIssue Idを含めてもらえますか?
  • WIPみたいなんですがレビューして大丈夫ですか?
  • Base branchが間違ってます、変更してください。
  • 変更履歴のdocsを更新してください。

このような「実装とは関係のない指摘」はできるだけ減らし、自動化したいものです。それを実現するのがDangerです。

Dangerとは

DangerのGitHubには次のように書かれています。

Formalize your Pull Request etiquette.

Stop saying “you forgot to …” in code review

つまり「Pull Requestの作法を形式化して指摘を自動化」するツールです。

DangerがチェックするのはPull Requestの体裁なのでそのPull RequestがSwiftのプロジェクトであろうがRubyであろうが関係なく、すべてのプロジェクトに導入可能です。 実際にRxSwiftというSwiftのプロジェクトやcapistrano、sentryなど様々なプロジェクトで導入されています。

Dangerの導入

Travis上でDangerを実行し、GitHubのPull Requestにコメントするところまでを説明します。

Gemfileの設定

GemfileにDangerを追加します。これだけでTravis上でDangerを実行できます。

source 'https://rubygems.org'

gem 'danger'

Dangerfileの追加

プロジェクトのルートディレクトリにDangerfileというファイルを追加します。

このファイルにDangerにどのような項目をチェックしてもらうかを記述していきます。

warn('あぶないよー!')

warn(message)と書くと「これは注意が必要だよ」とGitHubにコメントします。

通常は特定の条件によってコメントするのでifなどで囲みますが今回はこのままにしておきます。

.travis.ymlの設定

.travis.ymlにDangerを実行するように設定します。

cache:
  - bundler
before_script:
  - bundle exec danger

Cache機能を使えば毎回bundle installを実行する必要が無いので時間節約のためにおすすめです。

Travis環境変数の設定

TravisからGitHubへコメントするため、TravisにGitHubのアクセストークンを設定する必要があります。 アクセストークンはこちらのページから作成できます。

Dangerでは公開プロジェクトではpublic_repo、非公開プロジェクトではrepoの権限を持ったアクセストークンが推奨されています。

また、この時にアクセストークンを発行するGitHubアカウントは普段使っている個人アカウントではなくBot用のMachineアカウントの方が良いです。GitHubは1人1アカウントのMachineアカウントの作成を許可しています

発行したアクセストークンはTravisの各Repositoryのsettingsページで設定します。 f:id:Utmrer:20170228161943p:plain Environment VariablesDANGER_GITHUB_API_TOKENというNameで先程のアクセストークンを追加します。

Pull Requestの作成

以上でDangerの最低限の設定は終了です。Pull Requestを作成するとDangerからコメントが来ます。 f:id:Utmrer:20170228200733p:plain

ここまでの導入方法はDangerの公式HPにも載っています。 GitLabを利用している方やCircle CIを利用している方はこちらからご自分の環境にあった方法で導入してください。

構文

DSL

Dangerにはコメントを書き込むDSLがいくつかあり、最もよく使うであろう3つのDSLを紹介します。

📖 message

messageは単純にコメントを書き込むDSLです。定型文を返す時などに使えそうですね。

message('Pull Requestありがとうございます。レビューまで少々お待ち下さい。')

⚠ warn

warnは出来れば修正して欲しい、もしかしたら違反しているかもという場合に使います。Pull Requestのタイトルやディスクリプションに関するものはwarnがいいでしょう。

warn('descriptionを300字以上書いて下さい')

🚫 fail

failは許容できない内容の場合に使いましょう。このDSLを利用した時、Commit stateはfailureになります。

fail('LICENSEファイルは変更しないで下さい。')

変数

環境によってDangerfile上で扱える変数がいくつかあります。これらを組み合わせることでルールを作っていきます。今回はGitHub環境で使えるgitgithubについて紹介します。

git

gitという変数にはPull Requestの対象となっているCommitの情報などが含まれています。例えば「編集したファイルの一覧」「削除した行数」などです。

if git.deleted_files.include? "LICENSE"
  fail('LICENSEファイルは変更しないで下さい。')
end

github

githubという変数からPull Requestの情報が取得できます。github.pr_jsonからPull Request全体の情報を取得でき、GitHubのAPI Referenceなどを読むとgithub.pr_jsonにどのような値が入っているかわかるでしょう。

よく使うデータには専用の関数が用意されていてgithub.pr_titlegithub.pr_bodyからタイトルやディスクリプションがをシンプルに取得できます。

if !github.pr_json['mergeable']
  fail('マージできません。')
end
if !github.pr_title.include? "WIP"
  warn('このPull Requestは実装途中です。')
end

詳しくはReferenceをご覧ください。

Danger導入事例

Dangerの導入方法がわかったところで、OSSプロジェクトがDangerでどのようなことを行っているか見てみましょう。

capistrano

capistranoではDangerfileで次のようなことをチェックしているようです。

has_lib_changes = !git.modified_files.grep(/^lib/).empty?
has_test_changes = !git.modified_files.grep(/^(features|spec)/).empty?
has_changelog_changes = git.modified_files.include?("CHANGELOG.md")
if has_lib_changes && !has_test_changes
  warn("There are code changes, but no corresponding tests. "\
       "Please include tests if this PR introduces any modifications in "\
       "#{project_name}'s behavior.",
       :sticky => false)
end

「Libraryに変更があったならテストを書こう」ということをチェックしていますね。Reviewerに指摘される前にTestが書ければコミュニケーションが減って時間の節約になります。

if !has_changelog_changes && has_lib_changes
  markdown <<-MARKDOWN
Here's an example of a CHANGELOG.md entry (place it immediately under the `* Your contribution here!` line):
 ```markdown
* [##{pr_number}](#{pr_url}): #{github.pr_title} - [@#{github.pr_author}](https://github.com/#{github.pr_author}).
 ```
MARKDOWN
  warn("Please update CHANGELOG.md with a description of your changes. "\
       "If this PR is not a user-facing change (e.g. just refactoring), "\
       "you can disregard this.", :sticky => false)
end

またLibraryに変更があったならCHANGELOG.mdに変更点を書くように促しています。これは商用プロダクトでもHelpの更新や仕様書の更新などに使えそうなロジックです。

capistrano/Dangerfile

Sentry

SentryではPull Requestの体裁に関するものがいくつかあります。

# Make it more obvious that a PR is a work in progress and shouldn"t be merged yet
warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]" || github.pr_body.include?("#wip")

タイトルとディスクリプションをチェックしてPull RequestがWIPかどうかを判定しています。WIPなのにReviewをお願いすることを防げますし、Reviewerも「WIPなので見なくて大丈夫!」と判断できます。

@S_BIG_PR_LINES ||= 500
# Warn when there is a big PR
warn("Big PR -- consider splitting it up into multiple changesets") if git.lines_of_code > @S_BIG_PR_LINES

Pull Requestの実装行数が大きすぎる場合は分割するように促しています。実装内容が膨大なPull RequestはReviewが大変ですし、不具合が紛れ込む可能性も高いので分割してPull Requestを作るのは良いことですね。

sentry/Dangerfile

mamariQ

最後に、弊社のママリQ iOSアプリのDangerfileを公開します。

# --------------------
# test
# --------------------
is_source_changed = !git.modified_files.grep(/^mamariQ\/Classes/).empty?
is_test_changed = !git.modified_files.grep(/^mamariQTests\/Case/).empty?

if is_source_changed && !is_test_changed
  warn('コードの変更がありますが、テストの変更がありません。必要であればテストを追加・修正しましょう。')
end

# --------------------
# base branch
# --------------------
is_to_master = github.branch_for_base == 'master'
is_to_release = !!github.branch_for_base.match(/release-[0-9]+\.[0-9]+\.[0-9]/)
is_from_release = !!github.branch_for_head.match(/release-[0-9]+\.[0-9]+\.[0-9]/)

if is_to_master && !is_from_release
  warn('masterへmerge出来るのはrelease branchのみです。')
end

if is_to_release
  warn('release branchに対してPRを向けないで下さい。develop branchに向けてPRを作成し、develop branchをrelease branchにmergeしてください。')
end

# --------------------
# pr title
# --------------------
is_wip = github.pr_title.include? '[WIP]'

if is_wip
  warn('このPRは作業中です。')
end

is_to_develop = github.branch_for_base == 'develop'
is_start_with_issue_id = !!github.pr_title.match(/^#[0-9]+/)

if is_to_develop && !is_start_with_issue_id && !is_wip
  warn('PRのタイトルは<b>「#XXX Foo bar...」</b>とIssue idから始めてください。')
end

# --------------------
# diff size
# --------------------
is_big_pr = git.lines_of_code > 500

if is_big_pr
  warn('PRの変更量が多すぎます。可能であればPRを分割しましょう。分割が難しければ次から気をつけるようにしましょう。')
end

弊社のiOS開発ではgit-flowを採用していて、Pull RequestにおけるBase branchの選択がちょっと複雑です。そのため、DangerでBase branchのチェックを自動化しています。このDangerfileを書いたのは私なのに自分で引っかかることがあってDangerに感謝感激です。

人は人にしか出来ないことを

DangerにはPlugin機能があり、機能を拡張できるので可能性は∞です。Android LintSwiftLintのPluginもあります。(私の環境ではSwiftLint Plugin動かなかったので動作は保証できないですが…)

そういえば、Dangerを作ったOrtaさんKrauseFxさんはtry! Swiftで講演されるようですよ。try! Swiftに行かれる方はDangerの話もしてみてはどうでしょう?私もいきます☺

機械に出来ることは機械に任せて、人は人にしか出来ないことに集中していきましょう。