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

#再演 します。「エンジニアの技術力評価は難しい? - 5年間運用してきた技術力評価制度の改善の歴史 ‒」現役の評価者/被評価者も参加予定!

技術力評価会 再演

こんにちは、月日が経つのは早いものでCTO歴が6年半を越えたmakogaです。

ご縁があり、今年の1/12(木)にRegional SCRUM GATHERING Tokyo 2017で登壇しました。内容はエンジニアの技術力評価を5-6年掛けてどう改善してきたかです。

翌日スライドを公開したところ、多くの方に見てもらえ、はてブTwitterなどでたくさんのコメントもいただきました。

コメントを読むと、さまざまな考え方があると感じます。また、当日の懇親会や後日会った方との意見交換はとても楽しいものでした。今後も技術力評価会を改善していくにあたり、もっとたくさんの方と意見交換していきたいと思い、今回 #再演 イベントを開催することにしました。

また、当日の質疑応答では「うちでやろうとすると、評価するのを嫌がるエンジニアが出てくると思うのですが、そういうのはなかったですか?」というような質問がありました。それを社内のSlackにpostしたところ下記のようなやりとりがありました。

f:id:voyagegroup_tech:20170214175119p:plain

Regional SCRUM GATHERING Tokyo 2017ではVOYAGE GROUPからの参加は私だけでしたが、今回の #再演 イベントでは現役の評価者/被評価者も参加予定です。「評価されるって実際どうなの?」「評価者大変じゃない?」など、参加者の方々から疑問を投げかけてもらえると盛り上がるのではないかと期待しています。

エンジニアの評価や育成、組織における制度設計・運用などに興味がある人たちとの交流を楽しみにしていますので、まずは気軽に下記connpassイベントをクリックし「このイベントに申し込む」をポチっと押してみませんか!

connpass.com

GitHubにおけるPull RequestのAssign/Mergeを自動化して開発を加速させる

事業を支える技術 開発フロー

皆さんこんにちは. 現在はfluctにてfluct DRという広告配信システムの開発を行っております, 大関です.

GitHub上でのチーム開発では, レビューの依頼や, CIが通ったことを確認した上でのPull Requestのマージといった複数の作業が発生しますが, これらはGitHubのUIを複数回クリックする必要があり, 非常にストレスフルな作業です.

本稿では, こうした定形作業を自動化するbotとしてpopukoを開発・導入することで, 我々開発者のストレスを軽減するとともに, より堅牢かつフィードバックの多い開発が実施できるようになった事例を紹介します.

GitHubでの開発はとてもクリック操作が多い

前段でも述べたように, GitHubを用いたチーム開発においては, 数多くの定形作業が存在します. コードレビューの可能な人を探してレビューを依頼する, 依頼の度に対象者をAssigneeに追加する, コードをレビューする, upstream(多くの場合においてmasterブランチ)とのコンフリクトが発生していないかを確認する, reviewerによる許可が出た後にマージボタンを押す, Pull Requestをupstreamにマージした後もCIがgreenのままでいるかを確認する, などなど. GitHubのデフォルトのUIだけでは, これらは手動で実施する必要があり, 非常にストレスフルな作業です.

また, それぞれのPull Requestの状態についてはラベルで管理していない限りは一覧した場合に判断しにくく, かといってラベル管理している場合はそれはそれでラベルを付け替える手間が発生してしまいます.

我々はソフトウェア開発者ですので, このような鬱屈とした作業に関しては直ちに自動化を行い, クリック作業を減らし, ストレスの多い生活から開放されなければなりません.

自動化したい

このようなGitHubにおけるPull Request作業の自動化という分野については, homuと呼ばれる高機能かつ素晴らしいbotが存在しています. homuはGitHubにおけるPull Requestのマージ処理の自動化やTravisCIとの連携など数多くの機能を持つbotです. かつてMozillaにてRust Languageのtech leadを務めていたGraydon Hoareの語った

The Not Rocket Science Rule Of Software Engineering: automatically maintain a repository of code that always passes all the tests

の思想に基づき開発されたborsの流れを組んでおり, Rust ProjectならびにServo Projectで使われています.

ですが, 本家側の更新は止まって久しく, MozillaにてforkされたバージョンはMozillaのビルドインフラやServoプロジェクトに合わせた変更がなされており, 他のプロジェクトが自前でホストする形式での利用を積極的に薦める状態にはありません.

自前でホストするのを諦めるのであれば, homu.ioを使用するという選択肢もあります. しかしながら, 使用するに際して, 運営元の不明瞭な外部サービスにプライベートリポジトリへのアクセスを認めることとなり, セキュリティ上の観点からは決して望ましいものではありません.

また, homuは各リポジトリごとのreviewerの設定を, 中央集権されたhomu向けの設定ファイルに記述することで管理しています. ですが, 私達は, このような設定に関しては各リポジトリごとに管理可能にし, それぞれのリポジトリ内でreviewerやbotの機能の設定を完結させたいという意志がありました.

これらの理由から私達は, 自分達の要求に足るだけの最小機能だけをpopukoとして再実装し, 私達のプロダクト開発に導入することとしました.

popuko

popukoは以下のモデルでPull Request駆動による開発を補助します

  • webhookを起点にして動作を行う
    • コメントの書き込みややpushイベントに反応して動作する
  • ラベルを用いた状態の可視化を行う
    • rebase必須, レビュー待ち, マージ待ち, など
  • 各リポジトリのトップレベルディレクトリに配置されたOWNERS.jsonファイルに書かれた設定に基づき動作する

この原則に基づきに, 現在, 以下の機能を実装しています.

  • Pull Requestに書き込まれたコメントに基づいて何かする
    • reviewerの割当とラベルを変更する
      • Pull Requestがapproveされたらラベルを変え, 一旦upstreamとマージした結果を試す仮ブランチを作り, CIがgreenになることが保証されたら正式にマージする.
  • upstream側が変更され, Pull Requestがマージできなくなった場合, push logとともに其の旨を当該Pull Requestに書き込む

ラベルによる状態の可視化

popukoは以下のラベルを用いてPull Requestの状態を管理します. このラベル名はServo Projectのラベル定義を参考にしています.

ラベル名 意味
S-awaiting-review レビュー待ち
S-needs-rebase conflictしてるのでrebaseが必要
S-awaiting-merge マージ待ち
S-fails-tests-with-upstream Auto-Mergeを用いてマージしようとしたらテストが失敗してしまった

具体的な機能

それでは, それぞれの機能について見ていきましょう.

reviewerをassignする

r? @<reviewer> の形式でPull Requestにコメントすることにより, <reviewer>をAssigneesに登録し, ラベルをS-awaiting-reviewに変更します

popukoがreviewerをAssigneesに登録してラベルを変更する例
f:id:saneyuki_s:20170210193146p:plain

masterとのconflictを検知する

masterブランチにpushが為されたことをwebhookのpushイベント経由で受取り, openなPull Requestを巡回します. 巡回したPull Requestのうち, conflictを起こしているものに対して以下を行います

  • ラベルをS-needs-rebaseに付与する
  • pushイベント経由で取得したchangeset urlをコメントし, コンフリクトの原因となっている可能性の高い変更をコメントする

popukoによるconflict通知の例
f:id:saneyuki_s:20170210193216p:plain

マージ可能であると知らせる

@<botname> r+もしくは@<botname> r=<reviewer>とコメントすることにより, 当該Pull Requestの最新のcommit hashと共にreviewerの名前をコメントに書き込み, ラベルをS-awaiting-mergeに変更します. 他の作業とは異なり, このコメントを書き込める(botが反応する)のは, OWNERS.jsonファイルによって予め指定されたユーザーだけです.

Auto-Merge

OWNERS.jsonauto_merge.enabledtrueに指定されている場合, popukoはPull Requestの自動マージを試みます. フローは以下の通り:

  1. reviewerが@<botname> r+とコメントする
  2. popukoはマージ先のブランチに対して仮ブランチ(autoと呼びます)を作成する
  3. 2で作成されたauto branchに対して, Pull Requestの内容を仮マージする
  4. auto branchに対してCIを実行する(TravisCIであれば, CI対象ブランチを制限していなければ自動的にCIが動き出す)
  5. CIが完了したら其の結果を確認し, greenであれば改めて正式にPull Requestをマージする

これにより, masterにマージされるPull Requestは, マージ後も含めて常にgreenとなることが自動的に保証された上でマージされるようになります.

popukoによるブランチの取扱の図
f:id:saneyuki_s:20170210190956j:plain

popukoによるauto mergeの例
f:id:saneyuki_s:20170210193120p:plain

OWNERS.json

各リポジトリごとのrootディレクトリに配置します.

ここに@<botname> r+を発行できるreviewerの名前を記載しておくことで, popukoはそれを用いて, 許可した人間のみがPull Requestにマージ可能である旨を示せるようにしています.

導入の結果, 何が変わったのか?

popukoの導入の結果, 当初の目的通り, 以下の点が達成されました.

  • 3~4クリックが1クリックに減った,
  • 常にmasterの状態がgreenのまま維持されるようになった

特に前者に関しては, SlackとGitHubの二重のレビュー依頼をしている同僚も多かったのですが, popukoにより, GitHubに書くだけで自動的にassignまで行われるようになったのでGitHubだけで完結させることのほうが合理的になりました. Slackでレビュー依頼が必要な場合は, 長期間放置されているものの催促や緊急性の高い内容に限られるようになり, Slack上での会話の濃度も上がりました.

後者についても, Continuous Delivery/Deploymentの観点から非常に役に立ちます. 「動かない(既存のテストケースで洗い出せる問題のある)コードをウッカリmasterブランチに対してlandしてしまう」という人為的ミスが起こらなくなるので, より堅実かつ問題の切り分けを行いやすいプロダクトリリースを実現するための礎となります.

また, 導入前は「これ本当に便利なの?」と懐疑的だった同僚も, 今では「無いと仕事にならない. 病み付きになる」と述べるようになるなど, 「無くても成立するが, 一度使ってみると離れがたい引力がある」種類の道具となりました.

併せて社内に#popukoという名前のbotのサポート・開発専用channelを用意し, popukoのリポジトリへの設定方法についても Popuko as a Service (PaaS) と銘打って文書化しているため, 使用したい社内のリポジトリについては各リポジトリの判断で自由に導入できるようにしています.

FAQ

popukoって名前の由来は?

popukoは Practical Organized Productive Unlimited Kabuki-nized Operation の略です

popukoに機能追加するくらいならhomuをforkしたほうが良かったのでは?

forkせずスクラッチしたのは出来心も多分にありますが, Goでスクラッチしたのは以下の技術的な判断です:

  • go-gihubという便利なGitHub APIラッパーがあった
  • GitHubのREST APIを大量に呼び出すという性質と, 数多くのリポジトリで同時多発的にトリガーとなるコメントが書き込まれ, それを捌くことを考えた場合, goroutineによる処理モデルは当botの扱う問題に適していると判断した
  • シングルバイナリを生成して適当な環境に放り込んで動くのは便利
    • ゲリラ的に正規導入するかわからないプロジェクトを始めるときは, Dockerコンテナとか作らずに, とにかく大雑把に動かせることが重要
    • アプリケーション性質的にはNode.jsでも良かったが, とにかく単一ファイルを放り込んで終わりにしたかったので, この点に見合わなかった.
  • 別にhomuの機能全部は要らなかった
    • 「こんなの直ぐに実装終わるだろ!」と思った

類似のhomu再実装は他にもいくつか存在しますが, 以下の理由からそれらの採用は行っていません.

  • mgattozzi/thearesia
    • これは現在でも未完成なので, 自然と対象外になった.
  • gullintanni/gullintanni
    • r? @<reviewer>のようなhighfive相当の機能が実装されておらず, highfiveのデプロイも必要な事を考えると, homu + highfiveの構成をそのまま用意するのと大差は無かった.
    • reviewerに相当する設定をリポジトリ単位にコードとして管理させる機能も見当たらなかった
  • rultor
  • GitHub botのためにJVMを動かしたくはなかった(JVMを多用しているチームであれば, 有効だったかもしれない)
    • コマンド体系が(私は)良いとは思えなかった
    • highfive相当の機能も実装されていない

そもそもとして「 botと対話するインターフェースによって自動化がなされる 」というのが重要な点です. それを実現する実装は必ずしも重要ではありません. 今後, popukoへの機能追加が見合わないと判断した場合, 改めてhomuをforkすることや類似のbotに乗り換えることも勿論有りえます.

SlackからGitHub botを操作するほうが良いのでは?

私は良いとは思っていません. GitHubはコードを取り扱うサービスであり, コードおよびGitの操作に関してはGitHub上で完結させるべきだと考えています。merge botのヘルスチェックや再起動はSlackからの操作でも良いと思いますが、Gitの操作に関してはGitHub上でbotへの指示がなされるべきです.

masterとのconflictを検知する」とあるけれども, popukoはmaster以外に向けたPull Requestに対しては検知できないの?

現在のところは検知できる実装にはなっていません.

根本的な前提として, 我々のチームについてはmaster以外のブランチに向けてPull Requestを送る開発習慣が存在せず, masterブランチをtrunkと見做す方式でのTrunk駆動開発に基づいてプロダクトの開発をしています. feature branchを切ることや, release branch専用のPull Requestを用意することは頻度として稀です.

また, 設定を各リポジトリ毎に分散して持たせている以上, masterに相当するブランチは各リポジトリごとに持たせるべきではあります. このdesign上の制約により, masterブランチ以外も自由にtrunkとして設定可能にすると, pushイベントが発生するたびに設定ファイル取得のためにGitHub APIを消費してしまうという問題が有ります.

こうした点を考慮し, 課題であるとは認識していますが, 私達の実用上の観点から直ちに修正するべき課題であるとは考えていません.

まとめ

以下がまとめとなります

  • 面倒くさいことは自動化しましょう
    • 便利なのでGitHubにmerge botを導入しよう
  • masterブランチ(trunk)が常にgreenであることが機械的に保証される安心感はとても良い

お知らせ

弊社には AJITO という社内バーがあり, 毎夜のようにエンジニアが現れ, 酒を嗜み, 軽食をつまみつつ, エンジニアリング談義を行っています( #ajitingで既にご存知の方も多いと思います). 「私も一緒に(こういう)エンジニアリング談義をしたい!」という方がいらっしゃいましたら, 是非とも弊社エンジニア or @tech_voyageに, お声掛けいただければと思います.

また, fluctを含むVOYAGE GROUPアドテクユニットでは, 一緒に働いてくださる仲間を募集しております. VOYAGE GROUPや広告配信に興味を持たれましたら, お気軽にご連絡ください.

2016年を締め括るROCK FESの話

こんにちは。@kanufyです。
みなさん、2016年はいかがでしたか?
楽しめましたか?エンジニアリングしましたか?ROCKしましたか?

VOYAGE GROUP Advent Calender 2016
最終日はROCKの話をしたいと思います。

会社でROCKするんだよ、それがROCKだ

弊社にはサークル活動 (※1)があります。
色々なサークルがあるのですが、音楽好きが集まり、なんかよくわかんないけどいろんな楽器やってワイワイしようぜ!っていうサークルがあります。
その名もVOYAROCKサークル!

今日は、先日12/22にVOYAROCKのコンサートが開催されたのでその様子をお伝えしようかと思います。

エンジニアも営業も法務も役職に限らず

VOYAROCKに限らず、弊社の様々なサークルメンバーは職種に限らず多種多様であり、サブメンバーを含めるとエンジニアから法務といった様々の職種の方が所属しています。
そのため、普段関わらなかった人とも関われる機会となり、サークル活動は社内の情報交換の場としても使われていることが多いです。

VOYAROCKランチコンサート

VOYAROCKでは、定期的にコンサートを開催しており、前回は8月末にゲーム音楽をテーマとして開催されました。
12/22に行われたVOYAROCKコンサートでは、映画音楽をテーマとしてセットリストが組まれました。

  • 戦場のメリークリスマス
  • Goodbyedays
  • 桜流し
  • なんでもないや

昔から最新の映画音楽ですね!
弊社のバースペースであるAJITOにて開催されました。

f:id:kanufy:20161224014433j:plain:w500

f:id:kanufy:20161224014452j:plain:w500

f:id:kanufy:20161224014512j:plain:w500

VOYAROCK FES

同日の夜には場所を変えて大会議室にて夜の部が開催されました!
セットリストは以下になります。

  • なんでもないや~Band ver.~
  • Stand by me
  • Shape of My Heart
  • Thinking Out Loud
  • 月のしずく
  • swallowtail butterfly ~あいのうた~

映画音楽じゃないものもありますが、お酒を飲みつつROCKに浸る。
これぞフェスですね!

f:id:kanufy:20161224225817j:plain:w500

f:id:kanufy:20161224081846j:plain:w500

フェスのあとにはおかわりタイムということで、各自やりたい曲をやったり即興でセッションが行われたり
アンコールをしたりと楽しい時間を過ごしました。
VOYAROCKではどんどん機材も充実してきており、PAのスキルアップもできたりしますよ(笑)

なお、この記事を書いているのは雪山から帰還中の新幹線の中です。
わたし実はスキースノボサークルの部長やってます。そう、サークル活動で合宿中です。ROCKですね。
サークル活動にクリスマスイブとか関係ないですよね!

それでは2017年もVOYAGE GROUPをよろしくお願いします。

企業ブースで実コード公開をした話 #phpcon2016

php

この記事はVOYAGE GROUP techlog / Advent Calendar 2016の記事として書いています。

こんにちは、@pro_shunsukeです。

VOYAGE GROUPはPHPカンファレンス2016にスポンサーとして協賛させていただきました。PHPカンファレンス2016に関してはこのブログの中で事前告知発表のあとがき、また企業ブースを出して学んだことなどを記事として掲載しています。今回でPHPカンファレンス2016に関する記事は4つ目という事になります。

さて、上記の記事にも詳しく書かれているのですが、弊社ではスポンサーとしてプレゼン発表や企業ブースを出展させていただきました。どちらもとても多くの方に興味を持っていただきました。今回はその中でも特に興味を持っていただいた 企業ブースでの実コード公開 をした際の知見ついて書こうと思います。実コードを見せるに至った経緯や当日の反応、また全体を通しての反省点を記事として残していこうと思います。

なぜ実コードを見せる事になったのか?

カンファレンス当日は弊社の@dkkomaによる「老舗メディアが改善に取り組んでいる話」という発表が行われました。

speakerdeck.com

17年間運営しているECナビというサービスについて、歴史を踏まえてどのように改善に取り組んできたのかについて発表しました。 しかしやはり発表だけではどうしても綺麗事のように聞こえてしまう部分があり、より具体的な取り組みが分からないのでは?という事になりました。そこで 実際に改善したコードの具体例を紹介 することで、見に来ていただいた人に1つ1つの地道な改善を実感していただきたいと思いました。

どのようなコードを選んだのか?

実際にプロダクトで使われているGithubの Pull Request(PR) を3つ選び、差分を見ていただきながら改善の様子を紹介したり、レビューの様子を紹介しました。3つのPRは、発表の中でもありましたKAIZEN会*1で出たコードを選びました。また、 ドメイン知識の必要のない外向けに分かりやすい内容のもの を選びました。

当日紹介した実コードの例

f:id:pro_shunsuke:20161217154120p:plain

当日の反応

とても興味をもっていただいた人が多く、さまざまな反応をいただきました。

反応していただいた意見

以下のような意見を多くいただきました。

  • 「分かる。大変だよね」
  • 「どうやったらうちの組織でも改善出来る体制になるだろう?」

地道な改善作業に対する共感の声がありました。

また、改善するのは大切だけれどなかなか手が出せない、どうしたらそのような体制づくりが出来るだろう?という相談も多かったです。難しい相談ですが、1つは長くサービスを続けていく上では継続的な改善が必要だという事に対するプロデューサーの理解があった事が大きかったと思います。

どのような人に興味を持っていただいたか?

興味をもっていただいた人は30代, 40代くらいのベテラン感のある人が多く、若者にはあまり響かなかったようでした。やはり「改善」に焦点を当てていたこともあって、業界の経験が豊富な人により響いたようでした。

全体としての反省

基本的にはとてもよかったのですが、いくつか反省点もありました。

体制について

予想を上回る反応をいただいた事はとても嬉しい事でしたが、 当初想定していた体制では興味を持っていただいた人全員には説明しきれなかった という事がありました。

最初は説明者1人とモニター1台で望みましたが、徐々に来場者が多くなるに連れて説明が間に合わなくなりました。当日は臨時で説明者を増やし 3人体制 にしたことで、ピークのランチタイムにはちょうど良くなりました。

お客さんの要望について

例えば以下のような要望がありました。

  • 「プロジェクト全体のディレクトリ構造はどうなっているの?」

当初はPRだけを見せるつもりでいたので、ディレクトリ構造までは見せることが出来ませんでした。しかし後になって考えてみたところ、ディレクトリ構造くらいなら見せても良かったのでは、ということになりました。 どこまでを見せてよく、どこまでは見せてはいけないのかは最初にバチッと決めておいた方がよかった です。

最後に

反省点はあったものの、当日は予想を上回る人に興味をもっていただきとても嬉しかったです!次回同じような機会がありましたらこの反省を活かして、より良いブースを出展出来たらと思っています。

また、発表やブースを見に来ていただいた方々に 少しでも改善の文化が伝わっていただけたら と思っています。

それでは明日の記事もお楽しみに!

*1:KAIZEN会についてはこちらの記事でも詳しく紹介しています。 ECナビ KAIZEN会を実施しました

CTOからの挑戦状2016 2ndを手伝った際に書いたPythonコードを晒してみる。

Python

この記事はVOYAGE GROUPのAdvent Calendar 2016 11日目の記事として書かれています。 techlog.voyagegroup.com

みなさんこんにちは。ara_ta3 と言います。
今回はVOYAGE GROUPとサポーターズで行ったイベント
CTOからの挑戦状 2016 2nd にて採点したり、
学生向けに解答を用意したりしたのでその時の話をします。

supporterz.jp

参加された学生の方に参考になれば幸いですし、
今回はPythonでのLintや型ヒント + mypyの実装も記載しているので、
それらの使い方の参考にもなれば幸いです。

CTOからの挑戦状 2016 2nd とは?

  • 問題を解いて賞金が得られる
  • 問題はLV1, LV2, LV3がある
  • LV1は早い者勝ちで賞金 が得られる
  • LV2, LV3はCTO含む3人のエンジニアがコードを読み、評価順に基づいて賞金が得られる
    • 例えば
      • 変数や、メソッド、関数の命名がある程度適切か
      • 適度に処理がメソッド、関数に切り分けられているか
      • テストケースは適切か
    • みたいなところを見たりしました。

問題について

LV1 ~ LV3の問題はgistに記述していて、それぞれ下記URLになります。
問題はピザや寿司の割引クーポンの最適化問題という感じですね。

Level.1 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

Level.2 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

Level.3 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

これらの問題を自分も解いてみたので、
そのコードで気をつけた所などをつらつらと書いてみようかと思います。
Pythonの解答数が最も多かったので、私もPythonで書いてみました。
Versionは3.5.2です。

書いてみたコード

準備

Python詳しくなかったので、開発環境周りをとりあえず調べてみました。

[PR]Pythonの環境については別途Qiitaで記事も書いているので是非ご覧いただければと・・・ qiita.com

ざっとやったこととしては

みたいなところです。

LV1

問題は簡単に言うと、
複数の割引クーポンを使ってピザ・寿司の支払金額を出来る限り少なくすると言ったものでした。

問題
Level.1 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

私の解答
CTO Challenge 2016 2nd LV1 · GitHub

主に書いたコードは下記のような感じです。
単純に高い割引額のクーポンから使って行けば良さそうですね。
とりあえずLV1のケースをテストしました。
for文の中くらいのスコープなら適当な変数名でもいいけど、
少し広めなスコープのときは妥当そうな命名をつけるようにしました。

また、"準備" の部分に色々書いたりしましたが、
このタイミングではあまり複雑じゃなかったのでmypyは入れませんでした。

lowest_amount_for_using_coupon = 1000


def select_optimum_combination(amount, coupons):
    if amount <= lowest_amount_for_using_coupon:
        return []
    sorted_coupons = sorted(coupons, reverse=True)
    rest = amount
    used_coupons = []
    for c in sorted_coupons:
        if rest < c:
            break
        rest -= c
        used_coupons.append(c)

    return used_coupons


def test_select_optimum_combination_case1():
    selected = select_optimum_combination(1000, [500, 500, 200, 100, 100, 100])
    assert selected == []


def test_select_optimum_combination_case2():
    selected = select_optimum_combination(1210, [])
    assert selected == []


def test_select_optimum_combination_case3():
    selected = select_optimum_combination(1210, [500, 500, 200, 100, 100, 100])
    assert selected == [500, 500, 200]


def test_select_optimum_combination_case4():
    selected = select_optimum_combination(1530, [500, 500, 200, 100, 100, 100])
    assert selected == [500, 500, 200, 100, 100, 100]


def test_クーポンが降順になっていなくても割引額が高い順に割り引いてくれること():
    selected = select_optimum_combination(1530, [500, 200, 100, 500, 100, 100])
    assert selected == [500, 500, 200, 100, 100, 100]

LV2

LV2では少し仕様が増えました。
入力がLV1では金額でしたが、メニューに変わりました。
また、クーポンの使用回数に制限が加わりました。
さらにメニューの種類によって使えるクーポンが増えました。
めんどくさいですね

問題
Level.2 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

私の解答
CTO Challenge 2016 2nd LV2 · GitHub

CouponSelectorというサービスクラスにロジックを入れました。
テスト済のLV1で使った関数はそのまま利用したかったので、
CouponSelectorクラスのクラスメソッドとしてリファクタリングしました。
このタイミングで型ヒントを入れてみました。
メソッドの引数にCouponやMenuクラスを含むDictやListなどの少し複雑な型を渡したくなり、
それら以外は渡さないで欲しいことを明示的にわかるようにしたくて入れました。
Python3.5.2現在では型ヒントだけではPythonの処理系はなにもしてくれないのですが、
mypyで意図しない型の入力がないか確認しています。

coupon_selector.py

from typing import List, Dict
from functools import reduce
from collections import Counter
from menu import Menu

Coupon = int


class CouponSelector():
    def __init__(self,
                 limitation_of_coupons: Dict[Coupon, int],
                 coupons_available_for_pizza: List[Coupon],
                 lowest_amount_for_using_coupon: int
                 ) -> None:
        self.limitation_of_coupons = limitation_of_coupons
        self.coupons_available_for_pizza = coupons_available_for_pizza
        self.lowest_amount_for_using_coupon = lowest_amount_for_using_coupon

    def select_optimum_combination_by_menus(
            self,
            menus: List[Menu],
            coupons: List[Coupon]):

        amount = reduce(lambda amount, m: amount + m.price, menus, 0)
        available_coupons = _filter_limited_coupons(
            coupons,
            self.limitation_of_coupons
        )
        if not _pizza_exists(menus):
            available_coupons = [c for c in available_coupons
                                 if c not in self.coupons_available_for_pizza]
        return self.select_optimum_combination(amount, available_coupons)

    def select_optimum_combination(self, amount: int, coupons: List[Coupon]):
        if amount <= self.lowest_amount_for_using_coupon:
            return []
        sorted_coupons = sorted(coupons, reverse=True)
        rest = amount
        used_coupons = []
        for c in sorted_coupons:
            if rest < c:
                break
            rest -= c
            used_coupons.append(c)

        return used_coupons


def _pizza_exists(menus: List[Menu]):
    return any(m.is_pizza() for m in menus)


def _filter_limited_coupons(
        coupons: List[Coupon],
        coupon2limit: Dict[Coupon, int]):
    counter = Counter(coupons)
    for c, n in counter.items():
        counter[c] = min(coupon2limit[c], n)
    return list(counter.elements())

menu.py

from typing import Dict, List, Tuple


class Menu(object):
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def is_pizza(self):
        return type(self) == PizzaMenu


class PizzaMenu(Menu):
    def __init__(self, name, size, price):
        super(PizzaMenu, self).__init__(name, price)
        self.size = size


class SideMenu(Menu):
    def __init__(self, name, price):
        super(SideMenu, self).__init__(name, price)

テストケースはこちら

LV3

LV3ではセットメニューという概念が加わりました。
クーポンを使うよりもセットメニューの方がお得など、なんとなくありそうな仕様ですね。
セットメニュー特定のサイドメニューが無料になるものもあれば、
任意のサイドメニューが無料になるもの、
具体的な割引額が決まっているものもあります。
またセットメニューを頼める時間帯が決まっているものもあるようです。
仕様がめんどくさいですね。

クーポンの割引額とセットメニューの割引額をそれぞれで出して両者を比較すればいいかなと考えました。
なので比較する部分を除くと、LV2から追加されたのはSetMenuのディスカウント額を計算するプログラムのみです。

問題
Level.3 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

私の解答
CTO Challenge 2016 2nd LV3 · GitHub

SetMenuDiscounterというサービスクラスを作成しました。
処理の流れは
注文されたメニューと注文された時間から利用可能なセットメニュー一覧を取得し、
一覧の中で最も高い割引額のセットメニューを決めて返す。
と言ったものです。
工夫したのは、クーポンが利用可能か判定する部分を関数で表現しました。
仕様に1種類しかないので、抽象化するにはまだ早いかなと考えたためです。
今見返してみて、 SetMenuDiscount クラスのどのメソッドを見るべきが悩んだので、
privateメソッドかどうかも意識して書けばよかったなと思いました。

from datetime import datetime
from typing import List, Tuple, Callable
from menu import Menu


class SetMenuDiscounter():
    def __init__(self, discounts: List[SetMenuDiscount])->None:
        self.discounts = discounts

    def decide_discount(self,
                        ordered: List[Menu],
                        current_time: datetime
                        )->Tuple[SetMenuDiscount, int]:

        availables = [d for d in self.discounts
                      if d.is_available(ordered, current_time)]
        if not availables:
            return (None, 0)

        best, *rest = availables
        best_discount = best.calculate_discount(ordered)
        for d in rest:
            p = d.calculate_discount(ordered)
            if best_discount < p:
                best = d
                best_discount = p
        return (best, best_discount)


class SetMenuDiscount(object):
    def __init__(self, name: str,
                 required_menus_combinations: List[List[Menu]],
                 available_time_rules: List[Callable[[datetime], bool]],
                 discount_target_menus: List[Menu],
                 discount: int = None)->None:
        self.name = name
        self.required_menus_combinations = required_menus_combinations
        self.available_time_rules = available_time_rules
        self.discount_target_menus = discount_target_menus
        self.discount = discount

    def is_available(self, ordered_menus: List[Menu],
                     ordered_time: datetime)->bool:
        return self.in_available_period(ordered_time) \
            and self.is_matched(ordered_menus)

    def is_matched(self, ordered_menus: List[Menu])->bool:
        ordered_menu_set = set(ordered_menus)
        for combination in self.required_menus_combinations:
            c = set(combination)
            if c.intersection(ordered_menu_set) == c:
                return True
        return False

    def in_available_period(self, ordered_time: datetime)->bool:
        if self.available_time_rules is None:
            return True
        return any(map(lambda fn: fn(ordered_time), self.available_time_rules))

    def get_target_discount_menus(self, ordered_menus: List[Menu])->List[Menu]:
        t = set(self.discount_target_menus).intersection(set(ordered_menus))
        return list(t)

    def has_amount_discounts(self)->bool:
        return self.discount is not None

    def has_menu_discounts(self)->bool:
        return len(self.discount_target_menus) > 0

    def calculate_discount(self, ordered_menus: List[Menu])->int:
        price = 0
        if self.has_menu_discounts():
            menus = self.get_target_discount_menus(ordered_menus)
            prices = map(lambda m: m.price, menus)
            m = max(prices, default=0)
            price = max(price, m)
        if self.has_amount_discounts():
            price = max(price, self.discount)
        return price

テストケース

まとめ

CTOからの挑戦状 2016 2ndの際に書いたLV1, LV2, LV3のコードを晒してみました。
それぞれほんの少しですが、意図や工夫した点を書いてみました。
工夫した点を少しまとめると

  • coreなコード(ここで言うとcoupon selector)に仕様的なものが含まれないようにしました。
    • クーポンの具体的な割引額や使える枚数の制限など。
  • パフォーマンスを考えるよりも処理の流れがわかりやすくなるように心がけました。
    • クーポンが1億枚手元にあるからそこから選ぼうということはあまりないかなと考えてます。
  • 変数などの命名は出来る限りわかりやすくするようにしました。
  • Lintや型チェックなどを入れてテストを書かなくてもエラーがある程度わかるようにしました。

説明がわかりづらい、ここなんでこんなクソ実装なんだ!
とかありましたら是非我らがAJITOでお話でもできればと思うので、お声がけください!w

voyagegroup.com

感想

  • テストケースに不備があったらごめんなさい・・・指摘頂ければ幸いです。
  • あまり触ったことないコードに触るとき形(コーディング規約やLintなど)から入るといい感じにそれっぽくかけるから良い
    • ただし、エディタ設定沼にはまらないように注意
  • CTOからの挑戦状のコード書くの大変・・・w
  • 型ヒントとライブラリで頑張るならコンパイルでわかる(言語がサポートしてくれている)方が個人的にはいいなぁ

Google SpreadSheetをサーバーレスっぽく使ってみる

これは VOYAGE GROUP Advent Canlendar 2016 の 9日目のエントリです。

VOYAGE MARKETINGの @katzchum (ちゃむ)です。

先日のLTのネタで作成したアプリの技術的なフォローです。
過去記事ですが、LTの様子はこんな感じです。

techlog.voyagegroup.com

今回のLTの中でつくってみた系として発表したのが、
チェックインされた場所の一覧を時系列に検索するアプリです。

check map-in

https://k2tzumi.github.io/check-mapin

LTおもしろネタの題材として過去のチェックインリストを紹介する為に、視覚化を行うアプリケーションとして作成しました。 *1
(データの元ネタは自虐的なものなので、敢えて明言しませんが。。)

システム構成

LTなのでわざわざコストをかけて環境を構築するのもなんだかな〜と思いserverless チックなシステム構成としました。

Web Servergithub.io
ApplicationJavaScript
(Framework : JQuery(-ui))
Cache system WebStorage
* sessionStorage
* localStorage
BaaS Google Map API v.3
Rakuten WEB SERVICE(楽天トラベル系API)
Database Google SpreadSheet
(Query language : Google Visualization API)
Data seed データ収集 / Scraper (Google Chrome Extension)
データ集計・加工 / COUNTIF, SUMIF, VLOOKUP, DATEDIF(Google SpreadSheet)
スクレイピング / IMPORTXML(Google SpreadSheet)

Webサーバーもgithub.ioを利用して構築手間を省く、ずぼらっぷりとなりました。
アプリケーションの配信の手間もはぶけて楽チンです。

データベース周り

視覚化対象のデータは楽天トラベルの過去予約照会から引っ張ってきました。
データはGoogle SpreadSheetへ入力していきました。
ただデータの入力方法を考える必要がありました。
自身が考えていたよりもデータ件数が多かったのと、チェックインの履歴と住所情報を紐付けるのに、照会ページのUI(HTML構造)では手数が多く難しそうでした。

スクリプトを組んでデータ収集することも考えましたが見送りました。
認証をパスしないといけないのが手間なのと、ワンオフで問題ないのでコストが見合わないとの判断です。

ここでも手間を省くために以下のアプローチを採用しました。

  1. Google Chromeで楽天トラベルへアクセスし、extensionで必要なデータだけ抽出する
  2. Google SpreadSheetへ入力して、データの加工及び補足情報の追加はGoogle SpreadSheet側に任せる

1のデータ抽出はScraperを利用しました。
Scraperを利用すれば、認証後のページでもちょこっとしたスクレイピングは難なくこなせます。
使い方は、抜き出したいページの要素をXPathで指定するだけです。

こんなページを f:id:katzumi:20161127141011p:plain

こんな感じで抜けます。 f:id:katzumi:20161127141052p:plain

指定するXPath自体も Chromeのデベロッパーツールで調べられます。
前回の記事でも書きましたが、Chromeはこういう所でもやっぱり便利です。

2のデータ加工はスプレッドシートの関数で対応出来ます。
収集した予約履歴ではそのままではGoogle SpreadSheet側に正しい日付として判断されないので、そちらの加工等にも利用しました。
続いて補足情報の追加は IMPORTXML というExcelにはない便利な関数を利用します。

=IMPORTXML("http://techlog.voyagegroup.com/entry/2016/07/25/080000", "//title")

とすると

モバイルファーストなサービス開発におけるDockerの活用術 - VOYAGE GROUP techlog

指定したURLのタイトルを取得できたりします。

こちらの関数を利用してデータ加工で抜き出した施設IDをRakuten WEB SERVICEのREST API のエンドポイントURLを組み立てて呼び出します。
レスポンスをXML形式で取得できるのでXPathで必要な項目のみを抽出します。

同じ施設IDが複数存在した場合を考慮して、ユニークな施設IDのみを別シートにして IMPORTXML で補足情報を読み込んでおいて、実際のデータは VLOOKUP で補足情報を関連付けしておきました。
その他、チェックイン回数など事前集計できるものについては集計関数で計算を行っておきました。

最終的にはこんな感じのデータを作成しました。

f:id:katzumi:20161127142850p:plain

フロント周り

フロント周りは日付範囲のスライダー(jQRangeSlider)を利用したかったので JQueryを採用しました。
JQueryでゴリゴリとバックエンドのAPIを呼び出しして地図表示を行っています。

GoogleSpreadsheetにはデータアクセス用のAPIが幾つか用意されていますが、今回はSQLライクに参照ができる Google Visualization API を利用しました。

  var query = new google.visualization.Query('//spreadsheets.google.com/tq?key=' + key + '&pub=1');

  query.setQuery('SELECT B, E, MAX(N), MAX(O), SUM(M), COUNT(F) WHERE ((G >= ' + min_serial + ' AND G <= ' + max_serial + ') OR (J >= ' + min_serial + ' AND J <= ' + max_serial + ')) GROUP BY B, E ORDER BY MAX(O) DESC');

上記がクエリ発行部分になります。
基本的な選択、集計、ソートを行うことができます。
ただやはり複雑なサブクエリの発行や複数シートを跨るクエリ発行までは行えないので、一工夫が必要となります。
上述のデータベース側の対応で記載したとおり、検索やデータ出力し易い様にVLOOKUP関数等でデータを加工しておくことでカバーが出来るかと思います。

今回の実装例では、Google SpreadSheetの関数とGoogle Visualization APIのクエリを組み合わせを行って

  • 全期間でのチェックイン回数(滞在期間)
  • 対象期間でのチェックイン回数(滞在期間)
  • 対象期間内でのチェックイン回数順位

を表示させることを実現しています。

施設名と住所の一覧をGoogle Visualization APIから取得した後の地図(住所のピン)の表示はGoogle Map APIを利用しています。
住所のピン表示は住所から緯度経度をGeocoding APIで求めてから行っています。 *2
ピンをクリックして施設名のリンク表示する際もRakuten WEB SERVICEから情報を取得して行っています。

データ件数が多いので、そのままAPIを呼び出すと、 query over limit に引っかかるので2つの対策を行っています。

  1. APIをレスポンスをキャッシュする
  2. キャッシュヒットミスした場合にWait追加

レスポンスキャッシュはWebStorageを利用しました。
KeyValueでキャッシュすることができますので、APIの検索キーをハッシュ化してKey指定、 value はAPIのレスポンスのJSONを stringify して格納しました。
Javascript でwait処理をUIをロックさせない方法で実装するのは悩んだのですが、APIの呼び出し中の回数をカウントして、閾値を超えた場合にAPIを遅延実行する秒数を調整する方法を取りました。
ここら辺の query over limit を超えない範囲で、うまくAPIの並列性を保つ方法をもしご存知の方がいらっしゃいましたら、 @katzchum宛 もしくはブクマコメで指摘して頂けると助かります。

感想など

  • 課題・反省点
    jsでAPIをごりごり呼び出ししているので Deferred の嵐になってしまった。
    もう少しスッキリかけると良いかな〜と。
  • 得られたもの
    Google SpreadSheetを使えたのは、色々学びがありました。
    Google SpreadSheet自体でスクレイピングができたのは嬉しい発見でした。
    今回は利用しませんでしたが、Spread Sheet APIでデータ更新を行えばバックエンドサービスとして色々広がるなと感じました。
    又、Spreadsheetに入力していたデータをSQLで参照させるのはMicrosoft Accessっぽい感じがして面白かったです。
    今回の様な簡単なシステムでサーバーを調達するまでもないものにはフィットするのでは?と感じました。
  • LTをやってみた感想
    ランキングも発表したのですが、Slackのonairチャネルで先読みされて面白かったです。 皆 AJITING大好き。橙を根城にしている90年台のネタが通じる素敵な おじ様 猛者達がいることを再確認できました。

明日のエントリもお楽しみに!

*1:開発動機として、最近ロケーション情報を利用したアプリが増えてきているのでジオコーディングをやってみるか〜と思って作ってみました。

*2:Rakuten APIでもgeocodeが取れたりしますが、検証の為に敢えて住所緯度変換を行っています。

社内カフェのススメ

これは VOYAGE GROUP Advent Canlendar 2016 の8日目のエントリです。

お暇なときにお読みください。

今回は弊社の社内Bar AJITO...ではなく、社内カフェ Garden について書きます。

ガーデンとは

一言で言うと、「バリスタ常駐型の社内カフェ」です。

バリスタさんがドリップしてくれたおいしいコーヒーを、なんとたった200円で飲むことができます。驚きです。

ホット/アイスに加えて水出しコーヒー(一晩かけて水で抽出したコーヒー)というものもあります。

おしゃれなサイトとInstagramがあるのでぜひ見てみてください。

Garden

動画がおしゃれ。

Instagram (@officecafe_garden)

ちなみに先週のメニューはこんな感じでした。

f:id:saxsir256:20161207194151j:plain

豆がなくなると、また新しい種類が入ったり入れ替わりもあるので飽きずに通えます。

オススメのコーヒー

せっかくなのでバリスタさんにオススメの豆を聞いてみました。

  • 一般の店だと飲めないような海外の豆が飲める
  • しかも200円
  • 味は浅煎りから深入りまで、満遍なく揃えている

ということで、どれもおすすめらしいですが一番はなんですか?と聞いてみたところ エチオピア がオススメとのこと。

コーヒーの概念を覆す

どことなく紅茶のようで、苦手な人でも飲みやすい

コーヒーが苦手な人でも、これなら飲めるという人が多い

by バリスタのJさん

コーヒーを飲んでる間にクルー(社員)同士で軽く仕事の話をしたり、詰まってた実装のひらめきが降ってくることもあったり、私自身は日に1, 2回くらいコーヒーを飲んでます。

カフェインが苦手な人向けにデカフェ(カフェインレス)のコーヒーも用意してあったりします。

ちなみに個人的なオススメは

  • コスタリカ

    • 浅煎りコーヒーが好きになったきっかけ
  • ベラフィンカ(水出し)

    • 二日酔いの朝に効きます
  • エチオピア

    • 迷ったときのエチオピア

です。

f:id:saxsir256:20161207194108j:plain

美味しいコーヒーがお手頃価格で飲めるのはもちろん、バリスタさんがいつも頼む豆とか好みを覚えてくれているのはとても嬉しいです。

あと、私はタバコを吸わないのですがふとした時にコーヒーでも飲みに行くかーと席を立つきっかけになるので重宝しています。

というわけで、社内カフェのススメでした。

イニシャルコスト0円で導入できるらしいので、興味がある方はホームページを見てみると良いかもしれません。

Garden