TECH PLAY

dely株式会社

dely株式会社 の技術ブログ

236

こんにちは、dely の Android チームで施策をやりながらアプリ改善に取り組んでいる tummy です。 以下記事を書いてからもう 2 ヶ月が経過し、Android チームも 1 人増えて 4 人になりました。この 3 ヶ月でメンバーの数が倍になっています(すごい) tech.dely.jp この 3 ヶ月間主に 阿吽の呼吸でまかなえていたことを掘り起こして文章化し、チームの共通認識を作るところ に注力していました。今回はその取り組みについて紹介できればと思います。 リリースマネージャーのような役割になってみる リリースサイクルの策定 設計について議論したりする場所が爆誕 議事録や仕様書の共同編集化 プロダクトレビューについての内容の明確化 まとめ リリースマネージャーのような役割になってみる Android チームは 1 人 1 つ何かしらの施策を担当しており、同時並行で進んでいます。2〜3 人の時期は、bot のレオくんが水曜日になるとそろそろリリースしない?とお知らせしてくれるので、それをみたときにリリースするかどうか決めていました。 レオくんがお知らせしてくれている様子 しかし 3 人中 2 人が大きめの施策を担当しているのもあり、なかなかリリースしようという雰囲気になることが少なかったです。そして、4 人目がジョインした今月、 細かい施策が爆速で進むようになった結果、1 週間経った時点でマージされているプルリクが 10 を超えるようになりました。 さすがにこれはコンスタントにリリースしていかないとやばいと思い、リリースマネージャーのようなことをするようになりました。とはいってもそこまで形式張ったことはやっておらず、主に以下のようなことをしています。 リリースサイクルの策定(次に詳細を書きます) リリース作業、及びその効率化 以下の記事で書かれているように、弊社の Android CI は bot が大変活躍してくれているので、より多くの過程を bot にまかせられるように実装 & 調整中です。 tech.dely.jp リリースサイクルの策定 やりました。経緯は上記に書いたとおりです。 プロダクトレビューという文化が弊社にはある ため、そのレビュー結果への対応期間も考慮しつつも、基本的には水曜日にコードフリーズ & デバッグ、何もなければ木曜日リリースというフローを踏んでいます。 リリースノートの自動化など、このサイクル内でもまだ自動化できそうな箇所があるので、引き続き模索していきます。 設計について議論したりする場所が爆誕 クラシル Android の最初のコミットログ クラシル Android は 4 年もののアプリで、設計も複数パターンあったりしてとても複雑になっています。メンバーが 3 人になり、こういった箇所にもだんだん手を入れられるようになってきたことと、過去の事情によって実装されている箇所などを質問する場として Android エンジニア MTG が生まれました。 4 人になった現在は設計をこれからどうしていくか、コードスタイルについてどれを採用していくかなど、古くからあった懸念への対応方針について議論することが多くなり、改善方面にも力を入れられているのでとても活発になっています。(次回で 6 回目まで来ました 🙆) 議事録や仕様書の共同編集化 プロダクトレビュー等を通してガンガン仕様が変わる弊社ですが、仕様書が過去のまま更新されず、最新状態がわからないという問題がありました。また、それに伴い仕様の認識のズレが起きてしまっていました。 そこで、Qiita に共同編集にしたほうがいいものとしなくてもいいものを画像つきで記事にしてみました。 この記事がきっかけになったのか、積極的に共同編集にしてくれる方が増えてきててとても嬉しいです。 まだ問題は残っていますが、解決の一歩になったのではないかと思っています。 プロダクトレビューについての内容の明確化 プロダクトレビューは CXO の坪田さん発信で始まったものですが、なぜ?何をみてるの?という観点が明確ではありませんでした。そのせいか、せっかくのレビューの機会でぎくしゃくした雰囲気になることがありました。 そこで、自分が集められる情報を Qiita にまとめて坪田さんに共有、ヒアリングすることで足りない箇所をアップデートしました。坪田さんにとてもわかりやすい資料も作成いただいて、全体に共有されました。これからどんどんこの考えが浸透していくといいなと思いますし、自分も意識したいなと思います。 全員参加型のチームを作りたい願いを込めたオンボーディング資料の一部 pic.twitter.com/ryUZCu1rgE — 坪田 朋 / クラシル (@tsubotax) 2020年2月5日 まとめ ざーっと今までやってきた取り組みをご紹介しました。まだまだやれることはたくさんあるので、引き続きやっていきます。 また新しい取り組みを始めた際にはブログでお知らせできればと思います! dely では様々なポジションのエンジニアを積極採用中です!興味がある方はぜひご連絡ください!
アバター
はじめまして。開発部の sakura818uuu です。 CS(カスタマーサクセス)チームのサポートエンジニアを始めて2ヶ月が 経過しました。 今回は、ユーザーお問い合わせの技術的サポートについて 具体的にどんなことをやっているかをご紹介しようかと思います。 はじめに どんなお問い合わせが多いのか? どうやってサポートしているのか 1.ツール 2.データ 3.開発部内で相談 さいごに はじめに クラシルでは、ユーザーさんが困った時にお問いあわせをするページがあります。 お問い合わせの内容は課金やメルマガ、不具合の報告など様々です。 お問い合わせがあるとCSチームがユーザーさんとのやり取りを行います。 技術的サポートが必要な場合は、開発部のCS技術的サポート役にご相談 いただくフローになっています。 去年の12月から私が技術的なサポート役を担当しています。 どんなお問い合わせが多いのか? 課金やメルマガ、不具合のお問い合わせが多いです。 ※技術的サポートをしたお問い合わせに限るため、お問い合わせ全体のデータではありません。 お問い合わせの対応件数などを可視化して社内に共有をしています。 月によってお問い合わせ内容の傾向は違います。 新機能追加やマーケティング施策によってお問い合わせ内容の傾向は変化する ので、 日々のキャッチアップが非常に重要なことを身にしみて感じています。 どうやってサポートしているのか サポート方法はケースバイケースですが、主に3種類の方法があります。 1.ツール お問い合わせの内容に合わせて様々なツールを使います。 ツールでしか得られない情報や設定できない情報があるためです。 調査/設定した結果をもとにCSチームにご連絡します。 メルマガツール・課金管理ツールなど扱うツールの数はたくさんあります。 この2ヶ月でツールの使い方は全て社内にドキュメント化しました。 Qiita:Teamに数十個の記事があります ドキュメント化したことで確認する時間が減り対応時間が削減されました。 2.データ お問い合わせを調査する時に、ツールを使う以外に SQLでデータを出して調査することも多いです。 データを出して調査した結果をもとにCSチームにご連絡します。 主にredashでクエリを書いています。 CSの調査でよく使うクエリはお気に入り登録して、すぐ使えるようにしています。 参考:redashのクエリお気に入り登録について tech.dely.jp 3.開発部内で相談 アプリの不具合やサーバーサイドが絡んできそうなお問い合わせには、 私だけで解決できないので開発部内のメンバーと相談します。 開発部とCSチームのハブになって動きます。 ハブの役割は以下のようなことをしています。 <開発部へ伝えるとき> ・エンジニア側が調査しやすい情報を事前に揃える ・いつから何件発生しているかなど対応の温度感を伝える <CSチームへ伝えるとき> ・技術的な用語を使わずに原因を説明 ・調査に時間がかかりそうな場合の調整や対応検討 開発部のメンバーとはなるべく密に連絡を取り合うようにしています。 いつも迅速に対応していただきすごく助かっています。 さいごに CS(カスタマーサクセス)チームのサポートエンジニアとして ユーザーお問い合わせにどのように対応しているか をご紹介しました。 多種多様なお問い合わせが来るため調査が難しいこともありますが、 CSチームや開発部に助けてもらっています。 「迅速に、丁寧に」ユーザーさんのお問い合わせに答えられるよう これからもサポートしていけたらと思います。 delyの開発チームについて詳しく知りたい方はこちらをどうぞ
アバター
はじめまして。sakura818uuuです。 Zendeskのチケットをslackに通知する方法をご紹介します。 はじめに 手順方法 Incoming Webhookの設定 Zendesk拡張機能の設定 Zendeskトリガの設定 完成したらどうなるか はじめに 現在、私はCS(カスタマーサクセス)チームの技術的なサポートを行っています。 note.com delyのCSチームでは、 Zendesk を去年から使い始めました。 Zendeskのチケットが届いたり更新されたりすると、slackに通知が飛ぶようにしておりとても便利です。 このブログでは、Zendeskのチケットをslackに通知する方法をご紹介します。 手順方法 Incoming Webhookの設定、Zendesk拡張機能の設定、Zendeskトリガの設定を順に行っていきます。 Incoming Webhookの設定 この ページ でIncoming Webhookと検索・選択します 2. 「Slackに追加」ボタンを押します 3. Zendeskの通知を飛ばしたいslackのチャンネルを検索・指定します 4. チャンネルの指定が完了したら「Incoming Webhook インテグレーションの追加」を押します 5.インテグレーションの設定 にある Webhook URL をコピーします。 このコピーしたURLは次のZendesk拡張機能の設定で使用します。 Zendesk拡張機能の設定 Zendeskにログインします ホーム画面から歯車マークを押し設定ページに飛びます 3. 設定の拡張機能を選択します 4. ターゲットを追加を選択します 5. HTTPターゲットを選択します 6. HTTPターゲットの所定の欄を埋めていきます。 ↓デフォルト ↓埋めたもの ・タイトルはご自由に ・URLはIncoming Webhookの設定の5の手順で得たURLを貼り付け ・方法はGETからPOSTに変更 ・コンテンツタイプはJSON (コンテンツタイプはデフォルトでは表示されていませんが、方法をPOSTにすると表示されます) ・基本認証は今回はチェックなし 7. 送信ボタンの横にある選択肢をターゲットのテストからターゲットの作成に変更します 8. 送信ボタンを押します 9. ターゲットが作成されたら完了です Zendeskトリガの設定 Zendeskにログインします 2. ホーム画面から歯車マークを押し設定ページに飛びます 3. ビジネスルールのトリガを選択します 4. トリガを追加 を選択します 5. トリガ名と説明を書きます 6. 条件を設定します。ここの条件は使いやすいよう適宜変更してください。 今回の条件: 「チケットが作成され、ステータスが解決済みではなく、チケットにパブリックコメントがあるとき」 7. アクションを設定します。 まず、「ターゲットに通知」を選択します。 次に、「ターゲットに通知」の横枠に Zendeskの拡張機能の設定の9 で設定したターゲット名前が出てくるので選択します。 ここでは横枠に「テストです。」を選択しました。 コピペしやすい用 { "attachments":[ { "fallback":"新しいお問い合わせがありました", "pretext":"新しいお問い合わせがありました", "color":"#D00000", "fields":[ { "title":"件名: {{ticket.title}}", "value":"URL: {{ticket.url}}" } ] } ] } JSONボディももちろんカスタマイズ可能です 参考: Slack API attachmentsチートシート - Qiita 8. 作成ボタンを押します 9.トリガが作成されていれば完了です 完成したらどうなるか Incoming Webhookの設定、Zendesk拡張機能の設定、Zendeskトリガの設定が完了すると Zendeskからslackに通知が届くようになります🎉
アバター
こんにちは。delyのSREの井上です。 delyは先日開催された SRE NEXT 2020 にGOLDスポンサーとして協賛をさせていただきました!当日はセッション枠を頂き、「delyにおける安定性とアジリティ両立に向けたアプローチ」をテーマに発表もさせていただきました。 セッションでは、 前半:SRE本に則った理論の話 SREはプロダクト開発の速度を安全に高めるために存在しているということ プロダクト開発の速度を安全に高めるためには単純さを追求することが重要であること 後半:前半の理論に則ったdelyでの実践の話 をしました。スライドは公開済みですが、それだけだと伝わりにくい内容も含めてブログにも投稿させていただきます。 当日は多くの方が参加されていました! SRE NEXTに参加してみて SRE Loungeの勉強会は#5以降の会は全て参加させて頂いているのですが、いつも有意義な情報が得られて登壇者や運営の方には感謝しかないなと思っていました。なので、SRE NEXTでスポンサーという形でコミュニティに貢献できたことは良かったなと感じています。登壇の機会を頂けたことも感謝しかなく、今後も引き続きよろしくお願いしますという感じです。 個人的にすごく印象に残ったのは SREの今後 というトピックでした。時代の変化でアジャイル開発やスクラム開発が当たり前の存在になったように、おそらく SLOやエラーバジェットも今後何かのフレームワークに落とし込まれて開発文化に取り入れられていく のだろうと思いました。そうなるとSREの存在意義はほとんどなくなるので、どういった形に役割が変化していくのかなというところを考えるのも面白そうだなと思いました。 目次 目次 SREの存在意義 旧来のサービス運用と組織分離 組織分離によって発生するコストとSRE コストに対するSREのプラクティス 対立構造が引き起こすプロダクト開発速度の低下 開発速度/安定性のバランスのコントロール エラーバジェット SREの存在意義の再確認 エラーバジェットとは別のアプローチ エラーバジェット導入の難易度 想定外の複雑さとSRE 単純さの追求による開発速度と安定性の両方の向上 delyでの実践 規模とフェーズ 課題 具体的なアプローチ 想定外の複雑さを減らすアプローチ 改善MTG 課題洗い出し会 リファクタリング計画 想定外の複雑さを増やさないアプローチ 設計レビュー 意識 目的と手段の可視化 さいごに SREの存在意義 SREは「信頼性を高めるため」ではなく、「プロダクト開発の速度を安全に高める」ために存在しているということを出来る限り分かりやすく伝えるべく、SRE本に記載されている内容を元に順序立てて説明をしていきました。 SREが成し遂げようとしていることは結局SREだけでは達成できず、必ずプロダクト開発チームやマネジメント層との連携が必要になります。他組織と連携するためにもSREが何をしようとしているのかを誰もが理解できるように、可能な限り噛み砕いてスライドに落とし込みました。 旧来のサービス運用と組織分離 AWSやGCPが存在しない時代における「システム管理者」の業務は、既存ソフトウェアの活用だったので、サービスの運用業務はソフトウェアの開発業務とは別のスキルセットが必要とされました。 サービスが成長してくると、必然的に運用業務に人手が必要になるので人員調達の観点からほとんどの企業で 開発と運用の組織が分離 されました。 この組織分離には人員調達以外の点で大きなデメリットが存在します。 組織分離によって発生するコストとSRE 組織分離によるデメリットは直接的なコストと間接的なコストに分けられます。 直接的なコスト 手作業の運用業務による人件費の増加 間接的なコスト 目標の違いが引き起こす対立構造による開発スピードと安定性の低下 開発組織の「新しい機能を早くリリースする」という目標 運用組織の「サービスに問題が起きないようにしたい」という目標 これらのコストに対するGoogleのとったアプローチが SRE になります。 コストに対するSREのプラクティス SRE本に記載のあるいくつかのプラクティスをコスト別に分類してみました。 直接的なコストに対するアプローチは主に、「 運用作業をどのように自動化するのか 」と「 自動化に使える時間をどうやって維持するか 」の大きく2種類に分かれます。 間接的なコストに対するアプローチは主に「 組織間の対立構造をどのように解消するか(発生させないか) 」に焦点を当てたものになり、SLOやエラーバジェットはこちらに分類されると考えています。 SRE NEXTでもどちらの内容をテーマにするのか発表ごとに分かれていて、どちらに課題感を大きく感じているのかを考察しつつ聞くのもまた面白かったです。delyの発表では間接的なコストの方に対するアプローチについてお話しました。 対立構造が引き起こすプロダクト開発速度の低下 SRE本の中で「間接的なコスト」として語られている内容を図に落とし込みました。開発と運用の目標の違いが最終的にプロダクト開発速度を低下させる構造になっています。 重要なポイントとしては、 開発/運用 の組織体制が 開発/SRE という組織体制になったからといって、ただそれだけでこの 対立構造が解消するわけではない という点です。 開発速度/安定性のバランスのコントロール プロダクト開発速度を最大化させるには、開発速度/安定性のバランスをコントロールする必要があります。 ある項目において開発速度と安定性どちらを取るのか決めなければいけないタイミングが多々あると思いますが、客観的なデータがない限り 交渉力のある人 であったり 立場のある人 の一声で最後は決まってしまうと思います。 その場合、 誰かの感覚 がバランスを取っているということになると思うのですが、その「誰か」はどこかの組織に属しているはずなので結局、対立構造は解消しないということになってしまいます。 エラーバジェット 「誰かの感覚」に終止符を打つSREのプラクティスが エラーバジェット です。 予算がなくなった時点でリリースできなくなる という超強力なポリシー・・・! 機能のリリース速度を最大化するためにエラーバジェットの導入をSREの目標としましょうという内容がSRE本には記載されています。 エラーバジェットによって目標を統一することで、開発速度と安定性をコントロールしプロダクト開発速度の最大化を図ることが可能になります。 SREの存在意義の再確認 エラーバジェットの仕組みからも読み取れる通り、SREはSLOやエラーバジェット等のプラクティスを駆使して組織の対立構造を解消し、 プロダクト開発の速度を安全に高める ために存在しているということが言えると思います。 エラーバジェットとは別のアプローチ とはいってもエラーバジェット導入の難易度って時間がかかりますよね。なので、SRE本に載ってる プロダクト開発の速度を安全に高めるための別のアプローチ も同時に紹介しました。 エラーバジェット導入の難易度 エラーバジェット導入の難易度はとても高いです。 「その期間のエラーバジェットを消費しきったら残りの期間リリースができなくなる」というルールは、運用しているサービスのビジネスモデルによってはかなり厳しいポリシーだと思います。 もちろん、SREの文化を浸透させ最終的にはエラーバジェットの導入を目指すべきだとは思いますが、導入しようと思って数日でできるような手軽なアプローチではないことは間違いないです。 SRE本においてはエラーバジェット以外にもプロダクト開発の速度を安全に高めるためにSREがすべきことについて触れており、発表ではその内容について紹介しました。 それは 想定外の複雑さの削減 と 単純さの追求 です。 想定外の複雑さとSRE SRE本においては開発速度と安定性がどちらも低下する要因として 想定外の複雑さ を挙げています。 具体例としては、ソフトウェアの領域においては密結合なソースコード、インフラの領域においては手作業で構築されたサーバなどが挙げられます。 想定外の複雑さはレイヤーに関係なく発生しますが、どこに存在していたとしても開発速度と安定性のどちらも低下させる要因になってしまいます。 SRE本には 想定外の複雑さ に対してSREがとるべき行動が記載されています。 受け持っているシステムに想定外の複雑さが生じていたら差し戻す。 関わっているシステムや、運用を受け持つことになるシステムから複雑さを取り除く努力を継続的に行う。 単純さの追求による開発速度と安定性の両方の向上 ソフトウェアやインフラなどの領域に関わらず、 想定外の複雑さを継続的に削減していくこと と、 想定外の複雑さを新たに作り込まないこと が、開発速度と安定性を両方高めていくためには必要です。 delyでの実践 後半はdelyにおける実践の話をしました。 正直なところ、他社の参考にできそうな奇抜な施策が出来ている状況ではないです。そもそも当たり前なことがまだちゃんと出来ていない状態なので、ひとつひとつの課題解消を愚直に進めつつ基盤を整えているフェーズです。 そういったフェーズの会社が他にも存在すると思ったので、やっていることは割と普通なのですが、dely自体のことも知ってもらえる良い機会だと思いいくつか紹介させて頂きました。 規模とフェーズ サービスや組織のフェーズをまず知ってほしいと思ったので、サービスの規模とメンバーの増加傾向がわかるようにグラフを見て頂きました。 おかげさまでサービスは年々成長を続けていて、開発のメンバー数も増加傾向にあるのですが、注目してほしいのは 2016年から2018年までの2年間、サーバサイドがほぼ1人での開発だった というところです。 この頃の状況は下記の2つの記事から読み取ることが可能です。 note.com www.fastgrow.jp 2019年前半までとにかく機能リリース優先でやってきたこともあり、最近になってサービスの成長に伴って品質やセキュリティなどの点でいろいろな問題が発生し始めました。 課題 クラシルは開発開始から4年ほどが経っています。当然のように様々な課題を抱えています。 現状の課題が多いからといって、今までの選択が決して悪かったというわけではないですが、フェーズが変われば課題も変わるということは事実なので、話し合うべきなのは 「 これからどうしていくか 」というところだと思っています。 具体的なアプローチ delyでは直近1年をかけて 想定外の複雑さ を減らし、今以上に増やさないための文化づくりをしてきました。 下記の3つの視点でアプローチを行いました。 何が複雑なのかを認識し共有することにより可視化する 複雑なものを減らす計画 複雑なものを新たにつくらない仕組みづくり 想定外の複雑さを減らすアプローチ 改善MTG 複雑さの箇所を可視化するために行っているのが改善MTGです。想定外の複雑さを一番把握しているのは現場のメンバーですが、改善MTGを実施することでその感覚を言語化し、メンバー間で共有することを目的にしています。 想定外の複雑さ も誰かの感覚の中にある限り、解消することは理論上不可能です。しかしMTGを行いメンバー間で議論を行うことで、感覚の先の根本的な課題が明確になっていき、どういった対応が考えられるのかや解消によって得られる効果などが言語化されます。PdM等のタスクの優先順位付けに決定権を持つ人が、機能開発タスクより改善タスクの優先度を上げるという意思決定をできるように可視化しておくことが 想定外の複雑さ を解消するためには必須であると考えています。 改善MTGの詳細については既に記事があるので興味があればこちらを読んでみてください。 tech.dely.jp 課題洗い出し会 改善MTGも運用開始直後においては課題の提案が特定のメンバーに偏るという課題がありました。その課題に対して行ったのがこの課題洗い出し会になります。 課題が潜んでいそうな技術分野をリストアップし、メンバー間で回答が見えないようにそれぞれの分野に対する課題感を回答してもらいました。その回答をマージして課題感がメンバー間でずれている箇所に対して議論をします。議論した内容をもとに課題を登録してもらうことで、今までなかった観点の課題が提案されるようになりました。 課題の提案もある程度慣れが必要な部分だとは思うので、不得意なメンバーでも提案しやすくなるような仕組みや、新しいメンバーが遠慮してしまわないような工夫が重要だと考えています。 リファクタリング計画 改善MTGはあくまで可視化までが目的になっているので、解消していく工程については別途、別の手段で進める必要があります。誰か1人が進めるのではなく分担して解消していく必要があるので、やみくもに進めるのではなく計画をたてるようにしています。 スプレッドシートに今抱えている課題を一覧化して、重要度と緊急度の観点から優先順位を議論して決めます。優先順位に伴って、誰がどの課題を解消するのか担当を埋めていきます。あとは担当者が愚直に解消していくという感じで進めています。 詳細については既に記事があるので興味があればこちらを読んでみてください。 tech.dely.jp 想定外の複雑さを増やさないアプローチ 設計レビュー 増やさないためのアプローチの一つとしては設計レビューを紹介しました。 想定外の複雑さを解消していっても、別のところで随時新しく生まれていたら削減している意味がないです。 サーバサイドエンジニアの数が増えてきたタイミングで、コードを書き始める前に設計完了時点でレビューをするように開発フローに組み込みましたが、SREもSLOの維持に責任を持っているので、アーキテクチャの観点など想定外の複雑さが発生していないかレビューできるようにSREも参加を必須にしています。 内容によってはかなり時間をかけて議論を行っていて、コードを書く前から全員で徹底的に議論を行うような文化が出来ています。 このあたりの内容はコアすぎてあまり書けないのですが、何かの振り返りのタイミングで記事を書いて共有できればと考えています。 意識 目的と手段の可視化 いろいろなアプローチを考えつつ複雑さを解消しているのですが、複数人で分担していくと、ついついリファクタリングすること自体に目がいってしまいがちで、 本来の目的を見失う ことが少なからず発生してしまいます。 リファクタリングすること自体が目的化してしまうと、つい必要のないところまで手を伸ばしてしまうと思うのですが、それをやってしまうと今やらなくてよいものまで実施してしまうことになるので、今までのアプローチの効果が薄れてしまいます。 なので目的と手段を可視化することで、 今やっているタスクが何の目的の手段にあたるのか ということを常に意識できるようにしています。 例えば、リファクタリングや開発ガイドラインの見直しなどはタスクに分解すると、不足しているテストの作成や開発フローの見直しなどになりますが、これらのタスク自体も手段であって目的のために行っているという状態になっているはずです。意識しなければいけないのは目的の方であり「開発速度と安定性の両方の向上」や更にその上の事業の成長になります。 さいごに 偉そうなことべらべら書いてる割にdelyのSREは実は全然始まってすらいません。少しづつ始めている内容は下記の記事にかいてあるので是非読んで頂きたいです。 tech.dely.jp もちろんSREを絶賛募集しています!クラシルの開発スピードを落とさず、信頼性も担保するという難易度の高い課題に挑戦したい方はぜひ声をかけてください! www.wantedly.com delyの開発チームについて詳しく知りたい方はこちらもあわせてどうぞ
アバター
はじめまして。 dely, Inc.でクラシルの開発に携わっている @sakura といいます。 本記事では、分析データを社内に情報共有するまで私がどんなことしたかを 赤裸々に紹介します。 この記事は データ活用 Advent Calendar 2019 の21日目の記事です。 はじめに 泥臭くやったこと スプレッドシートにしてハードルを下げる データを整理して見やすく、負荷を少なく 社内の複数人にメンションをつけて共有 オフラインで共有してアドバイスをもらう Qiitaにも残して検索しやすく bot作成してslackでかんたん確認 bot警察をしてデータを見る文化を作る まとめ はじめに 「こんなにも貴重なデータがあるのに全然活用できていない…てか誰も見ていない… 」 この状況を打破すべく一歩一歩やっていったことを共有します💪 泥臭くやったこと スプレッドシートにしてハードルを下げる まずは、データをスプレッドシートにまとめました。 なぜかというと、そのデータが redash でしか閲覧できなかったからです。 エンジニア以外にとってはredashは心理的負荷が高く、スプレッドシートのほうが普段から使い慣れているので扱いやすいです。 弊社ではG Suiteを使用しているので、 スプレッドシートにすることでデータを見るハードルがぐっと下がり ました。 データを整理して見やすく、負荷を少なく データの性質そのものではなくて、データを閲覧してほしい人を軸にして数千件のデータをカテゴリ分けしました。 上の画像は実際のスプレッドシートです。タブでカテゴリごとに分かれています。 何千件もあるデータを見るのは脳の負荷が高く疲れて しまいます。 従って、事前にカテゴリ分けをして この人はここだけ見ればOK というようにしました。 社内の複数人にメンションをつけて共有 スプレッドシートが完成したら、データを見てほしい人にメンションをつけて共有しました。 特定のチャンネルで特定の人に「こういうデータがあって、ここの部分を利用できると思うんですがどうでしょうか」という風に、 チャンネル / メンションをつける 人ごとにメッセージを変え 、そのデータの効果が最大化するように努めました。 やってることは パーソナライズに近い かもしれないです。 オフラインで共有してアドバイスをもらう オンラインで伝えることは効率がよいですが、一番物理的距離が近く、必ず伝わるのはオフラインだと思い 2つのMTGで「貴重で役に立つデータがここにあるのでぜひ見てください」と共有しました。 その場で見てくださる方もたくさんいて、アドバイスを頂くこともできました。(このデータ役に立ちそう、といわれた時はうれしかったです) Qiitaにも残して検索しやすく 全社員が見れる社内の共有ドキュメントツールのQiitaを使い、軽くドキュメントを残しておきました。 slack内に残すと「あれ、あの情報どこいったっけ」となりがちなので、Qiitaにもまとめました。 ドキュメントに残すことで 検索のヒットもしやすくなり、 後から見返す時も探しやすくなります 。 bot作成してslackでかんたん確認 定期的に確認したほうがいいデータだと思ったので、 botを作成 しました。 redashのデータを定期的にslackに通知することで、最新の情報をかんたんに確認することができます。 botの作成には色んなエンジニアさんが手伝ってくれました。ありがとうございます。 bot警察をしてデータを見る文化を作る 弊社のslackにはbotが何十個もおり、いくらかんたんに見れるといっても見過ごしてしまうことも多いです。 どうにかこのデータを見る文化を作ることはできないか、と考えました。 そこで、 botが通知してくるデータを私自らが解釈・要約して投稿 することもしていました。 botの内容に 徐々にスタンプがつき、返信がつき、最近では私以外のメンバーが能動的に反応してくれることも多いです。 (すごい!) まとめ 本記事では、分析データを社内に情報共有するまでにどんなことをしたかを赤裸々に紹介しました。 まとめとして、この経験から学んだ情報共有の五箇条を記しておきます。 「情報共有難しい…💭」 と悩んでいる方の参考に少しでもなれば幸いです。 最後に告知です。delyではエンジニアを絶賛募集中です。 ぜひお気軽にご連絡ください。 https://www.wantedly.com/projects/329047    
アバター
こんにちは、delyコマース事業部エンジニアの小川です。 先月11月に入社し、エキサイティングな毎日を過ごしています。 この記事は dely Advent Calendar 2019 - Qiita の24日目の記事です。 昨日はSREの松嶋さんが「AWS RunCommandを使ってEC2上に監視ダッシュボードをサクッと作る(Ansible+Terraform+Grafana編)」という記事を書いてくれましたので是非そちらも読んでみてください! tech.dely.jp コマース事業部では、現在「事業開発」と「ソフトウェア開発」がほぼ同時に進行しており、プロジェクトにおける確定要素と不確定要素が複雑に絡み合っています。 スピード重視でゴリゴリ実装していくのも興奮しますが、変化に耐えづらい実装をしてしまうと、その後の開発スピードに影響していまい、事業のスピードが落ちるなんて事にもなりかねません。 そこで、プロジェクトの保守性や拡張性をあげるには、どういった設計をしたら良いかを、OSSである GitLab を一例として見ていきたいと思います。 今回は、model view controller 以外のどんなディレクトリ(=クラス)を導入しているか、それがどんな役割を担っているかを見ていきましょう。 それではいってみましょう。 GitLabが導入しているディレクトリ finders 実装を見ていく メリットになりそうな事 presenters 実装をみていく メリットになりそうな事 最後に GitLabが導入しているディレクトリ GitLabはRailsで実装されています。app配下のディレクトリで、デフォルトで作成されないものを抽出すると以下のようになりました。 finders さまざまな条件に基づいたコレクションを取得するクラス graphql Graphqlに関するクラス policies 権限確認系に使われるクラス。独自のDSLで実装されている。 presenters viewに関わるロジックやデータを持つオブジェクトをviewに提供するクラス serializers フロントで使われるJSONを構築するためのビジネスルールをカプセル化しているクラス uploaders CarrierWaveに依存しているUploader services ビジネスロジックが取りまとめられているクラス validators Activerecordが提供しているカスタムバリデーター workers SidekiqのWorkerクラス 今回はこの中から、個人的にあまり導入したことがない、 finders 、 presenters あたりを見ていきます。 finders finderクラスは「さまざまな条件に基づいたコレクションを取得する」責務を持つクラスになっています。 例えば、プロジェクトモデルの中でこのようなイシューを取得するメソッドを実装するより、 class Project def issues_for_user_filtered_by (user, filter) # たくさんのロジック... end end issues = project.issues_for_user_filtered_by(user, params) 下記のように実装すると、よりモデルを薄く保つことができるよ!っていうイメージですね。 issues = IssuesFinder .new(project, user, filter).execute GitLabのFinderクラスは、基本的に#executeのみをパブリックメソッドとして持っているみたいです。 実装を見ていく では実際に ProjectsFinder を例に挙げて見ていきましょう。 まずはどこで#executeが実行されているか探してみます。 ありました、Admin::ProjectsControllerで以下のように実行されています。 ProjectsFinder .new( params : finder_params, current_user : current_user) .execute .includes( :route , :creator , :group , namespace : [ :route , :owner ]) .preload( :project_feature ) .page(finder_params[ :page ]) paramsにfinder_paramsを、current_userにはcurrent_userを指定しています。 find_paramsは、取得するプロジェクトの条件、つまりフィルタリングするパラメーターや、ソートの条件などのパラメーターを含めています。 では、ProjectsFinderの実装はどうなっているのでしょうか。 #initializeには、paramsとcurrent_userとproject_ids_relationをキーワード引数で渡します。 def initialize ( params : {}, current_user : nil , project_ids_relation : nil ) @params = params @current_user = current_user @project_ids_relation = project_ids_relation end #excuteの実装は以下のような形です。 def execute user = params.delete( :user ) collection = if user PersonalProjectsFinder .new(user, finder_params).execute(current_user) # rubocop: disable CodeReuse/Finder else init_collection end collection = filter_projects(collection) sort(collection) end 変数collectionに、フィルタリングのベースになる、プロジェクトのコレクションを代入しています。 その後、#filter_projectsでプロジェクトのフィルタリングをおこなった後、#sortにて結果のソートをおこなっていました。 フィルタリングとソートの条件は、#initializeの時に渡したparamsで指定しています。 メリットになりそうな事 パブリックメソッドである#executeが見通しがよく、コードリーディングしやすいと感じました。 デルメルの法則にも違反していなく、依存も少ない(浅い?)と言えそうです。 このコレクションの取得を、modelに#filterのようなメソッドで実装したらどうなるでしょうか? ProjectsFinderに実装されている、多くのprivateメソッドがmodelにも実装されることになります。しかもそのメソッド達は、#filterの結果を達成するために切り出されている(特に他のメソッドでは使われない)ロジックなので、そのメソッド達はfat modelになってしまう要因の一つだと思います。 取得のロジックが単純なうちは良いですが、上記まで複雑になってきたり、必要なパラメーターが増えてきたら、Finderクラスの実装を考えていいかもしれません。 ですが、プロジェクトの初期段階でも、取得系の実装が大きくなることが分かっている、かつ不確定要素がたくさんありそうならば、取得系のロジックをFinderに集約し、呼び出し側が変更に影響しないように実装するのもありだと思いました。 presenters presenterは、viewに関わるロジックやデータを持つオブジェクトをviewに提供するクラスとなっています。 viewに直接書いてあるロジックや、modelにviewに関連するロジックやデータのメソッドは、presenterに実装します。 実装をみていく 使われ方を見てみます。今回はProjectPresenterを見ていきます。 presenterはviewで下記のように呼び出されていました。 -# @projectはProjectPresenterのオブジェクト = render ' stat_anchor_list ' , anchors : @project .statistics_anchors( show_auto_devops_callout : show_auto_devops_callout) パーシャルのreder時に、引数として ProjectPresenter#statistics_anchors の返り値を渡しています。 renderされているパーシャルは、 - anchors = local_assigns.fetch( :anchors , []) - return unless anchors.any? % ul . nav - anchors.each do | anchor | % li . nav-item = link_to_if anchor.link, anchor.label, anchor.link, class : anchor.is_link ? ' nav-link stat-link d-flex align-items-center ' : " nav-link btn btn- #{ anchor.class_modifier || ' missing '} d-flex align-items-center " do . stat-text . d-flex . align-items-center = anchor.label のようになっています。 anchorsで渡されたデータを使って、リンクを生成していますね。 このパーシャルは、#link, #label, #is_link, #class_modifierのメソッドもつオブジェクトに依存しています。 では次に、ProjectPresenter#statistics_anchorsの実装見てみます。 下記はProjectPresenterの実装の一部です。 class ProjectPresenter < Gitlab :: View :: Presenter :: Delegated presents :project AnchorData = Struct .new( :is_link , :label , :link , :class_modifier , :icon ) def statistics_anchors ( show_auto_devops_callout :) [ commits_anchor_data, branches_anchor_data, tags_anchor_data, files_anchor_data ].compact.select(& :is_link ) end def commits_anchor_data AnchorData .new( true , statistic_icon( ' commit ' ) + n_( ' %{strong_start}%{commit_count}%{strong_end} Commit ' , ' %{strong_start}%{commit_count}%{strong_end} Commits ' , statistics.commit_count).html_safe % { commit_count : number_with_delimiter(statistics.commit_count), strong_start : ' <strong class="project-stat-value"> ' .html_safe, strong_end : ' </strong> ' .html_safe }, empty_repo? ? nil : project_commits_path(project, repository.root_ref)) end end ProjectPresenter#statistics_anchorsは#commits_anchor_dataや、#branches_anchor_dataを集めて返しています。 #commits_anchor_dataは、構造体であるAnchorDataを生成・返却しています。ProjectPresenterは、 Gitlab::View::Presenter::Delegated (Rubyの標準のSimpleDelegatorを使用)を継承しているので、Projectのインスタンスメソッドである、#statisticsにもアクセスできます。 ちなみに、#branches_anchor_dataなど、#*_anchor_dataという命名のメソッドは全てAnchorDataを返却していました。 他にもオブジェクトの生成の仕方や、URL生成のヘルパーなど、しっかり作り込まれているのですが、長くなりそうなので割愛します。 オブジェクト生成の部分を端的に紹介すると、直接ProjectPresenter.newすることを禁止しており、下記のようなパターンで生成するようにしています。 # presentメソッドで生成するパターン @project .present # Factoryクラスを使って生成するパターン Gitlab :: View :: Presenter :: Factory .new( @project ).fabricate! メリットになりそうな事 viewの表示のルールがpresenterに集約されていて読みやすいと思いました。 表示してはいけないものを表示してしまったなどの事故が起きにくくなりそうです。viewがコンフリクトした時の恐怖からバイバイできるのも個人的に好きです。 また、viewに比べかなりテストしやすくなっています。 導入するときには、presenterがどういった粒度で実装されるかをチームで決めておいた方が良いかもしれません。 GitLabはmodelに対してpresenterが実装されている風に見受けられました。(models/project.rbに対して、presenters/project_presenter.rbのイメージ) プロジェクトによって、違う粒度での実装もありえると思います。例えば、viewで関連データが複雑かつ、多くのところで使われているパーシャルがあったら、それと1対1で対応させるようにすれば幸せになれそうですね。コントローラーにも同じロジックを書かなくて済むし、変更に対する影響範囲も明確です。もしコマース事業部で導入するとしたら、draperを使ったDecoratorとどんな住み分けするか(そもそも導入しない)とか、チーム内で議論してみたいです。 最後に 今回はこんな風にGitLabのクラス設計を見ていきました。 プロダクトの特性によって設計の仕方も変わってくると思うので、違うOSSも読んでみるのも楽しいと思います。 「保守性がモリモリ上がるクラス設計」と題しましたが、クラス設計に銀の弾丸はない気がするので、プロダクトの変化しやすい部分はどこか、設計することでどんな問題を解決したいかを、チーム内で議論を重ねて実装・検証することが良さそうです。 コマース事業部では事業も開発も挑戦することが多く、エンジニア・デザイナーを 強く 募集しています。 もし興味がある方がいましたら、お気軽にご連絡ください! www.wantedly.com www.wantedly.com
アバター
こちらは、dely advent calender 2019の23日目の記事です。 qiita.com adventar.org 昨日は、サーバーサイドエンジニアのyamanoiさんが「画像管理をActiveStorageからCarrierWaveへ乗り換えた話」という記事を書きました。興味を持った方は、是非読んでみてください! tech.dely.jp こんにちは! 今年11月からdelyに入社しました開発部SREの松嶋です。 本記事では、Systems ManagerのRunCommand (Ansible-playbook)を使うことでより簡単に監視ダッシュボードを作ることができたので、その手順について紹介したいと思います。 目次 目次 はじめに Grafanaプロビジョニング用設定ファイルの用意 Ansible-playbookの作成 terraformで環境構築 Ansible-playbookの実行 Grafana確認 最後に はじめに 今年9月にGitHubまたはS3に保存しているAnsible-playbookを直接実行する機能がSystems Managerに導入されたのを覚えていますか。 この新機能によって、Ansibleを使うためにEC2にssh接続用の公開鍵の作成や管理をしなくてもplaybookを実行可能になりました。Ansibleのplaybookを作成するだけでサーバー設定ができるのは、設定や管理面で楽になりますよね。 ちょうど弊社でも運用やセキュリティの観点からSystems ManagerのRunCommandやAutomationを使用することで、sshしなくてもデプロイできる手順に順次置き換えている最中です。 aws.amazon.com また今年の11月には、Grafana6.5がリリースされました。このアップデートによって、AWS Cloudwatchのメトリクスをより効率的に監視できる機能が新しく追加されました。例えば、ワイルドカードを使って動的なクエリを書くことが可能になったり、事前構築されたダッシュボードが用意されるようになったため素早くモニタリング開始することができるようになっています。 aws.amazon.com そこで、今回はこれらの新機能を試すために、Systems Managerの公式ドキュメント「AWS-ApplyAnsiblePlaybooks」を使ってRunCommandでGrafanaの監視ダッシュボードを構築してみたいと思います。 今回使ったものは、以下の通りです。 Grafana 6.5.2 Nginx 1.16.1 Terraform 0.12.12 AWS-ApplyAnsiblePlaybooks (Systems Managerドキュメント) Grafanaプロビジョニング用設定ファイルの用意 現在のGrafanaでは、データソースやダッシュボードをファイル管理することが可能となっています。そのため、事前に設定ファイルを用意しておけば、Grafanaの起動と同時にモニタリングを開始することができます。 まずは、データソース設定用ymlファイルを用意します。ここでは、データソースのタイプと認証方法の設定を記載します。セキュアな情報が必要となるのでAWSのパラメータストア等を使用して安全に管理してください。 cloudwatch-datasource.yml apiVersion: 1 datasources: - name: cloudwatch type: cloudwatch jsonData: authType: keys defaultRegion: ap-northeast-1 secureJsonData: accessKey: $AWS_ACCESSKEY secretKey: $AWS_SECRETKEY 続いて、ダッシュボード設定用ymlファイルを用意します。optionsのpathはダッシュボードのjsonファイル置き場を指定しています。 cloudwatch-dashboard.yml # # config file version apiVersion: 1 providers: - name: 'cloudwatch' orgId: 1 folder: '' folderUid: '' type: file options: path: /var/lib/grafana/dashboards #dashboard jsonファイル置き場 監視ダッシュボード用jsonファイルは、公式サイトからダウンロードしました。今回は、以下3つのダッシュボードjsonファイル(EC2,EBS,Billding)を用意しました。 amazon-ebs_rev1.json amazon-ec2_rev1.json aws-billing_rev13.json 公式のダッシュボードは、他にもLambdaやCloudwatchLogs、RDSがあります。ここはお好みのものをどうぞ。 grafana.com Ansible-playbookの作成 RunCommandで使用するplaybookは以下のような構成にしました。playbookの中身は、grafana及びnginxのインストール、設定をするタスクを記載しています。これらをzipファイルにまとめて後ほどs3にアップロードします。 . ├── README.md ├── main-ansible.yml └── roles ├── grafana │   ├── files │   │   ├── amazon-ebs_rev1.json │   │   ├── amazon-ec2_rev1.json │   │   ├── aws-billing_rev13.json │   │   ├── cloudwatch-dashboard.yml │   │   └── cloudwatch-datasource.yml │   ├── handlers │   │   └── main.yml │   ├── tasks │   │   └── main.yml │   └── templates │   └── grafana.ini.j2 └── nginx ├── files │   └── grafana.conf ├── handlers │   └── main.yml └── tasks └── main.yml 10 directories, 13 files 使用したplaybookのサンプルをgithubにあげていますので、参考までに。 github.com terraformで環境構築 ここまで準備ができたら、terraformで必要なものを構築していきます。 今回Grafanaサーバー用に構築したのは、以下の通りです。VPCやサブネットは既存のものを使用しました。 EC2インスタンス セキュリティグループ IAMロール IAMポリシー S3バケット S3オブジェクトのアップロード Route53のAレコード EC2インスタンスに付与するIAMロールに関しては、RunCommandの実行やGrafanaがCloudwatchのメトリクスを取得できるように以下のIAMポリシーをアタッチしておく必要があります。 Cloudwatchのメトリクス取得 作成したS3バケットのアクセス権限 AmazonEC2RoleforSSM (Amazon管理ポリシー) IAMロールとアタッチしたポリシーの例は、github上に置いているので参考にしてみてください。 github.com Terraformの実行は、GithubActionを使うとGithub上でterrraform initからvalidateやshow, applyまで完結するのでおすすめです。GithubActionで実施したplanやapply履歴もgithubで確認することができます。 github.com Ansible-playbookの実行 Systems Manager > Run CommandからAWS公式コマンドドキュメントの「AWS-ApplyAnsiblePlaybook」を選択します。 コマンドのパラメータは、以下のように設定しました。Source InfoはS3バケットに保管しているオブジェクトURLを記載してください。 Source Type: S3 Source Info: {"path":"object-url"} Install Dependencies: True Playbook File: grafana-ansible/main-ansible.yml Extra Variables: SSM=True Check: False Verbose: -v 後はRunCommand先のインスタンスを選択し、ログが必要な場合はCloudwatchLogsまたはS3に出力するように設定して実行すればOKです。 成功すれば以下のように表示されます。 RunCommand実行結果 S3にアップロードしておくだけで、ssh経由せずにAnsible-playbookを実行することができるのはかなり便利だと思いました。ただ、デバックオプションを使ってログ出力していてもplaybookのどのタスクで失敗したのか表示されずデバッグしづらかったので、事前にplyabookが想定通りに動作することを確認の上で使う必要があると思います。この点は、RunCommandの実行結果で見れるようになると良いですね。 Grafana確認 Ansible-playbookの実行が成功したら、Grafanaにログインしダッシュボードを見てみましょう。ダッシュボード一覧にプロビジョニングしたダッシュボードが表示されていることが確認できると思います。 Grafana6.5では、ワイルドカードを使えるようになったため動的なクエリに対応できるようになり、AutoScalingでEC2インスタンスが増減しても自動でダッシュボードに反映されるようになっています。 EC2インスタンスのダッシュボード また、グラフをクリックすると「View in Cloudwatch console」というCloudwatchコンソール画面に遷移するためのディープリンクがコンテキストメニューに追加されていることが確認できます。このリンクをクリックすれば、Cloudwatchコンソール画面に飛び、対象メトリクスを表示させることも可能となっています。 ディープリンクが表示される Cloudwatchコンソール画面に遷移できる まだ、発展途上な感じではありますが今後よりAWS Cloudwatchと親和性が高くなる予感がするので、さらに使いやすくなるのではと思います。 最後に delyではSREを大募集しています!興味ある人は気軽にまずはオフィスに遊びにきてください! www.wantedly.com delyの開発部について知りたい方はこちらをご覧ください! speakerdeck.com
アバター
本記事は dely Advent Calendar 2019 22日目の記事です。 qiita.com adventar.org 昨日はiOSエンジニアのknchstが「“ダーク“な2019年」という記事を書きました。 tech.dely.jp こんにちは、delyでサーバーサイドエンジニアをやっているyamanoiです。 弊社のとあるプロダクトにて画像アップロード処理周りに、ActiveStorageを使用していたのですが、使いづらい点がいくつかあったため、採用実績があったCarrierWaveへ乗り換えました。 この記事では なぜ乗り換えたのか と、 乗り換える手順 を書いていきたいと思います。 なぜActiveStorageから乗り換えたのか 1. CDNとの相性が悪い 2. 画像のリクエストがRailsに向いてしまう 3. DBへのリクエストが頻繁に走る CarrierWaveへ乗り換える手順 1. 設定ファイルを消す 2.CarrierWave gemの追加と書き換え 3. 移行スクリプト作る 4. テーブルの削除 まとめ 最後に なぜActiveStorageから乗り換えたのか 1. CDNとの相性が悪い ActiveStorageはアタッチされたモデルのurlメソッドを使用するとActiveStorageが定義した /rails/active_storage/blobs/* へのパスを生成します。 このパスへアクセスすると、各クラウドストレージ上のオブジェクトに対する一時的な認証コード付きURLへリダイレクトし、画像を取得することができます。 このURLをキャッシュしてしまうと、URLが期限切れになってしまうと画像が表示されなくなってしまいます。 ActiveStorageとCDNを併用するにはActiveStorageの機能を独自に拡張することで利用はできるのですが、拡張を行うことによってシステムが複雑になってしまい、アップデートの障壁になったりすることが容易に想像できるため今回は避けました。 2. 画像のリクエストがRailsに向いてしまう ActiveStorageを有効にするとActiveStorage用のroutingが新しく追加されます。 ActiveStorageを用いて画像を取得する際はすべてこのルーティングを経由する必要があります。 静的ファイルはパフォーマンスの観点からアプリケーションサーバーを通さずnginxやs3等のバケットから直接配信したいですよね。 また追加されるルーティングは自分で定義している config/routes.rb の後にロードされるため、以下の様なルーティングを定義しているとActiveStorage側のルーティングにマッチする前にルーティングが解決されてしまい、画像が正しく表示されない問題に直面しました。 get " *path " , controller : ' front ' , action : ' spa ' , via : :all 3. DBへのリクエストが頻繁に走る ActiveStorageは active_storage_attachments と active_storage_blobs の2つのテーブルを作成し、そこに画像のメタ情報やモデルとの関連を保持します。 ActiveStorageを使う場合は少なくともアタッチするモデルと1対1の関連が発生します。 何も考えず使用すると容易にN+1を誘発します。そのためActiveStorageではN+1を回避するためのメソッドが用意されています。 また上記の2つのテーブルですべてのモデルに対しての画像を扱うため、レコード数の多いテーブルが複数存在するとレコード数が増加し、パフォーマンスに影響が出てしまう可能性もありそうです。 CarrierWaveへ乗り換える手順 1. 設定ファイルを消す config/application.rb でrails/allしている場合は不要なファイルもロードしてしまうので必要なもののみをロードするように変更します デフォルトだとconfig/application.rbに以下の様な記述があると思いますが、これだとActiveStorageもロード対象になってしまうので使うものだけをロードするように変更します。 Before require ' rails/all ' ↓ After require " rails " require " active_model/railtie " require " active_job/railtie " require " active_record/railtie " require " action_controller/railtie " require " action_mailer/railtie " require " action_view/railtie " require " sprockets/railtie " require " rails/test_unit/railtie " 2.CarrierWave gemの追加と書き換え CarrierWave gemを入れて実際にコードを置き換えていきます。 置き換える点で厄介になりそうなところはActiveStorageのvariantを使用している場合です。 ActiveStorageではアタッチされている画像に対してvariantメソッドを使うことでリサイズ処理を手軽に実現することができます。 <%= image_tag @post.thumbnail.variant(resize:'50x50').processed %> 様々なサイズを気軽に生成することができるので便利な機能なのですが、CarrierWaveではUploaderクラスに予めversionとして画像のパターンを定義しておく必要があります。 今回のプロダクトではこのvariantの機能をフル活用している場所はあまり無かったのでそこまで問題にはなりませんでした。 3. 移行スクリプト作る 弊社の場合、ActiveStorageで保存された画像はs3に置いてあり、移行するにあたってファイルの保存場所を変更する必要があったため移行するスクリプトを作りました。 ActiveStorageは1つのバケットにフラットに画像を保存するため、パスを特定し、置き換えていきます ↓サンプルコード class FileDataStringIo < StringIO attr_accessor :original_filename , :cnotent_type def initialize (*args) super (*args[ 2 .. -1 ]) @original_filename = args[ 0 ] @content_type = args[ 1 ] end end class ActiveStorageBlob < ActiveRecord :: Base ; end class ActiveStorageAttachment < ActiveRecord :: Base belongs_to :blob , class_name : ' ActiveStorageBlob ' belongs_to :record , polymorphic : true end ActiveStorageAttachment .all.each do | attachment | blob = attachment.blob key = blob.key filename = blob.filename record = attachment.record name = attachment.name content_type = blob.content_type s3 = Aws :: S3 :: Resource .new( region : ' ap-northeast-1 ' ) obj = s3.bucket( ENV [ " ACTIVE_STORAGE_S3_BUCKET " ]).object(key).get data = obj.body.read io = FileDataStringIo .new(filename, content_type, data) record.send( "#{ name } = " .to_sym, io) record.save! end 4. テーブルの削除 最後にActiveStorageの有効時に生成されたテーブルを削除します。 class DestroyActiveStorageTables < ActiveRecord :: Migration drop_table :active_storage_blobs drop_table :active_storage_attachments end まとめ Rails備え付けの機能だからと言ってすぐ取っつかず、開発しているプロダクトの要件をしっかり満たせるかを検討しながら、Gemの選定を行いましょう 最後に delyではサーバーサイドエンジニアを募集中です。ご興味ありましたらぜひこちらから! note.com www.wantedly.com delyの開発について知りたい方はこちらもあわせてご覧ください! speakerdeck.com
アバター
こんにちは!クラシルiOSアプリを開発しているknchstです。 この記事は「dely Advent Calendar 2019」の 21 日目の記事になります。 qiita.com adventar.org 昨日は Android チームの tummy による「何もわからない状態からいち早く脱するためのコードリーディング法(Android 版)」という記事でした。 tech.dely.jp ダークモードな2019年 WWDC2018で発表された macOS Mojave にて実装されたダークモードを皮切りに世界的にダークモードへの対応が進んできました。しかしダークモードについては様々な意見があり、その評価や必要性についても賛否両論です。 iOSやAndroidのような主要なモバイルプラットフォームがOSレベルでダークモードをサポートしたことにより、Twitter, Instagram, Slackなどメジャーなデベロッパーたちは早々にダークモードに対応しました。 Dark Mode Listというサイトではダークモードに対応しているアプリの一部が確認できます。 darkmodelist.com ダークモードがもたらした功罪 ダークモードに対する一般的な評判はとてもいいように感じます。iOSやAndroidがダークモードに対応する前から LINEが黒のテーマをリリース した時や、SpotifyのアプリのUIがかっこいいという声はよく聞きました。 Every app should have a dark mode, and it’s good for your eyes, your brain and your battery https://t.co/Ir50mjHfWl — The Wall Street Journal (@WSJ) 2019年12月1日 さらにウォールストリートジャーナルは、ダークモードを強く推奨している記事を書いていました。そしてその記事を今月のはじめに再びツイートしていました。しかしながら面白いことに当のWSJのアプリはこの時点ではダークモードに対応していなかったのです。ダークモードが追加されたのは最近の話です。 この動きはメディアに限らず個人レベルでも起きつつあります。そう、ダークモードハラスメントが起こっているのです。以下は著名なeスポーツプレイヤーのツイートです。 Everyone gives me shit for using light mode on my phone? Sorry I get outside and experience Vitamin D, as a gamer most of you are in the dark anyways, maybe by letting some light in even on your phone you’ll be happier and not giving me shit for dwelling in the dark. Losers — Gabriel (@Laxing) 2019年12月3日 なぜダークモードはここまで人気なのでしょうか。 いち早く一般ユーザー向けのサービスでダークな外観を提供していたSpotifyのディレクターだったMichelle Kadir氏はインタビューで以下のように述べています。 www.fastcompany.com We believe that when you have music or art that’s very colorful and very artistic, and you have beautiful cover art for music, that it really shows more clearly visible in a product like this, when it’s about entertainment. Everything else settles in and isn’t as much in the way when you have a white background Spotifyがダークな外観をアプリに採用したのは多くの背景パターンをユーザーに対してテストした結果ダークな外観がユーザーに好まれることが分かったからです。 さらにダークモードの人気は近年の人々の日常のニーズを反映していると言えます。 おそらく誰もが照明が暗い空間でスマホをいじる人にイラついたことがあると思います。ダークモードがあるおかげで他人の邪魔をせずに暗闇でスマートフォンを使用することができます。 例えば映画館・美術館などの照明が灯っていないもしくは非常に暗い状況下での使用です。あるいは、パートナーが近くで寝ている間にスマートフォンを操作したい場合などもです。 さらにダークモードにすることにより憎っくきブルーライトを軽減し、安眠に貢献してくれることでしょう。 また、スマートフォンのディズプレイへの様々な工学的理由によって採用されている有機EL(OLED)との相性の良さもダークモードの普及をさらに促すことになると思います。今年のGoogle I / Oでのセッションによると、Android Qのダークモードは、「 有機EL(OLED)下での利用でバッテリー消費を最大60%削減できる 」と述べていました。 しかしながらダークモードが全てのユーザーに適しているかは疑問があり現在でも議論がつづけられています。以下のQuoraの質問ではメガネをかけている人や乱視など状態にある人にとってはダークモードが眼精疲労を助長する恐れがあると主張しています。 www.quora.com 以下の2008年のブログで引用された論文では、黒地に白のテキストが白地に黒よりも26%読みやすいと主張しています。ただ、その論文は1980年代のスクリーンを使用しているため、今の私たちが利用しているディズプレイなどで再度研究してみる必要があるかもしれません。 tatham.blog ダークモードとレガシー ダークモードに対応するということは、既存のサービスのUIと闘う必要があります。ただ単に背景を黒に変更すればいい、という訳ではありません。 以下記事に書かれているAlibabaアプリのダークモード対応では、色の最適化の重要性について主張しています。 www.uisdc.com Alibabaのブランドカラーである明るいオレンジを使用する時、暗い背景に適しているか考慮する必要があります。あるいはブランドカラーを拡張させた補助色を利用することも検討できます。これにより、コントラストを維持し、より疲れにくく美しさを保ったUIを提供することができます。 また別の例としてロシアの検索エンジンを提供しているYandexが開発しているメールクライアントではダークモードに対応する際に大きな課題にぶつかりました。 habr.com Eメールは古くからあるコンテンツ形式であるため、ほとんどのメールクライアントは背景色が白であるという前提で設計されているため、メール本文ないに利用される画像は背景が白で作られていることが多くあります。また、一部ではメールテンプレートの背景色を調整している可能性もあります。 このためメールクライアントがダークモードに対応すると以下のような問題が発生します。Amazonのロゴ部分は背景が白い画像であるためダークモードにしたときに不自然に見えます。 おわりに ダークモードについて話してきましたが、サービスに本当にダークモードが必要かも検討する必要があるかもしれません。Spotifyの事例ではそれがユーザーに最適であるということがテストによってわかったのに加え、サービスのコンテンツがダークモードとの相性が良かったので採用されました。自分はなんでも間でもダークにすればいいとは思いませんし、ダークモードもあまり使っていません。 しかしながら、Appleにプラットフォームの開発者である以上 Appleの決定 には従わざるおえないのも事実です。来年もいろんな意味でダークな一年になりそうですね。。 delyでは一緒に食の課題を解決してくれるエンジニアを募集しています!興味ある人は気軽にまずはオフィスに遊びにきてください! www.wantedly.com speakerdeck.com
アバター
こんにちは。 dely の開発部でクラシルの Android を担当している tummy です。 2019 年 12 月から dely に入社して、たくさんキャッチアップしながら初めての施策を実装しています。ついていくので精一杯です。。笑 この記事は「dely Advent Calendar 2019」の 20 日目の記事になります。 qiita.com adventar.org 昨日は同じ Android チームの kenzo による「エンジニアは体が資本でしょ。と思って始めた習慣とその続け方」という記事でした。 自分は三日坊主になることが多いので、次になにか継続しようと思ったときはアドバイスをもらおうと思います。 tech.dely.jp 今回は、新しいプロジェクトに入った際のキャッチアップ時に行っている、担当する Android アプリの中身を「なるべく早く」「ざっくりと」把握するためのコードリーディング方法を紹介できればと思います :) 目的 まずやること コードを読む方法 PR を見に行く Android Studio の機能フル活用 Breakpoint にひっかけて Frames を見る Layout Inspector Hyperion-android の活用 どのクラスにいるかを Toast で出す まとめ さいごに 目的 何がどこにあってどう使われているか、を粗方把握している 状態を目指します。この状態になっていれば、とりあえず該当箇所を見に行けるため一次調査が自分で行えると考えているからです。 まずやること build.gradle で何が入っているのか見る デバッグ周りを調べる package 構成を見る 以上の 3 つです。 まず、build.gradle を見ます。ライブラリの一覧が記述されているので通信ライブラリや Android SDK まわりで使っているものを確認できます。また、flavor や Lint の設定なども記述されているため、手元で開発するときの環境が把握できます。 その後、デバッグまわりを調べてデバッグメニューが実装されているかどうか、Stetho やこのあとも紹介する Hyperion などのデバッグツールが入っていないか見ます。 そして、package 構成を見に行きます。 ここまでやったあとにコードを読みに行きます。 コードを読む方法 PR を見に行く コードを読めばなんとなくやっていることはわかりますが、「なぜこういう実装になっているのか?」という箇所を発見した場合、プルリクに書いてあればそれを元に理解することができる可能性があります。Find Pull Request という Intellij プラグインがあるのでそれを活用します。 plugins.jetbrains.com List Pull Requests にチェックを入れて Annotate すると Pull Request の番号が表示される GitHub 等を使ってプルリクベースで開発していることが前提になりますが、こういった情報も概要を掴む上で助けになってくれるはずです。 Android Studio の機能フル活用 ショートカットをたくさん使うのはもちろんですが、他に特に便利なものを紹介します。 Breakpoint にひっかけて Frames を見る ブレークポイントを任意の場所(例えばどこから遷移してくるのか知りたい Activity の onCreate)に置き、Debugger を起動してブレークポイントの場所を通ると Debug のビューが開くと思います。 そこに Frames という欄があり、アプリ起動からそのブレークポイントに至るまでが表示されています。 上記の場合、「MainActivity の initListener メソッドで onClick が呼ばれ、SecondActivity の Companion Object 内にある createIntent が呼ばれた」と読むことができます。 このことから、MainActivity から SecondActivity に飛ぶ際の Intent が SecondActivity 内のメソッドで作られている事がわかります。 Layout Inspector 実行中のアプリのレイアウトがどういった構造になっているか確認することができます。 Tools > Layout Inspector から起動できます。 Layout Inspector を起動したところ 2 つ以上 Activity を開いていれば、起動時にダイアログが表示されて Activity のクラス名が判明します。実機で触ってみるフェーズにおいて、こちらを併用しながらコードを読んでいくと捗るかと思います。 スタックに溜まっている Activity のリストが表示される ※ Android Studio 4.0 から使える Live Layout Inspector もあるとより便利ですね! Hyperion-android の活用 hyperion-android というデバッグライブラリがあり、Timber の中身やクラッシュログを確認することができます。またこのライブラリのサードパーティで、任意のアイテムを追加できるものがあり、今回はそちらを活用します。 github.com どのクラスにいるかを Toast で出す Application クラス内で以下のように実装します。 override fun onCreate() { super .onCreate() val listener = object : ActivityLifecycleCallbacks { private var activityName: String? = null override fun onActivityPaused(activity: Activity) { } override fun onActivityStarted(activity: Activity) { } override fun onActivityDestroyed(activity: Activity) { } override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { } override fun onActivityStopped(activity: Activity) { } override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { } override fun onActivityResumed(activity: Activity) { activityName = activity.javaClass.simpleName } fun getCurrentActivityName() = activityName } registerActivityLifecycleCallbacks(listener) val item = SimpleItem.Builder( "debug" ) .image(R.drawable.ic_launcher_foreground) .text( "Show class name" ) .clickListener { Toast.makeText( this , listener.getCurrentActivityName(), Toast.LENGTH_LONG).show() } .build() SimpleItemHyperionPlugin.addItem(item) } 実際にうごかすとこのようになります FragmentLifecycleCallbacks を使えば Fragment でも同様に取得できると思います。 まとめ 最初からすべてを把握するのは大変難しく、時間を要します。そのため、基本的な設計とメインループだけわかっていれば最初の入りとしては上出来で、他の画面等については施策やレビュー等でいじった際に理解していけば良いと考えています。 自分は Android Studio やちょっとした実装をすることでカバーしていましたが、他にこういうツールも便利ですよっていうのあったらぜひ教えて下さい! 明日は iOS エンジニアのふくさんが、「“ダーク“な2019年」というタイトルでお届けします。 さいごに dely では Android エンジニアを絶賛募集中です、ご興味あればこちらのリンクからお気軽にエントリーください! www.wantedly.com 開発チームについて詳しく知りたい方はこちらから 🙆 wevox.io note.com
アバター
www.youtube.com はじめまして。 dely, Inc. の @sakura です。 この記事は Google Products Advent Calendar 2019 - Adventar の20日目の記事です。 昨日は新坂さんのGoogle Homeの記事でした。Google Homeに関する様々な実体験が書かれており、とてもほっこりするエントリでした。 polasleep.hateblo.jp 本記事では、YouTubeのレコメンドの仕組みについて書かれている論文『 Deep Neural Networks for YouTube Recommendations 』を紹介します。 なぜ読んだのか 本論文について イントロダクション 1.スケールの問題 2.新鮮さの問題 3.ノイズの問題 わかったこと レコメンドシステムの2層のニューラルネットワーク 暗黙的フィードバック アルゴリズムに使われている情報 CTRより視聴時間 レコメンド群の候補たちを生成するモデル まとめ さいごに なぜ読んだのか YouTubeは普段から利用するサイトの一つであり、興味があったからです。 また、社内では「サーベイチャレンジ」という試みがありせっかくの機会なので読んでみました。 tech.dely.jp 本論文について 本論文の1ページ目 論文へのリンク https://static.googleusercontent.com/media/research.google.com/ja//pubs/archive/45530.pdf タイトル: Deep Neural Networks for YouTube Recommendations 著者:Covington, Jay Adams, Emre Sargin これは2016年に発表された論文で、YouTubeのレコメンドに関する技術について書かれています。 イントロダクション さっそく本論文の内容に入っていきます。 ※ここから先は書き言葉になります。そうしないと〇〇らしいです。〇〇そうです。の文章のオンパレードになってしまい読みくいため イントロダクションには、 YouTubeのレコメンドは3つの大きな挑戦がある と書いている。 1.スケールの問題 既存のレコメンドアルゴリズムの多くは小さな問題ではうまく機能することが証明されているが、 YouTubeの規模では機能しない 。 YouTubeの規模になるとユーザーデータも莫大にあるからそこをどう対処していくか。 2.新鮮さの問題 毎秒何時間もの大きな動画がアップロードされるのでレコメンドシステムはこれに耐えうる性能でなければならない。 また、 ユーザーは新しいコンテンツを好むことはわかっているがそれと引き換えに適合度(関連性)や既存のコンテンツとのバランス は慎重に考慮する必要がある。 3.ノイズの問題 YouTubeでの過去のユーザー行動は、 スパース性 (まばらという意味)と色んな観測不能な外部要因があるから予測がかなり困難。 加えて、各ビデオのメタデータの構造が不十分である。 従って、アルゴリズムを作るにあたってこのような特性を十分に加味しておかなければならない。 わかったこと 論文を読んでわかった部分のいくつかを紹介します。 全てについては解説していないのでご了承ください。 レコメンドシステムの2層のニューラルネットワーク Figure 2: Recommendation system architecture demonstrating the “funnel” where candidate videos are retrieved and ranked before presenting only a few to the user. レコメンドシステムには2層ニューラルネットワーク を使用している。 1つはレコメンド群の候補たちを生成するもの、もう1つはそれらをランキングのように並び替えるものである 。 前者には特徴量行列としてユーザーのYouTubeアクティビティ履歴を使用しているとのこと。 また、前者には協調フィルタリングも使用していてパーソナライズを実現している。協調フィルタリングにはアイテムの類似度のみならず、ユーザー同士の関係性も必要になってくるがそこはどんなビデオを見ているか、どんな検索クエリを入力しているか、あとは一般的な人口統計などの情報を使っているらしい。 暗黙的フィードバック YouTubeのGood/Badボタン YouTubeにはGood/Badボタンなど様々な明示的フィードバックがあるが、モデルをトレーニングする時は暗黙的なフィードバックを使う。 暗黙的なフィードバックとはなにかの説明については以下のブログがわかりやすかったです。 この記事では,ユーザの行動履歴をフィードバックと呼びます.一言にユーザからのフィードバックと言っても様々な種類があります.例えば,ユーザがアイテムに対して点数(rating)をつけたものや,like・dislikeのようにそのアイテムを好きか嫌いかを表すものがあります.このタイプは,ユーザがそのアイテムを好きかどうかがはっきりとわかるので,明示的フィードバック(explicit feedback)と呼ばれます.一方で,単なる閲覧・購入履歴などのデータも存在します.このデータからは,閲覧したけど好きだったかどうかはわかりませんし,購入したけど満足したかどうかはわかりません.したがって,このタイプは暗黙的フィードバック(implicit feedback)と呼ばれます. 「推薦」の定式化から推薦システムを理解する | カメリオ開発者ブログ アルゴリズムに使われている情報 論文中に記載されていただけでもこれだけの情報がレコメンドアルゴリズムに使用されている可能性があることがわかった。 3.3 Heterogeneous Signalsなどに記載されている。 ・ユーザーのYouTubeアクティビティ履歴 ・ユーザー間の類似度 ・ビデオ間の類似度 ・ユーザーの視聴履歴 ・ユーザーの地域 ・ユーザーの性別 ・ユーザーのログイン情報 ・ユーザーの年齢 ・ユーザーはいくつこのチャンネルの動画を見ているか ・ユーザーがこのトピックに関するビデオを最後に見たときはいつか (・もしレコメンドした動画をユーザーがみなかったらその動画は降格させる など CTRより視聴時間 Ranking by click-through rate often promotes deceptive videos that the user does not complete (“clickbait”) whereas watch time better captures engagement [13, 25]. [13] E. Meyerson. Youtube now: Why we focus on watch time. http://youtubecreator.blogspot.com/2012/08/ youtube-now-why-we-focus-on-watch-time.html. Accessed: 2016-04-20. [25] X. Yi, L. Hong, E. Zhong, N. N. Liu, and S. Rajan. Beyond clicks: Dwell time for personalization. In Proceedings of the 8th ACM Conference on Recommender Systems, RecSys ’14, pages 113–120, New York, NY, USA, 2014. ACM. 「CTRで測定するランキングはしばしばdeceptive(こすい、詐欺的)なビデオを促進するものだ。それよりも視聴時間を見るほうがユーザーのエンゲージメントをよく捉えているといえるだろう。[13,25]」 と引用して記載されている部分がある。 レコメンド群の候補たちを生成するモデル Figure 3: Deep candidate generation model architecture showing embedded sparse features concatenated with dense features. Embeddings are averaged before concatenation to transform variable sized bags of sparse IDs into fixed-width vectors suitable for input to the hidden layers. All hidden layers are fully connected. In training, a cross-entropy loss is minimized with gradient descent on the output of the sampled softmax. At serving, an approximate nearest neighbor lookup is performed to generate hundreds of candidate video recommendations. 図はレコメンド群の候補たちを生成するモデルを説明しているものだ。 視聴履歴や検索履歴をベクトル化して、そこにユーザーの地理的な情報や年齢、性別などの情報をベクトル化してくっつけている。 それをReLU関数に3回かけてsoftmaxでトレーニングしてクラスの確率を推定してる。(多クラス分類なので 最後に、上記でやった結果を最近傍探索して上位N件を取り出している。 まとめ YouTubeのレコメンドに関する論文 『 Deep Neural Networks for YouTube Recommendations 』を紹介しました。 YouTubeってついつい見て時間経っちゃってる時ありますよね。 その裏ではこんなことが起こっているんだな、と一端を知ることが出来ました。 機械学習に詳しいわけではないので理解できない部分も多々ありましたが、 YouTubeのレコメンドの仕組みが少しでも理解 できて感動しました。 もし、YouTubeに興味がある方がいれば読んでみると面白いかもしれません。 さいごに 最後に告知です。delyではエンジニアを絶賛募集中です。 ぜひお気軽にご連絡ください。 https://www.wantedly.com/projects/329047
アバター
本記事は dely Advent Calendar 2019 19日目の記事です。 昨日はWebフロントエンドエンジニアのしらりんくんが「Vue.jsでカスタムディレクティブを使ってユーザーの「見てる」を可視化する」という記事を書きました。ぜひご一読を。表示されて1秒経ったら色が変わる動画が見ていて気持ちいいです。 こんにちは。継続の鬼、kenzoです。 冬ですね。寒いですね。みなさまにおかれましてはますますご健勝のこととおよろこび申し上げます。 。。本当にご健勝でしょうか。風邪をひいていたり、なんとなく体調悪い日が続いている、なんてことはないでしょうか。 「日々開発に打ち込むためにはまず健康でなければならない」 そのような思いからこの1年、自分の体のためにいくつか続けてきたことがあります。 この記事ではその続けてきたこと、それを始めてから続けられた理由、そこから見えてきた新しいことを始めて習慣化させる方法についてお伝えします。 続けてきたこと ごはんログ 健康は食事から。ということで、自分の食生活を見直し、改善するために1年間ほぼ全ての食事や間食の写真を撮ってTwitterに投稿してきました。 以前、弊社の管理栄養士の同僚が趣味で食生活を見てくれるという取り組みがあり、それに手を上げてお願いしたのが始まりでした。 https://twitter.com/sakkko_dely/status/1067048758322454528 食事を投稿することで、自分が普段食べているものをきちんと把握したり、人の目を意識することでヤバい食べ方を減らすことを期待してのことでした。 一部の例外(家で飲む水や薬、サプリメント等)を除き、朝昼晩の食事に加えて仕事中に食べるおやつ、筋トレ後のプロテイン、デパートでの試食、果ては駅伝中にもらった水まで撮影してTwitterに投稿してきました。 お昼ごはん pic.twitter.com/g2J50q7LWn — kenzo (@kenzo_gohan) 2019年11月21日 飲み物 pic.twitter.com/Qz9edDCreV — kenzo (@kenzo_gohan) 2019年11月16日 この取り組みは1年間続いたところで卒業?という形で終了することになりました。 体組成記録 日々の自分の体の変化を認識できるよう、毎朝体組成計で自分の体組成を計測しています。その内容はTwitterにも投稿しています。 これも前述の同僚の取り組みに合わせて始めましたが、ごはんログを卒業?した今でも続けられています。 体重:63.90kg 体脂肪率:15.40% 筋肉量:51.30kg 筋肉スコア:1 内臓脂肪レベル:6.5 基礎代謝量:1509 体内年齢:25歳 推定骨量:2.80 #tanita #healthplanet #beyond2020 #マイベストプログラム — kenzo (@kenzo_gohan) 2019年12月17日 タニタの体組成計を使っているので、Twitter連携で自動でこのような投稿をしてくれます。 家トレ 毎日家で筋トレをしています。こればかりは色々な理由(けが等)により1年間は続いてはいませんが、いろんなパターンをやってみて、ここ3ヶ月ほどは継続中です。 筋トレアプリ 筋トレ用のアプリを使って日々家トレをしていました。様々な部位の筋トレメニューを日替わりで提供してくれるアプリでした。けがをして一度途絶えた後、再開時の強度が高めだったためか、復帰したものの長続きさせることはできませんでした。 筋トレグループ 家で腹筋をした回数を報告し合うLINEグループを友人と作って競い合っていました(二人でしたが)。当時はその回数をスプレッドシートにメモして月々の記録をしていました。こちらは数年間続きましたが、他の競合筋トレ台頭の憂き目に遭い、今ではそのグループも静かになってしまいました。 プランクアプリ これは今でも続いている習慣です。上記の筋トレアプリと同様にメニューを提供してくれるアプリでプランクに特化したものを使っています。こちらも1度けがによる離脱ピンチがありましたが、前回(上記の筋トレアプリ)の反省を踏まえ、復帰時には強度を弱くしてハードルを下げた状態で再開したためか、再度習慣化させることができています。 続けられた理由 以上の習慣について振り返ってみると、やってこれた(やめてしまった)のにはいくつかの理由がありました。 ごはんログ やります宣言・人に見てもらう そもそもの始まりが食事を見てもらうためだったということもあるのですが、きちんと「やります!」と宣言し、Twitterに投稿して見てもらうことで、続けざるを得ない環境となりました。 褒めてもらう・定期的なリアクション ランチや飲み会の場でも摂取する全てのものを撮影してきたので、同席した方から「え、インスタ?インスタ?ww」などと聞かれることが度々ありました。 その度にこういう取り組みをしていますと伝えてきましたが、その際によく「へーよく続けられるね」のようなお褒めの言葉をいただくことができ、続けるモチベーションになっていました。 また、Twitterをフォローしてくれている同僚からも、たまに自分の投稿した食事を話題に上げてもらえることがあり、ちゃんと見てもらえているし続けようという気持ちになりました。 体組成記録 起きたら計る 毎朝起きたら必ず体組成計に乗る。というやり方で実施してきました。 起きたら目覚ましを止めて毎回同じところに向かうというルーティーンの動作ができ、忘れずに実施することができていました。 「起きたら◯◯する」は他の習慣づくりにも良さそうです。ただ、朝起きたら家トレも並行して実施していた時期には、体組成計に乗るのを忘れてしまうこともありました。同時にいくつものことを忘れずにやるのは難しいですね。 仕組みづくり 毎日体組成計に乗っていると、自分の組成がどんな変化をしているか知りたくなります。 そこで、GASでいくつかの項目の実測値と移動平均のグラフを作成し、その画像をTwitterに投稿するスクリプトを作成しました。詳しくは後述します。 ちょっと手間をかけてスクリプトを作ったので途中でやめたらもったいない。何より自分でスクリプト作って投稿されるグラフが見たい。という気持ちで続けることができました。 グラフ #tanita #healthplanet pic.twitter.com/6VtdxBpBON — kenzo (@kenzo_gohan) 2019年12月15日 家トレ カレンダーに○ 使用していたアプリではトレーニングが完了するとカレンダーに◯がつきました。 ただやった日に○がつくだけなのに、それが並んで増えてくるとどうしても途絶えさせたくなくて続けていたところがありました。 成果を報告 友人と毎日成果を報告し合うことで、やらないといけない雰囲気になっていました。 時に褒め合い、時に煽り合うことで互いにモチベーションを高められていました。 ただし、これについては双方のモチベーションや回数に偏りがある場合にはうまく噛み合わないこともありました。 充実感・体の変化 他の2つと比べて時間もかかり負荷も高い家トレでしたが、その分をこなしたときの充実感は高く、毎日やることで「自分、めっちゃやってる」感を得られていました。 これは意外と大事で、休みの日にこれ以外なにもしなくても、「今日は筋トレしたから充実した日だった。 」と思えるくらいのパワーがありました。 また、やっているうちにお腹周りがすっきりしてくる等の体の変化を感じられるようになってきました。 起きたらやる これも上記の体組成記録と同様、朝目が覚めたらやる形式でやっていました。 筋トレ → プロテイン → シャワーという流れができ、ちゃんと目も覚めるため、うまく続けられていました。 しかし、上記にもあるように、他の朝起きたらシリーズと被ってしまい、その習慣の実施を忘れてしまうこともありました。 高負荷(ダメだった理由) 一度は習慣となっていた筋トレアプリでしたが、一旦離れてしまった後に再開する際にきつめのトレーニングから始めてしまったため、「これからこんなんやってくの、、無理」という気持ちになってしまいました。こうなってしまうと再び習慣化するのは難しく、数日で終わってしまいました。 習慣化させるには 人を巻き込む 一人だけで新しいこと *1 を始め、習慣化させて実施し続けるのはかなり難しいことだと思います。 しかし、上記の理由にいくつかある 人に宣言する 人に見てもらう 人と競う のように他の人を巻き込むことで、「見られているしやらないと」だったり「あいつには負けてられない」というように、始めることや続けることに対するモチベーションを高めることができます。 私はこれによってハードルをかなり下げることができました。ぜひ何かを始める際には身近な方を巻き込んでやってみてください。 負荷を減らす しんどいものはなかなか続きません。 負荷の高いものでも一度や二度なら我慢できるかもしれませんが、それを長期的に継続していくのはかなり難しいと思います。 *2 逆に言うと、初めのうちはかなり簡単なこと(こんなんでいいの?くらい)から始めると、比較的簡単に続けることができると思います。 負荷を高めるのはそうして簡単なことを続けられた後でよいと思います。 もったいない 「もったいない」という気持ちも習慣を続けていく上では役に立ちました。 上記の例であげた「せっかくスクリプトを作ったし使わないと」「せっかくここまで◯をつけたし途切れさせたくない」といった、せっかくやってきたのにもったいないという気持ちによって続けられた部分はありました。 一見するとサンクコストの説明で悪い例として使われそうなこの心の動きも、良い行動の習慣化にはうまく利用できました。 習慣化のためにはちょっとひと手間かけたり、自分のやってきた積み重ねが見えるようになっていると、離脱の防止になるのでおすすめです。 まとめ 習慣化することは気持ちの勝負のようなところがあります。ですので、いかに自分がそれをやる気持ちを高められるか、やりたくない気持ちを抑えられるかが決め手となります。上記の内容もほぼそのどちらかに当てはまるものとなっています。 今回は私が習慣化してきた内容を振り返り、その要素をご紹介いたしました。これがみなさまの習慣化に少しでも役立てばと思います。 また、これらは個人の経験ベースの話なので、心理学等に基づいた習慣化の話とは同じところも異なるところもあり、見比べてみても面白いと思います。 おまけ 体組成のTwitterへのグラフ投稿の自動化 体組成計記録において作成した、Twitterに投稿した毎日の体組成データを元にグラフを描画し、その画像をTwitterに毎週投稿するスクリプトの詳細です。 基本的にはGASで下記のことをやっている感じです。 Twitter APIの使用準備 毎日の投稿を蓄積 自分のTwitterの投稿から特定のタグをついたものを取得 https://api.twitter.com/1.1/statuses/user_timeline.json?user_id=TwitterのID&count=20&trim_user=t パースして項目毎にスプレッドシートに保存 ついでに平均値を計算してそれも保存 毎週グラフをTwitterに投稿 必要な日数分のデータを別シートにコピー グラフを描画 Twitterに投稿 グラフ #tanita #healthplanet pic.twitter.com/6VtdxBpBON — kenzo (@kenzo_gohan) 2019年12月15日 実際に使っているコードに近いものがこちらです。 TANITAの体組成計連携Tweetを元に作成したグラフを投稿 · GitHub おわりに 明日はAndroidエンジニアのtummyさんによる「何もわからない状態からいち早く脱するためのコードリーディング法(Android 版)」です。お楽しみに! delyではエンジニアをめちゃめちゃ募集中です。ご興味ありましたらぜひこちらから! delyの開発について知りたい方はこちらもあわせてご覧ください! *1 : 難しかったり苦痛を伴うものです。ゲームとか楽しいから習慣化するのめちゃ簡単ですもんね。 *2 : もちろん他のやる理由があればその限りではありません(お金を払って通うジム、業務上やらないといけないことなど)
アバター
はじめまして。 dely, Inc. の @sakura です。 本記事では、 Redash の使用状況及び便利なTipsを3つ紹介します。 この記事は BIツール研究所 Advent Calendar 2019 - Qiita の19日目の記事です。 社内のBIツール使用状況 誰が使用するか どのくらいのペースでクエリが増えるか Redashのここが便利 クエリパラメータ ダッシュボード クエリのお気に入り登録 まとめ さいごに 社内のBIツール使用状況 Redash公式のトップページ 社内のBIツールは 主に Redash を使用 しています。 バージョンは7系を使用しています。 過去には Metabase と併用していたこともあるのですが、同じ用途のものが複数あることで「どんな時にどっちを使用するの?」など利用者の混乱を招いてしまっていたため現在は Redash のみを使用しています。 誰が使用するか エンジニア、マーケ、調理部、セールス など色々な人が使用します。 クエリを書くのはエンジニアやマーケが中心です。 どのくらいのペースでクエリが増えるか 入社した当初(約2年前)はRedashにほぼクエリがなく、 「何をお手本にクエリを書けばいいんだ…」と絶望したことを覚えています。 あれから会社の人数も増え、知見もどんどん溜まり、クエリを書く人も増え、 今では毎日クエリが増えていっています 。 1日で数個〜数十個増えます。 自分の場合、今年は200個以上クエリを書きました。 Redashのここが便利 Redashを使っていて、便利で頻繁に使用するTipsを紹介します。 クエリパラメータ クエリパラメータを日付に適用している例 公式ドキュメント https://redash.io/help/user-guide/querying/query-parameters クエリパラメータとは、クエリの一部をパラメータ化して、クエリ枠外にそのパラメータに値する数値や文字列をいれるだけで、 クエリを実行できる優れもの です。 これの何が良いかというと、普段クエリを書かない方にとってとても操作が簡単になることです。 あの初めて見るものにとっては不思議な文字列であるSQLを直接触らなくて済むというのは心理的負荷がぐっと下がります。 私はこの機能を頻繁に利用しています。 クエリパラメータを使用したクエリを共有している例 クエリパラメータはとても便利なのでぜひ一度使ってみてください。 ダッシュボード 公式ドキュメントdashboard-editing内にある画像 ダッシュボードの例 公式ドキュメント https://redash.io/help/user-guide/dashboards/dashboard-editing ダッシュボードは複数のクエリの結果をまとめて、1ページ内にカスタマイズして見ることができる機能 です。 クエリの結果の表示の仕方は、表形式かグラフ形式か選択することができます。 全体を俯瞰して結果を確認したいときや新機能の結果を複数のデータから判断したい時などにダッシュボードを使用すると便利です。 私の使用例をあげると、クラシルの検索機能を定期的に監視する用にダッシュボードを使用しました。 キーワードの検索回数のグラフ、OS別の検索離脱率のグラフ、検索結果が0件だったキーワードの表など検索に関する様々な情報についてまとめました。 毎日確認するのでも1ページにまとまっているので確認が非常に便利でした。 クエリのお気に入り登録 地味すぎてこの機能を使っている人は思ったよりそんなに多くないかもしれません。 その名の通り、クエリをお気に入り登録でき、お気に入りしたクエリ一覧を見ることができる機能です。 この機能はRedashのバージョン5から導入されました。 公式ドキュメント https://redash.io/help/user-guide/dashboards/favorites-tagging お気に入りする方法は、該当クエリのクエリタイトルの横にある星をクリックするだけです。 白枠の星から黄色になったらお気に入りした証拠です。 右側に表示されているこの枠のFavoritesを押すと、お気に入りしたクエリ一覧が確認できる お気に入りしたクエリを見るには、ホーム画面もしくはQueriesを押した時にでる右枠のFavoritesを押すと確認できます。 Redashにクエリが溜まれば溜まるほど、「あれ?あのクエリどこいったっけ」「検索してもあのクエリ見つからない」 ということが多くなります。頻繁に使用するクエリやストックしておきたいクエリにはお気に入りをしておくと便利です。 まとめ 本記事では、Redashの使用状況及び便利に使うためのTips3選を紹介しました。 BIツールを導入しても知らない機能っていっぱいありますよね。 私もまだまだRedashの知らない機能があると思うので、発掘して便利に活用していこうと思います。 さいごに 最後に告知です。delyではエンジニアを絶賛募集中です。 ぜひお気軽にご連絡ください。 https://www.wantedly.com/projects/329047    
アバター
目次 目次 はじめに とある日のこと カスタムディレクティブとは やりたいこと クラシルのデータ分析基盤 イベント定義シート イベントパラメーター型定義シート サンプル実装 カスタムディレクティブのコード Vueコンポーネントに適用してみる クラシルWebに実際に導入してみた結果 この実装を通して学んだこと おわりに はじめに こんにちは、dely株式会社 開発部の白石( しらりん )です。 2019年新卒として入社し、現在Webフロントエンドエンジニアを担当しています。 昨日はiOSエンジニアのtakaoさんが、「 個人アプリの開発で陥った6つの失敗とそこから学んだやらないことの重要性 」という記事を書いてくれました。 本記事は dely Advent Calendar 2019 18日目の記事になります。 qiita.com 本記事のテーマは、タイトル通りVue.js(以下Vue)でユーザーの「見てる」を可視化することです。 delyに入社して実際に行った業務を通して考えたこと・学んだことをなぞりながら書いていきます。 この記事内では以降「ユーザーの見てる」を「インプレッション」と表現します。 とある日のこと 入社して1ヶ月半ほど経った5月半ば、この頃の自分の業務はOJTを兼ねたクラシルWebのリファクタリングが中心で、slim-railsで記述されていたコードをTypeScript + Vue + Node.js(+ vue-server-renderer)でSPA・SSR対応したものに書き換えたりしていました。 そんなある日、Web開発チームにビジネスサイドでWebの広告担当をしているAさんからこんな依頼がきました。 Aさん「(とあるページ)の広告枠が全然ユーザーの目に留まっていない気がする。ユーザーがページのどこを見ているのか調べる仕組みが欲しいです。」 その依頼自体は先輩フロントエンドエンジニアのOさんに来ていたのですが、そのやりとりを盗み聞きしてた自分が、 「そのタスク、おもしろそうなのでください^^」 と迫り奪取に成功、初めて分析系のタスクを担当することになりました。 タスクをもらえたのは良いものの、どう実装しようかなと悩んでいたところにOさんがアドバイスをくれました。 Oさん「これならカスタムディレクティブを使うといいかも。」 自分「?」 Vue歴 ≒ 社会人歴な当時の自分はカスタムディレクティブを知りませんでした。 カスタムディレクティブとは ディレクティブには「指令・命令」という意味があり、Vueにおけるディレクティブについて以下のサイトではこのように説明されています。 ディレクティブとは、 DOM 要素に対して何かを実行することをライブラリに伝達する、マークアップ中の特別なトークンです。 012-jp.vuejs.org Vueを使われている方にはお馴染みの v-if や v-for など、接頭辞に v が付くこれらがVueのディレクティブです。 ( - より後ろはディレクティブIDといいます) そしてこのディレクティブは自分で作ることができ、それをカスタムディレクティブといいます。 詳しい内容は以下を参照してください。 jp.vuejs.org やりたいこと ページがユーザーにどこまで読まれているかを計測する必要があったとして、恐らくそれを実現する一番単純な方法は、ページの高さに対してスクロールされた高さで割合を求めることだと思います。しかし、これではスクロールされた位置までに表示されていた要素がユーザーに見られていたかは正確に分かりません。 例えば、とあるページが上から順にa, b, cという要素で構成されている時、cの位置までスクロールされているからといってaとbがユーザーに見られているとは限りません。aとbをスクロールで飛ばしてcを見ているかもしれないからです。 そこで、インプレッションを計測したい特定の要素の高さと幅の指定割合が画面内に指定秒数の間継続的に表示されていることを一度だけ検知できるカスタムディレクティブを実装することにしました。 クラシルのデータ分析基盤 この記事ではカスタムディレクティブを使ってインプレッションを検知するサンプルを実装しますが、データの送信や保存に関する実装には言及しません。参考までにクラシルではこんな感じでやってるよというのを簡単に書いておきます。 構成 クラシルで使用している分析基盤はAWSを利用しており、運用するイベントはスプレッドシートに定義するようにしています。 スプレッドシートに定義したイベントは、特定のプリフィックスをつけたブランチをpushすることでコードを自動生成することができ、そのブランチを取り込み(またはそのブランチ上で)開発していきます。 イメージ図 イベントは、 イベントの識別子(必須) イベントのカテゴリ(任意) イベントのパラメーター(必須) といった感じで値を定義できるようになっており、1行が1イベントになります。イベントパラメーターにはTypeScriptの型を指定することができ、複数イベントで使いまわせるものや膨らみがちなものはイベントパラメーターの型定義用シートがあるのでそちらで管理するようにしています。 その他にも、イベントおよびパラメータの詳細説明や開発ステータス(0だったら実装中、1だったら実装完了(運用可能))などを記述し、実装者以外のメンバーと認識を合わせやすいように運用しています。 この分析基盤を活用し、サービスをより良くするために様々な施策を回しています。 以下は今回のインプレッション計測のイベント定義の例です。 イベント定義シート 列の名前 行の値 イベント識別子 element_imp イベントの説明 指定した要素のインプレッションを計測するイベント イベントパラメーター ImpTargetElementName imp_target_element_name int? index イベントパラメーターの説明 imp_target_element_name...要素の名前、必須 index...要素が表示される順番(0始まり)、任意 ↓ 日付や実行されたページのパスなどを生成するベースの分析イベントを継承したイベントが生成される イベントパラメーター型定義シート 列の名前 行の値 型名 ImpTargetElementName key carousel_slide ads horizontal_scroll_item card_layout_item value carousel_slide ads horizontal_scroll_item card_layout_item ↓ 列挙型が生成される export enum ImpTargetElementName { carousel_slide = 'carousel_slide' , ads = 'ads' , horizontal_scroll_item = 'horizontal_scroll_item' , card_layout_item = 'card_layout_item' , } サンプル実装 それでは実際にサンプルを実装してみます。 カスタムディレクティブのコード ここでは分かりやすくするために、インプレッションの条件を満たした要素の色を変更するようにしています。 import _Vue , { PluginFunction , VNode } from "vue" ; import { DirectiveBinding } from "vue/types/options" ; import { ImpTargetElementName } from '../enums/autogen/ImpTargetElementName' ; export const impressionDerective = { inserted: ( el: HTMLElement , binding: DirectiveBinding , vnode: VNode ) => { const { name , index , time , threshold } = typeof binding.value === 'object' ? binding.value : { name: binding.value , index: null , time: null , threshold: null } ; const handler = (() => { let timer = null as null | number ; let isExecuted = false ; return ( entries: IntersectionObserverEntry [] ) => { if ( !isExecuted ) { entries.forEach ( entry => { if ( entry.isIntersecting ) { const key: keyof typeof ImpTargetElementName = name ; if ( ! ( key in ImpTargetElementName )) { console.error ( `${key}はImpTargetElementNameに定義されていません` ); return; } timer = window .setTimeout ( () => { if ( timer ) { /* データを送信する処理 例) vnode.context.$pushLog({ name: EventName.element_imp, params: { imp_target_element_name: ImpTargetElementName[key], ...(index == null ? {} : { index }), }, }); */ el.classList.add ( "isActive" ); } isExecuted = true ; timer = null ; } , time || 1000 ); } else if ( timer ) { clearTimeout ( timer ); timer = null ; } } ); } } ; } )(); const observer = new IntersectionObserver ( handler , { threshold: threshold || 0.5 } ); const observeHander = () => observer.observe ( el ); const unobserveHandler = () => observer.unobserve ( el ); const removeAllHandlers = () => { el.removeEventListener ( "impPluginObserve" , observeHander ); el.removeEventListener ( "impPluginUnobserve" , unobserveHandler ); el.removeEventListener ( "impPluginRemoveAllHandler" , removeAllHandlers ); } ; el.addEventListener ( "impPluginObserve" , observeHander ); el.addEventListener ( "impPluginUnobserve" , unobserveHandler ); el.addEventListener ( "impPluginRemoveAllHandlers" , removeAllHandlers ); el.dispatchEvent (new CustomEvent ( "impPluginObserve" )); } , unbind: ( el: HTMLElement ) => { el.dispatchEvent (new CustomEvent ( "impPluginRemoveAllHandlers" )); } } ; const install: PluginFunction < never > = ( Vue: typeof _Vue ) => { Vue.directive ( "imp" , impressionDerective ); } ; export default install ; } ; const install: PluginFunction < never > = ( Vue: typeof _Vue ) => { Vue.directive ( "imp" , impressionDerective ); } ; export default install ; Vueコンポーネントに適用してみる ページっぽいコンポーネントを作り、カスタムディレクティブを適用してみます。 任意のファイルで Vue.use(インポートしたv-impプラグイン) する。 < template > < div class = "SpRoot" > < div class = "SpRoot-carousel" > < Carousel :per-page= "1" :autoplay= "true" :loop= "true" :autoplayTimeout= "5000" paginationPosition= "bottom-overlay" paginationColor= "#ccc" > < Slide v- for = "(item, i) in slideItem" :key= "i" > < div v-imp= "{ name: 'carousel_slide', time: 2000, threshold: 1, index: i }" class = "SpRoot-carouselSlide" > Slide {{ i }} </ div > </ Slide > </ Carousel > </ div > < h2 > Section Title </ h2 > < div class = "SpRoot-horizontalScroll" > < div v- for = "(item, i) in horizontalScrollItem" :key= "i" v-imp= "{ name: 'horizontal_scroll_item', index: i }" class = "SpRoot-horizontalScrollItem" > Card {{i}} </ div > </ div > < Ads v-imp= "'ads_1'" /> < h2 > Section Title </ h2 > < div class = "SpRoot-cards" > < div class = "SpRoot-card" v- for = "(item, i) in cardItems" :key= "i" > < div v-imp= "{ name: 'card_item', index: i }" class = "SpRoot-cardContent" > Card {{i}} </ div > </ div > </ div > </ div > </ template > インプレッションを取りたい要素に v-imp ディレクティブを付与し、imp_target_element_nameに定義済みの値を設定します。 デフォルトでは指定した要素の50%が画面内に1秒以上表示されていることをインプレッションとみなすように条件にしています。 < Ads v-imp= "'ads'" /> イベントパラメーターにインデックス値を含めたりインプレッションとみなす条件を変更したいときは、 v-imp ディレクティブに対して以下のようにイベント名を含めたオブジェクトでオプションを指定することができます。以下の例では、カルーセルのスライド要素の100%が画面内に2秒間継続的に表示されていた場合をインプレッションとみなすようにオプションを指定しています。 < Slide v- for = "(item, i) in slideItem" :key= "i" > < div v-imp= "{ name: 'carousel_slide', time: 2000, threshold: 1, index: i }" class = "SpRoot-carouselSlide" > Slide {{ i }} </ div > </ Slide > 実際に動かしてみるとこんな感じになります。 後で使う pic.twitter.com/o3YYUUXtot — しらりん (@Srrn97) 2019年12月18日 ディレクティブを指定した要素の50%が継続的に1秒以上(スライドは100%が2秒以上)画面内に表示された時色が変わったのが確認できました。 クラシルWebに実際に導入してみた結果 実際に導入し、グラフ化してみた こちらが実際にクラシルWebの本番環境のとあるページに導入し、計測した値をグラフにしたものです。 青い棒はカスタムディレクティブを適用した要素のインプレッション数で、一番左がページトップの要素で左から順にページの上から表示される要素となっています。赤い折れ線はユーザーの離脱率を表しています。 当然ページの下に行くほど離脱率は高くなっていきますがこのページは特にその傾向が強く、目を止めて欲しいコンテンツにたどり着く前に80%近くのユーザーが離脱していることが分かったりしました。 この実装を通して学んだこと このタスクをこなす以前の自分は「効率的な実装をしたい」とか「ここのUIやアニメーションもっとこだわりたい」とかエンジニアリングに対する意識にばかり頭がいきがちでした。 この機能を実装し、実際にユーザーの動きが目に見える形になって初めて、入社した時から言われていたデータを見ることの重要性を深く理解することができ、より良いサービスづくりをする上でどちらも欠かせないものだと気づきました。 まだまだ自分が見えている範囲は周りの先輩たちに比べて狭いですが、より良いものを作るために必要な技術を伸ばし、データを見る → どうしてそうなるのか考える(法則を見つけるための実装をする) → データを活かす実装をする → データを見る の積み重ねができるエンジニアを目指して頑張ります。 おわりに 現在delyではエンジニアを積極募集中です。フロントエンドエンジニアの方も大大大歓迎なので、興味のあるかたはぜひご応募ください。 www.wantedly.com delyの開発部について少しでも気になる方は、ぜひこちらも読んでみてください。 明日はAndroidエンジニアのkenzoさんが、「エンジニアは体が資本でしょ。と思って始めた習慣とその続け方」という記事を書いてくれます。お楽しみに! adventar.org
アバター
こんにちは。 delyコマース事業部エンジニアの john です。 もともとは開発部でiOSエンジニアとしてクラシルのiOSアプリ開発をやっていましたが、今年のはじめから新規事業のコマース事業部でwebのフロントエンドやRailsアプリケーションとかいろいろと開発をしています。 この記事は「 dely Advent Calendar 2019 」の16日目の記事です。 昨日はSREの井上さんによる「10分で完成!WEBサイトパフォーマンス計測基盤 ver.2019」という記事でした。 tech.dely.jp 今回は、Capistranoを使ってRailsアプリケーションをデプロイしたときに環境変数でハマった話を書きます。 なかなか、これ系の記事が少なかったので、gemの中を見るところまでしてみました。 1つのサーバーを使いまわしてのデプロイの話です。インフラがコード化(Infrastructure as Code)され、使い捨てサーバー(Disposable Components)でのデプロイでは当てはまらないかもしれません。 1. Capistranoとは? 2. .bash_profileに記載した環境変数が読み込まれない 3. pumaをrestartしても環境変数が読み込まれない 1. Capistranoとは? まず最初に軽くCapistranoの説明をします。 https://capistranorb.com  にはこう書かれています。 A remote server automation and deployment tool written in Ruby. https://github.com/capistrano/capistrano  にはこう書かれています。 Capistrano is a framework for building automated deployment scripts. Although Capistrano itself is written in Ruby, it can easily be used to deploy projects of any language or framework, be it Rails, Java, or PHP. リモートサーバーの自動化のデプロイツールのようです。 デプロイツールなので、Railsアプリケーションだけでなく、他の言語のプロジェクトでも使えるようですね。 また、Rubyで書かれているのでRubyのプロジェクトでは使いやすいですね。 オフィシャルのプラグインとして Capistrano::Bundler Capistrano::Rails サードパーティのプラグインとして Capistrano::Puma Capistrano::yarn などのRailsに関連するプラグインがあって、デプロイのプロセスの間にタスクを走らせることができます。 たとえば、 Capistrano::Puma lib/capistrano/puma.rb#L90-L93 では、プラグインをインストールすると def register_hooks     after ' deploy:check ' , ' puma:check '     after ' deploy:finished ' , ' puma:smart_restart ' end 上記のようにdeployが終わったら、puma:smart_restartするようになっていて、再起動してくれるようになっています。 実際のpuma:smart_restartはこちらに書いてあります。 Capistrano::Puma lib/capistrano/tasks/puma.rake#L59-L78 if test " [ -f #{ fetch( :puma_pid ) } ] " and test :kill , " -0 $( cat #{ fetch( :puma_pid ) } ) " # NOTE pid exist but state file is nonsense, so ignore that case execute :pumactl , " -S #{ fetch( :puma_state ) } -F #{ fetch( :puma_conf ) } #{ command }" else # Puma is not running or state file is not present : Run it invoke ' puma:start ' end 起動していなかったらstartするようになっています。 次から、本題に入っていきます。 2. .bash_profileに記載した環境変数が読み込まれない あらかじめ、sshでログインして、.bash_profileに環境変数を記載していました。 先ほどのpumaの例に示したとおり、デプロイ後に再起動したのに、環境変数が読み込まれていないという現象にぶち当たりました。 sshでサーバーにログインしてrails sするとちゃんと読み込まれているのに、なぜでしょう?🤔 理由は、公式の記事を見つけたので、こちらをみたらわかりました。 capistranorb.com By default Capistrano always assigns a non-login, non-interactive shell. Capistranoはnon-login, non-interactiveなshellだからでした。 BashInitialisationFiles 上記のように.bash_profileや.bashrcは読み込まれないという、shellのお話でした。 対応策としては、capistranoで定義されているdefault_envという変数に渡すということをしています。 default_envは最終的にはcapistrano内部で使っているsshkitに渡されるようです。 capistrano/sshkit lib/sshkit/command.rb#L149-L151 上記の方法をとった理由は、dotenvを本番環境で使いたくなかったのと、他に簡単にできる方法を思いつかなかったからです。もしかしたら別の方法があるかもしれません。 イマイチなのが、すべてのコマンドで渡されてしまっているところと、環境変数が増えてくると辛いことです。 環境変数に関しては、別のtaskの中で処理するのもありかと思っています。 以下はaws systems manager パラメータストアを使った場合のtaskになります。 aws systems manager パラメータストア についての説明は省かせていただきます。 require ' aws-sdk-ssm ' namespace :env do def ssm_client Aws :: SSM :: Client .new end def ssm_path " /path/to/ssm " end def fetch_parameters parameters = [] is_finished = false next_token = nil until is_finished result = ssm_client.get_parameters_by_path( { path : ssm_path, recursive : false , with_decryption : true , next_token : next_token } ) parameters += result.parameters next_token = result.next_token is_finished = next_token.nil? end puts " parameter count: #{ parameters.size }" parameters end def ssm_env dict = {} fetch_parameters.each do | params | key = params.name.gsub(ssm_path, '' ) dict[key] = params.value end dict end task :set_default_env do set :default_env , fetch( :default_env ).merge(ssm_env) end end deploy.rbでdeloyのtaskが呼ばれる前に呼ぶにしています。 invoke ' env:set_default_env ' 3. pumaをrestartしても環境変数が読み込まれない 2. の対処でようやく環境変数が読み込まれるようにはなったのですが、環境変数を追加・変更してdefault_envに反映しても、デプロイ後の再起動では反映された状態での再起動が起きませんでした。 GitHubのIssueで聞いているものもあります。 github.com どうやら原因はpumaの再起動の仕組みによるものでした。 puma/restart.md at master · puma/puma · GitHub puma/signals.md at master · puma/puma · GitHub pumaはシグナルを使ってプロセス間の通信を行っているようです。 再起動時は"SIGUSR2"を送っています。 bundle exec pumactl restart --state path/to/state_file # lib/puma/control_cli.rb#L206 when " restart " Process .kill " SIGUSR2 " , @pid 起動中のプロセスがそれを受け取ってリスタートさせるようです。 #lib/puma/launcher.rb#L407 Signal .trap " SIGUSR2 " do restart end 起動中のプロセスで以下のメソッドが呼ばれ、再起動されます。 最終的にはKernel.exec(*argv)をして再起動しているようです。 # lib/puma/launcher.rb#L407 log "* Restarting..." ENV.replace(previous_env) @runner.before_restart restart! # /lib/puma/launcher.rb#L238 def restart! argv = restart_args Dir.chdir(@restart_dir) argv += [@binder.redirects_for_restart] Kernel.exec(*argv) # このときのargsは # ["path/to/ruby", "/path/to/bin/puma", "-C", "config/puma.rb", {:close_others=>true, 10=>10}] # なので、pumaをもう一度起動するように実行しているようです。 この際、pumactl restart時に渡した変数は、起動中のプロセスへは渡されないので、環境変数は読み込まれないということになるようです。 対応として、pumaを停止して、再度起動するということをしています。 bundle exec cap production puma :stop bundle exec cap production puma :start 一旦アプリケーションが停止してしまうので、あまり良い対処ではないのですがこれでしのいでいます。 今回ブログを書くにあたって、普段使用しているだけのpumaやcapustranoの中の実装を探ってみて、いろいろ新しい発見があって楽しかったです。  明日は、iOSエンジニアのtakaoさんの記事です。お楽しみに! 最後に、コマース事業部では事業も開発も挑戦することが多く、エンジニアを募集しています! また、デザイナーも大大大募集しているので、お知り合いの方で興味がありそうなかたがいれば、ぜひ教えてあげてください。 もし興味があるかたがいれば、お気軽にご連絡ください。 www.wantedly.com www.wantedly.com 参考 Pumaの使い方 まとめ - 猫Rails RackサーバーのPumaについて調べてみる - ゆーじのろぐ  
アバター
はじめに 本記事は dely Advent Calendar 2019 の15日目の記事です。 昨日は開発部サーバサイドエンジニアの高橋くんが「Rails6の複数データベースの仕組みと実装時にハマったところ」という記事を書きましたので是非読んでみてください。 tech.dely.jp こんにちは!dely開発部SREの井上です。 本記事ではWEBサイトのパフォーマンスを定期的に計測する仕組みについて紹介をしたいと思います。 実は去年のAdvent Calendarでも 同じような記事 を書いたのですが、時代背景に沿って計測するツールをsitespeed.ioからLighthouseに変更したので理由も含めて紹介させてください。 基盤の構築においては下記のサービスやツールを利用しています。 AWS CodeBuild S3 Athena Terraform Lighthouse 前置きはいいから構築方法だけ知りたいという方は こちら にどうぞ! 目次 はじめに 目次 WEBサイトのパフォーマンスについて WEBサイトのパフォーマンスの計測手段 定期的に計測することの必要性について 定期的なWEBパフォーマンス計測について 定期計測に適したパフォーマンス計測ツールについて 定期計測に適したパフォーマンス計測手法について PageSpeed Insights API について WEBパフォーマンス計測に特化した有料外部サービスについて Lighthouseを「計測する環境に左右されず」かつ「自動で定期的に動作させる」ための機構 可視化例 構築方法 前提 準備 1.リポジトリのフォーク 2.Terraformのtfstateファイル管理用のS3バケットの作成 3.terraform.tfvarsの作成 4.Terraform作業ディレクトリの初期化 5.対象サイトの指定 構築 計測結果の参照 HTML SQL カスタマイズ Lighthouseの設定 計測間隔の調整 料金について さいごに WEBサイトのパフォーマンスについて もう今さら言うことでもないと思いますが、WEBサイトのパフォーマンスはユーザ体験に大きく影響を与えます。 直近(2019/12/10)でも下記のような記事をGoogleが公開しています。 developers-jp.googleblog.com Chromeブラウザにおいて読み込みが速いサイトと遅いサイトを見分けるためのバッジが将来的に付けることを検討しているという内容ですが、サイトの読み込み速度をより一層ユーザとサービス提供者に意識させようという意思を感じます。 上記のような動きもあり、今後もWEBサイトのパフォーマンスを重視する傾向は変わることはないだろうと個人的には予想しています。 WEBサイトのパフォーマンスの計測手段 WEBサイトのパフォーマンスは重要だという話ですが、改善を行うためにもまずは現状を計測する必要があります。 2019年12月現在、WEBのパフォーマンスを計測する主な手段としては下記のようなものが存在します。 Chrome Developer Tools Lighthouse PageSpeed Insights WebPageTest sitespeed.io TestMySite Google自身もパフォーマンス計測ツールについてまとめているので興味があれば見てみてください。とても参考になります。 developers.google.com 定期的に計測することの必要性について WEBのパフォーマンスにおいてボトルネックを特定するだけであれば、その時点で数回の計測を実施すればよいですが、対策に伴う効果測定をしたい場合は対策後に再度計測を実施する必要があります。複数の対策を長期的に実施していく場合などは、対策を行う度に計測しなおす必要があります。 また、上記とは反対に何かのタイミングで意図せずパフォーマンスが悪くなっていないかを検知したいときやパフォーマンスが悪化した前後で何が原因だったのかなどを遡って確認したいこともあります。 上記のような要件を満たすためには、一定間隔で繰り返しWEBパフォーマンス計測を実施する必要があります。 定期的なWEBパフォーマンス計測について 定期計測における要件は下記のようになると考えます。 一定の条件下において指標の変動を可視化することが可能 計測する環境(例えばPCの性能やネットワーク環境)が起因して計測値がずれてしまうのであれば、何が原因で指標が上下しているのか正しく分からなくなるため、一定の条件化で計測し続けることが必要です。 パフォーマンスの問題のデバッグが可能 パフォーマンスが劣化したと考えられるタイミングにおいて、例えば問題がcssにあるのかjsにあるのかサーバのレイテンシにあるのかなどといった原因分析を可能とする指標が記録されているべきです。 ユーザ体験に関連するさまざまな指標が計測可能 ときにはリスク込みでパフォーマンスを犠牲にしなければいけない選択をする場合もあると思います。そういった選択をできるようにしておくためにも、パフォーマンスの劣化がどれだけユーザ体験に影響を与えているのかということも指標として記録できている必要があります。 定期計測において重要なのは「指標の変動が分かり」、「変動の原因がどこにあるのか明確にできること」だと考えます。 逆に言えば定期のパフォーマンス計測においては上記の要件を満たせば成り立つため、詳細なボトルネックの原因分析などを行うタイミングのパフォーマンス計測とは計測ツールを分けても問題ないと考えます。 定期計測に適したパフォーマンス計測ツールについて 2019年12月現在においてはLighthouse一択だと思っています。 LighthouseはGoogleがオープンソースで開発・公開しているWeb開発向けの診断ツールです。 下記のさくらのナレッジさんの記事が非常に詳しく解説してくれているのでこちらをご参照ください。 knowledge.sakura.ad.jp WEBサイトのパフォーマンスを手軽に計測するためにGoogleが提供しているサービスがPageSpeed Insightsになりますが、PageSpeed Insightsの分析エンジンにはLighthouseが利用されていることがGoogleによって公開されています。 webmaster-ja.googleblog.com WEBサイトのパフォーマンスを向上することの目的は当然ユーザ体験になるべきですが、その前段としてSEOが語られることは少なからずあると思います。そういったときは大抵の場合、指標のマスターとしてPageSpeed Insightsが挙げられます。定期計測において計測する指標や数値は可能な限りPageSpeed Insightsのものと一致させておいたほうが意思決定の根拠として扱いやすいのは間違いありません。 その証拠として前述で挙げているツールのほとんどが内部でLighthouseを利用する手段を用意しています。 www.sitespeed.io www.webpagetest.org 去年の記事においてはsitespeed.ioを測定ツールとして採用しましたが、上記の理由からLighthouseを使った計測にリプレースしました。 定期計測に適したパフォーマンス計測手法について 定期計測においては前述の通り計測する環境に左右されないことが必須になるため、ローカルPCの環境に依存するツールを用いた測定手法は適していません。また人間が定期的にツールを実行するのは現実的ではないため、自動で定期的にツールが実行される仕組みが必要になります。 計測ツールにLighthouseを使う場合でも、Lighthouseを「計測する環境に左右されず」かつ「自動で定期的に動作させる」ための機構は別途考える必要があります。 PageSpeed Insights API について PageSpeed Insights API を使うことで環境に左右されずLighthouseによる計測を行うことが可能です。 developers.google.com PageSpeed Insights API については低頻度であれば、利用するにあたってAPIキーすら不要なため非常に手軽に使えます。しかし、 Lighthouseの設定 が出来ないため、例えばPCとmobileの二通りで計測したり、ユーザエージェントをカスタマイズして計測したりといったユースケースに合わせたカスタマイズが出来ないことがデメリットとして挙げれれるため今回は採用していません。 WEBパフォーマンス計測に特化した有料外部サービスについて 最近はLighthouseが組み込まれた有料サービスが出てきています。 こういったサービスを使えば「計測する環境に左右されず」かつ「自動で定期的に動作させる」を達成することが可能そうです。(使ったことがないので明確には言えません) とても便利そうですがもちろん利用料がかかります。そもそも使ったことがなく評価出来ていないので検討の余地はありますが、今回はそこまで出来ていないので今後の課題にしようと思います。 calibreapp.com speedcurve.com Lighthouseを「計測する環境に左右されず」かつ「自動で定期的に動作させる」ための機構 やっと本題になりますが、Lighthouseを「計測する環境に左右されず」かつ「自動で定期的に動作させる」ための機構をAWSを使って考えてみました。 CodeBuild Lighthouseを実行する実行環境として利用します。 CloudWatchEventによって定期的に動作します。 S3 Lighthouseによる計測結果の格納と、HTMLのホスティングとして利用します。 Athena S3に格納された計測結果に対してSQLを実行するために利用します。 可視化ツール Athenaをデータソースとしてグラフ化などを行います。 可視化ツールに何を使うのかまではこの記事内には記載しませんが、delyではRedashを利用しています。 可視化例 delyでは例えば下記のSQLを実行することによって、 SELECT ' https://webperf-by-codebuild-58b94c1bbe1c0755.s3-ap-northeast-1.amazonaws.com/html/ ' || DOMAIN || ' / ' || device || ' / ' || category || ' / ' || YEAR || ' / ' || MONTH || ' / ' || DAY || ' / ' || hour || ' / ' || MINUTE || ' /output.report.html ' as link , DOMAIN, device, category, YEAR, MONTH, DAY, hour, MINUTE, CAST (YEAR || ' - ' || MONTH || ' - ' || DAY || ' ' || hour || ' : ' || MINUTE as timestamp) AS time, metrics.details.items[ 1 ].speedIndex / 1000.0 as speedIndex, metrics.details.items[ 1 ].firstContentfulPaint / 1000.0 as firstContentfulPaint, metrics.details.items[ 1 ].firstMeaningfulPaint / 1000.0 as firstMeaningfulPaint, categories.performance.score * 100 as performance_score, categories.accessibility.score * 100 as accessibility_score, categories. " best-practices " .score * 100 as best_practices_score, categories.seo.score * 100 as seo_score FROM " <Terraformによって生成されたデータベース> " . " lighthouse " where DOMAIN = ' {{ domain }} ' and device = ' {{ device }} ' and category = ' {{ category }} ' AND CAST (YEAR || ' - ' || MONTH || ' - ' || DAY || ' ' || hour || ' : ' || MINUTE as timestamp) >= current_timestamp - interval ' 1 ' month order by CAST (YEAR || ' - ' || MONTH || ' - ' || DAY || ' ' || hour || ' : ' || MINUTE as timestamp) desc ; 下記のような結果を得ることができます。 また上記の結果を下記のようにRedashを使ってグラフ化しています。 上記は弊社での可視化例ですが、Lighthouseによって様々なメトリクスが取得されているので色々な指標の相対的な変動を計測することが可能になっています。 構築方法 ここまで読んで頂いた方には伝わると思いますが、こんなに色々考えるのは面倒ですよね。 やりたいことはただ「WEBサイトのパフォーマンス計測を定期的に行いたい」だけなのに、どこまで考えなければいけないのだろう・・・という印象を持ってしまうと思います。 なのでこの基盤を10分で構築する方法を手順化しましたので紹介させて頂きます。 本記事で使用するコードは全てGithubにあげています。 github.com 前提 基盤構築に伴って最低限下記が実行環境に設定されている必要があります。 awscliのインストールとcredentialの設定 Terraformの利用に伴ってAWSのアクセスキーとシークレットキーの設定が必要です。 Terraformのインストール IAMに対するTerraformを実行するための十分な権限設定 AWS CodeBuildとGitHubの接続 AWSコンソール上のCodeBuildにおいてプロジェクト設定時のページで下記のように、「GitHubアカウントを切断」と表示されている必要があります。されていない場合は「GitHubに接続」ボタンからGitHub連携を済ませてください。 CodeBuildとGitHubが接続されていないとTerraformのapply時にエラーになりますのでご注意ください。 準備 1.リポジトリのフォーク 下記のリポジトリをご自身のアカウントにForkします。Cloneして新しくリポジトリを作成しても大丈夫です。 github.com 2.Terraformのtfstateファイル管理用のS3バケットの作成 空のS3バケットを1つ作成します。 既存のものでも大丈夫ですが、tfstateファイルのkeyをコードにべた書きしているので新規でS3バケットを作成することをおすすめします。 3.terraform.tfvarsの作成 sitespeed.ioの出力したhtmlをS3のホスティング機能を使って参照するため、自身のIPでアクセス制限をかけます。 またCodeBuildがソースを取得する先を、手順で作成したリポジトリのURLに変更します。 サンプルファイルがあるのでコピーしてから、 $ cd terraform $ cp terraform.tfvars.sample terraform.tfvars ファイル内のリポジトリのURLを作成した自身のGitHubリポジトリに変更します。 # 作成した自身のGitHubリポジトリ git_repository = "https://github.com/<user-name>/<repository-name>" 4.Terraform作業ディレクトリの初期化 下記のコマンドでTerraformの作業ディレクトリを初期化します。 $ make terraform-init cd terraform && \ terraform init Initializing the backend... bucket The name of the S3 bucket Enter a value: S3バケットの入力を求められるので手順2で作成したtfstate用のS3バケットの名前を入力します。 5.対象サイトの指定 計測するページのURLをurls.csvに記載します。 一行に ドメイン名,対象URL,カテゴリ の順で記載します。 複数行記載すると1回のCodeBuildの実行で複数のURLが計測されます。 カテゴリ名は対象URLをカテゴライズするために付与します。任意の文字列を入力してください。 例えば https://www.kurashiru.com/ を計測対象URLとする場合、下記のようになります。 トップページなのでカテゴリはtopとしました。 ※計測対象のWEBサイトはご自身が管理されているものを記載してください。 www.kurashiru.com,https://www.kurashiru.com/,top 構築 Terraformを実行します。 $ make terraform-apply しばらくすると下記のようにコンソールに出力されるので、確認の後に「yes」と入力するとリソースの生成が始まります。 Plan: 16 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: 上記コマンドで生成したリソースを削除する場合は下記コマンドを実行します。 $ make terraform-destroy 計測結果の参照 最大1時間待つか設定済みのCodeBuildを手動で1度実行するとS3に計測結果が置かれます。 HTML S3バケットにhtmlが出力されているのでブラウザで開くことで計測結果を閲覧することができます。 例えばURLは下記のようなものになります。 https://s3-ap-northeast-1.amazonaws.com/webperf-by-codebuild-33916b229c860831/html/example.com/desktop/top/2019/12/14/10/37/output.report.html SQL AWSコンソールのAthenaでSQLを実行すると結果が取得できるようになっています。 SQL例 SELECT * FROM " webperf_by_codebuild_33916b229c860831 " . " lighthouse " limit 10 ; カスタマイズ Lighthouseの設定 Lighthouseには実行時に独自のヘッダを設定することが可能です。 https://github.com/GoogleChrome/lighthouse#cli-options またネットワーク速度をエミュレートすることなども可能です。 https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md 検証環境などベーシック認証などをかけている環境へも上記のような設定を含めることで計測対象にすることが出来ます。 計測間隔の調整 計測のトリガーはCloudWatch Eventで行っています。そのためCloudWatch Eventの設定を変更することで計測間隔の調整をすることが可能です。 Terraformの該当ファイルは terraform/codebuild_trigger.tf です。schedule_expressionの cron(0 * * * ? *) を変更することで間隔を変更することが可能です。 反映するにはもう一度 make terraform-apply を実行します。 料金について 本記事の設定で実際にかかっている料金をご参考までにお伝えします。 現在4つのURLの計測を1時間毎に実行していますが、AWSの料金は1日$0.6程になります。格安かどうかで言うと活用次第だとは思いますが、サーバの運用や管理もいらないため、とりあえず動かしておくというのもありなのではないでしょうか。 さいごに 去年作ったWEBパフォーマンスの定点観測の仕組みをバージョンアップしてみました。 WEBパフォーマンスの計測はフロントエンド技術の進化や検索エンジンのアルゴリズムの変化に伴ってどんどん変化する分野だと思うので、常に同じ計測方法をし続けるのではなく定期的に見直していかなければいけない分野だと再認識しました。 本記事の内容はSREとしてのお仕事とは少しずれているのでSREのことが気になる方は下記の記事を是非読んでみてください! tech.dely.jp delyではSREを絶賛募集しています!クラシルの開発スピードを落とさず、信頼性も担保するという難易度の高い課題に挑戦したい方はぜひ声をかけてください! www.wantedly.com 明日はdelyコマース事業部エンジニアのjohnさんが投稿します!お楽しみに! delyの開発チームについて詳しく知りたい方はこちらもあわせてどうぞ!
アバター
こんにちは、開発部の高橋です。 本記事は dely Advent Calendar 2019 の14日目の記事です。 昨日はミカサ(acke_red)さんの「デザイン負債を返済する - クラシルのデザインの展望2020」という記事でした。 note.com 目次 目次 はじめに 複数データベースの仕組み 複数データベースに関連するActiveRecordの全体像 1. master/slave構成 利用方法 DatabaseSelectorの利用方法 2. 複数のデータベースの利用 利用方法 アプリケーションでの実際の実装 開発時にハマった箇所 POSTのあとのGETでの更新処理で競合が発生 readingロールに対して更新していることがテストで気付きにくい まとめ 最後に はじめに 10月の半ば辺りにRails6の複数機能を利用し、master/slave構成に対応した新規アプリケーションを本番リリースしました。 Rails5まではこのような対応する場合は他のgemを利用する必要がありましたが、これらのgemはActiveRecordの内部をオーバーライドしていたりするため、Railsのバージョンを上げた際に壊れるみたいなことはあるあるなのではないかと思います。 今回は新規アプリケーションということもあり、またRailsもちょうど6が出たタイミングだったため、gemを使うのではなくRailsの複数データの機能を利用してmaster/slave構成に対応することにしました。 基本的にRailsガイドに大抵の設定・実装項目は書いてあるのでそれを読みつつ実装することで組み込み自体はスムーズに行うことができました。 railsguides.jp ただその一方で、一重にRails6の複数データベースといっても実態としては単にreader/writerへ外にもいくつかの機能が合わさっており、どの機能がどこに作用するかという部分がイマイチ明確ではなく混乱した部分もありました。 今回は自分なりに調べた複数データベースの仕組みや、導入した際にハマった部分を知見として共有できればと思っています。 複数データベースの仕組み 複数データベースに関連するActiveRecordの全体像 複数データベースを理解するにあたり、コネクション周りの全体像がいまいちよくわからなかったので全体図を作ってみました。 複数データベースの機能がActiveRecordのどの辺りに作用しているかという観点でまとめています。 紐付けは各要素同士の参照を表しています。 注: Rails 6.0.1時点でのものです。 上記画像をもとにRails6の複数データベースの機能を大別すると以下のようになります。 master/slave構成 コネクション自動振り分け機能(DatabaseSelector) 複数のデータベース利用 なおRails6.0ではシャーディングの機能はなく、シャーディングをしたい場合は依然として octopus のようなgemを利用するなど別途対応する必要があります。 今後機能入れる予定ではあるらしく、シャーディングを入れる準備段階の実装のPRなども上がっているようでした。 github.com 1. master/slave構成 上記の画像の①の部分を振り分ける機能に当たります。 writing/readingというロールに対して、 ActiveRecord::ConnectionAdapters::ConnectionHandler のインスタンスがそれぞれに作成されます。 またそれぞれの connection_handler の間に prevent_writes という参照がありますが、これはRDBへの書き込みをRails側で抑制する機能です。 実行スレッドに対して値が設定されます。 rails/connection_pool.rb at v6.0.1 · rails/rails · GitHub つまり、状態としては以下の4通りがありえることになります。 向き先がWriter・書き込み可能 向き先がWriter・書き込み不可 向き先がReader・書き込み可能 向き先がReader・書き込み不可 通常は1と4の状態が利用され、2は書き込み直後の読み取り時などに利用されることになります。 3はその状態にはできますが、意味はありません。 利用方法 利用方法は以下のように database.yml の各envの直下にwriter/reader名を記述し、 ApplicationRecord にて connects_to メソッドでreading/writingのロールをそれぞれのDBにマッピングして使います。 production : primary : <<: *default host : <%= ENV["DB_HOST"] %> primary_replica : <<: *default host : <%= ENV["DB_REPLICA_HOST"] %> replica : true replica: true にすると、ActiveRecordのConnectionAdapterに情報が渡され、そのコネクションを経由した書き込みクエリは実行できないようになります。 例えばMySQLでは BEGIN,COMMIT,EXPLAIN,SELECT,SET,SHOW,RELEASE,SAVEPOINT,ROLLBACK,DESCRIBE,DESC,WITH以外が書き込みクエリに該当します。 rails/database_statements.rb at v6.0.1 · rails/rails · GitHub モデルでの定義は以下のように抽象クラスに定義します。 class ApplicationRecord < ActiveRecord :: Base self .abstract_class = true connects_to database : { writing : :primary , reading : :primary_replica } end これで ApplicationRecord のロード時に ActiveRecord::Base.connection_handlers にwriting/readingのconnection_handlerが作成されます。 # - config.eager_load = false # - bin/rails console実行直後(pry) [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connection_handlers.transform_values(& :class ) => { :writing => ActiveRecord :: ConnectionAdapters :: ConnectionHandler } [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ApplicationRecord .connection_handlers.transform_values(& :class ) => { :writing => ActiveRecord :: ConnectionAdapters :: ConnectionHandler , :reading => ActiveRecord :: ConnectionAdapters :: ConnectionHandler } [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connection_handlers.transform_values(& :class ) => { :writing => ActiveRecord :: ConnectionAdapters :: ConnectionHandler , :reading => ActiveRecord :: ConnectionAdapters :: ConnectionHandler } 呼び出し方は以下のようになります。 ActiveRecord :: Base .connected_to( role : :reading ) do # 読み取り処理 end ActiveRecord :: Base .connected_to( role : :writing ) do # 書き込み処理 end 接続しているコネクションもreadingロールとwritingロールで異なります # default_connection_handlerはwritingロール [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connected_to( role : :reading ) { ActiveRecord :: Base .connection_pool.equal? ActiveRecord :: Base .default_connection_handler.retrieve_connection_pool( ' primary ' ) } => false DatabaseSelectorの利用方法 上記で呼び出し処理を書きましたが、これを逐一実装の中で手書きするのは骨が折れますし、ヒューマンエラーも起きがちになりそうです。 そこでRailsはRackミドルウェアとして DatabaseSelector という仕組みを用意してくれています。 これは以下のような特性を持ちます。 HTTPリクエストがGET/HEADの場合はreadingロールを使う GET/HEAD以外の場合はwritingロールを使う writingへ向いてから一定時間内(デフォルト2秒)のリクエストに対しては、writingロールを使う この間、書き込みは不可(prevent_writes == true) この際、リクエスト元の判別はデフォルトでsession_store(cookie)を利用する writingロールへの処理の最後に、 session[:last_write] に現在時刻のタイムスタンプを挿入します 利用方法は以下のように config/application.rb などに設定します。 config.active_record.database_selector = { delay : 2 .seconds } config.active_record.database_resolver = ActiveRecord :: Middleware :: DatabaseSelector :: Resolver config.active_record.database_resolver_context = ActiveRecord :: Middleware :: DatabaseSelector :: Resolver :: Session 自動切り替えの仕組みやリクエスト判別の仕組みは自前で実装することも可能で、その際は上に設定する自作クラスに変更すればOKです。 2. 複数のデータベースの利用 前掲の画像の②の部分の機能に当たります。 こちらはmaster/slave切り替え機能とは異なり、別のデータベースを利用するための仕組みとなります。 例えば、 foo_production というメインのDBと bar_production という別のDBを併用することができます。 内部的にはConnectionHandlerの先のConnectionPoolを切り替える仕組みになっています。 同一スレッド内で prevent_writes をtrueにした場合、primaryとAnimalBaseの両方に影響があります。 [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connected_to( role : :writing , prevent_writes : true ) do [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> Foo .first [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> Animal .create! [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> end Foo Load ( 2 .0ms) SELECT ` foos ` .* FROM ` foos ` ORDER BY ` foos ` . ` id ` ASC LIMIT 1 ActiveRecord :: ReadOnlyError : Write query attempted while in readonly mode : INSERT INTO ` animals ` ( ` created_at ` , ` updated_at ` ) VALUES ( ' 2019-12-11 12:31:37.696328 ' , ' 2019-12-11 12:31:37.696328 ' ) 利用方法 こちらも database.yml にDB名を設定し、モデルでマッピングします。(Railsガイドと合わせて名前はanimalsとします) production : animals : <<: *default host : <%= ENV["ANIMAL_DB_HOST"] %> migrations_paths : db/animals_migrate animals : <<: *default host : <%= ENV["ANIMAL_DB_REPLICA_HOST"] %> replica : true migrations_paths でmigrationファイルの置き場を分けることができます。 class AnimalBase < ApplicationRecord self .abstract_class = true connects_to database : { writing : :animals , reading : :animals_replica } end class Animal < AnimalBase end このように実装すると、紐づく ConnectionPool が異なるようになります。 [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> Foo .connection_specification_name => " primary " [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> Animal .connection_specification_name => " AnimalBase " [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ApplicationRecord .connection_pool.equal? Foo .connection_pool => true [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ApplicationRecord .connection_pool.equal? Animal .connection_pool => false またdatabase.ymlに追加するとdbコマンドにもanimals用のものが追加されます。 bin/rails -T | grep db: rails db:create # Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases rails db:create:animals # Create animals database for current environment rails db:create:primary # Create primary database for current environment rails db:drop # Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases rails db:drop:animals # Drop animals database for current environment rails db:drop:primary # Drop primary database for current environment rails db:environment:set # Set the environment value for the database rails db:fixtures:load # Loads fixtures into the current environment's database rails db:migrate # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog) rails db:migrate:animals # Migrate animals database for current environment rails db:migrate:primary # Migrate primary database for current environment rails db:migrate:status # Display status of migrations rails db:migrate:status:animals # Display status of migrations for animals database rails db:migrate:status:primary # Display status of migrations for primary database rails db:prepare # Runs setup if database does not exist, or runs migrations if it does rails db:rollback # Rolls the schema back to the previous version (specify steps w/ STEP=n) rails db:schema:cache:clear # Clears a db/schema_cache.yml file rails db:schema:cache:dump # Creates a db/schema_cache.yml file rails db:schema:dump # Creates a db/schema.rb file that is portable against any DB supported by Active Record rails db:schema:load # Loads a schema.rb file into the database rails db:seed # Loads the seed data from db/seeds.rb rails db:seed:replant # Truncates tables of each database for current environment and loads the seeds rails db:setup # Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first) rails db:structure:dump # Dumps the database structure to db/structure.sql rails db:structure:load # Recreates the databases from the structure.sql file rails db:version # Retrieves the current schema version number 利用する際は通常は connects_to が設定されてあるモデル(上記の場合はAnimalクラス)をいつもどおり使います。 [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> Animal .create ( 0 .8ms) BEGIN Animal Create ( 1 .9ms) INSERT INTO ` animals ` ( ` created_at ` , ` updated_at ` ) VALUES ( ' 2019-12-11 13:20:03.478803 ' , ' 2019-12-11 13:20:03.478803 ' ) ( 4 .0ms) COMMIT => #<Animal:0x00007ff4b817f890 id: 1, created_at: Wed, 11 Dec 2019 13:20:03 JST +09:00, updated_at: Wed, 11 Dec 2019 13:20:03 JST +09:00> [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> Animal .first Animal Load ( 2 .9ms) SELECT ` animals ` .* FROM ` animals ` ORDER BY ` animals ` . ` id ` ASC LIMIT 1 => #<Animal:0x00007ff4b72f5b58 id: 1, created_at: Wed, 11 Dec 2019 13:20:03 JST +09:00, updated_at: Wed, 11 Dec 2019 13:20:03 JST +09:00> root@localhost (13:20:44) [animal_development]> select * from animals; +----+----------------------------+----------------------------+ | id | created_at | updated_at | +----+----------------------------+----------------------------+ | 1 | 2019-12-11 13:20:03.478803 | 2019-12-11 13:20:03.478803 | +----+----------------------------+----------------------------+ 1 row in set (0.00 sec) また一応 conncted_to メソッドの引数としてdatabase引数を渡せるため、それ経由で ActiveRecord::Base 経由からもアクセスできます。(後述しますが非推奨です) [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connection.execute( ' select * from animals ' ) ( 2 .3ms) select * from animals ActiveRecord :: StatementInvalid : Mysql2 :: Error : Table ' foo_development.animals ' doesn ' t exist [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connected_to( database : :animals ) do [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connection.execute( ' select * from animals ' ).to_a [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> end ( 4 .9ms) SET NAMES utf8mb4 COLLATE utf8mb4_general_ci, @@SESSION .sql_mode = CONCAT(CONCAT( @@sql_mode , ' ,STRICT_ALL_TABLES ' ), ' ,NO_AUTO_VALUE_ON_ZERO ' ), @@SESSION .sql_auto_is_null = 0 , @@SESSION .wait_timeout = 2147483 ( 1 .8ms) select * from animals => [[ 1 , 2019 - 12 - 11 13 : 20 : 03 +0900, 2019 - 12 - 11 13 : 20 : 03 +0900]] ただし、このアクセス方法にはいくつか問題があります。 一つは、内部で establish_connection が呼ばれて ConnectionPool の再生成処理が走ることです。これによりパフォーマンス劣化などの問題が懸念されます。 また、上記のブロックを抜けても自動でprimaryに接続が戻らないという問題もあります。 [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connected_to( database : :animals ) do [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connection.execute( ' select * from animals ' ).to_a [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> end ( 4 .9ms) SET NAMES utf8mb4 COLLATE utf8mb4_general_ci, @@SESSION .sql_mode = CONCAT(CONCAT( @@sql_mode , ' ,STRICT_ALL_TABLES ' ), ' ,NO_AUTO_VALUE_ON_ZERO ' ), @@SESSION .sql_auto_is_null = 0 , @@SESSION .wait_timeout = 2147483 ( 1 .8ms) select * from animals => [[ 1 , 2019 - 12 - 11 13 : 20 : 03 +0900, 2019 - 12 - 11 13 : 20 : 03 +0900]] [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> Foo .first Foo Load ( 2 .5ms) SELECT ` foos ` .* FROM ` foos ` ORDER BY ` foos ` . ` id ` ASC LIMIT 1 ActiveRecord :: StatementInvalid : Mysql2 :: Error : Table ' animal_development.foos ' doesn ' t exist そのため、自分でprimaryへ戻る処理を書く必要があります。 [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connected_to( database : :animals ) do [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .connection.execute( ' select * from animals ' ).to_a [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ensure [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> config_hash = ActiveRecord :: Base .resolve_config_for_connection( :primary ) [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> ActiveRecord :: Base .establish_connection(config_hash) [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> end ( 1 .9ms) SET NAMES utf8mb4 COLLATE utf8mb4_general_ci, @@SESSION .sql_mode = CONCAT(CONCAT( @@sql_mode , ' ,STRICT_ALL_TABLES ' ), ' ,NO_AUTO_VALUE_ON_ZERO ' ), @@SESSION .sql_auto_is_null = 0 , @@SESSION .wait_timeout = 2147483 ( 1 .4ms) select * from animals => [[ 1 , 2019 - 12 - 11 13 : 20 : 03 +0900, 2019 - 12 - 11 13 : 20 : 03 +0900]] [ Ruby : 2.6 . 5 ][ Rails : 6.0 . 1 ] pry(main)> Foo .first Foo Load ( 1 .5ms) SELECT ` foos ` .* FROM ` foos ` ORDER BY ` foos ` . ` id ` ASC LIMIT 1 => #<Foo:0x00007ff4c8476d68 id: 1, created_at: Wed, 11 Dec 2019 13:35:49 JST +09:00, updated_at: Wed, 11 Dec 2019 13:35:49 JST +09:00> 一応できるってだけで、基本的には事前に connectes_to で設定していたモデルから参照するのが良さそうという所感です。 アプリケーションでの実際の実装 今回自分が担当した新規アプリケーションではmaster/slave機能のみを利用し、また DatabaseSelector による自動振り分け機能も利用しています。 基本的に DatabaseSelector に乗っかる形で問題なく稼働できていますが、 GETリクエストで作成・更新したいケース 更新処理はないが、POSTリクエストで大量にリクエストをさばきたいケース という2つの例外ケースがアプリケーションの要件上一部存在してしまっています。 これらを解決するために、現状は以下のようなようなメソッドをコントローラに追加しています。 def with_reader (&block) ActiveRecord :: Base .connected_to( role : :reading , &block) end def with_writer (&block) ActiveRecord :: Base .connected_to( role : :writing , &block) end これらを around_action などを利用して必要な箇所で呼び出すことによって DatabaseSelector でまかない切れないケースに対応しています。 開発時にハマった箇所 POSTのあとのGETでの更新処理で競合が発生 アプリケーションの仕様として、POSTリクエストが走ったあとで、GETリクエストでデータベースに更新がかかるケースがあったのですが、その際に状況によってエラーが出たり出なかったりするという現象が起きていました。 これは DatabaseSelector でPOSTリクエストのあとに2秒の間、Rackミドルウェア上で prevent_writes = true がセットされており、またRailsアプリケーション側でそれをfalseにする処理を挟んでいなかったためにエラーが出たり出なかったりしていました。 つまり、POSTのあと2秒未満のGET更新の場合はエラーが発生し、2秒以上経過した後にGETリクエスト経由での更新処理の場合にエラーはでず、時間要因で結果が変わっているという状況でした。 これに関してはissueにて議論がなされていました。 github.com またその結果としてv6.0.1では connected_to メソッドの引数に prevent_writes が追加されています Call `while_preventing_writes` from `connected_to` by eileencodes · Pull Request #37065 · rails/rails · GitHub ただし、今回の場合はバージョンアップまだ行えてなかったため、コントローラーの処理でwriterに向ける際には以下のように prevent_writes にfalseを入れる処理を追加しました。 def with_writer (&block) ActiveRecord :: Base .connected_to( role : :writing ) do ActiveRecord :: Base .connection_handler.while_preventing_writes( false , &block) end end Rails6.0.1では以下のように書けます。( prevent_writes オプションがデフォルトでfalseなので) def with_writer (&block) ActiveRecord :: Base .connected_to( role : :writing , &block) end readingロールに対して更新していることがテストで気付きにくい 以下のissueでも議論されていました。 github.com Railsには use_transactional_tests というオプションがあり、これを true にしているとDBへの更新処理は COMMIT されず各example後に ROLLBACK されます。 これによってテスト後に毎回DBを TRUNCATE する必要がなく、テストのパフォーマンスも向上するため、できるだけ true にしたまま開発したいという気持ちがあります。 このオプションをONにすると内部的にはreadingロールのコネクションプールがwritingロールのコネクションプールにすり替わるようになります。 これにより COMMIT せずともwritingロールで更新を行ったデータをreadingロールでも読み取ることができるようになります。 rails/test_fixtures.rb at v6.0.1 · rails/rails · GitHub その一方で、コネクションへ replica フラグが渡されなくなるため、テスト中にreadingロールへの更新処理を行っていても ActiveRecord::ReadOnlyError が発生しなくなります。 そのため、readingロールへ更新処理を行っていることが自動テストでは検知できませんでした。 今回の自分のプロジェクトでは規模的には小さかったのもあり、実機検証するタイミングで検知するという方針をとって開発を進めました。 一応 use_transactional_tests を必要なテストに挿入して対応することもできそうではありますが、それがどこに必要なのかを判断する基準は各開発者の意識に依存するため実運用は難しそうな印象です... まとめ 詰まった箇所なども書きましたが、自分の担当していたアプリケーションでは概ね良好につかえていた印象です。 これから複数データベースを使おうと思ってる方の参考になれば幸いです。 最後に 明日は開発部SREの井上さんの記事です!お楽しみに。 qiita.com adventar.org また、delyではRailsエンジニアを絶賛募集中なので興味のある方は是非是非。 speakerdeck.com www.wantedly.com CXOとVPoEへのインタビュー記事もあります。 wevox.io
アバター
はじめまして。 sakura818uuuです。 この前、社内で初めてPMっぽい動きをしたら盛大に失敗したので 反省すべきことや学んだことを書きます。 この記事はdely Advent Calendar 2019の12日目の記事です。 Qiita: https://qiita.com/advent-calendar/2019/dely Adventar: https://adventar.org/calendars/4134 昨日はiOSエンジニアのnancyさんが「 Combine と RxSwift を比較してみた - dely tech blog 」を書いてくれました! では本題に戻ります。 はじめに 料理動画アプリ『クラシル』のある機能Aを改善したいと思っていました。 そして、初めてPMっぽい動きをしたら盛大に失敗したので 反省すべきことや学んだことを書きます。 今回はあるMTGの一幕をお届けします。 ※本内容は社内限定で公開していたものを一部改変してお届けしています。 反省すべきとこ 【その1】 会議のゴールを決めきれていなかった 会議が最終的にどこまでいきたいかを詰めきれていなくて議論が右往左往してしまいました。 <なぜ起こったか> なぜ起こったかは2つの理由があると思います。 一つは 想定していたゴールとは別の方向に議論の流れがいってしまったこと です。 会議を行う前、以下の2つをゴール(必ずマストで決めたい)として考えていました。 ①機能Aを改善する必要性の認識合わせをし、機能Aを改善する方向にもっていく ②改善に伴う開発スタイルを決定する ①は認識合わせもある程度うまくいきました。しかし、②の議論は想定していた流れとは違う方向になりました。 これは②を決める前に議論すべきことがあったからなのですが、そっちの正しい方向に参加メンバーがひっぱってくれて感謝してます。ありがとうございます。 もう一つの理由は PMをびびっていたこと です。 はじめてPMっぽい動きをするにあたりステークホルダーにずっと悩んでいました。 主催者側としては②を早く決めたかった理由が 「長期化する開発スタイルになる場合、 ステークホルダーも多くなるのは確実 。そうしたら情報の認知・共有も段違いにむずかしくなる し、それに加えてそれだけの人数をまとめてひっぱっていくことが自分にはできるのか なるべくステークホルダーを最小限にとどめてごりごり改善していってまずはすすめていこう」 みたいな個人の理由が大きいです。 これはめちゃくちゃ反省していて、よくなかったなと思います。すいません。 ステークホルダーが多くなろうが機能Aをよくすること、それに集中することだけを考えればよかったです 自分のPMスキルを理由にしてこういう雑念を考えるのはよくないな、と学びました <次はどうすれば解決する?> ・ゴールを機能Aをよくすることに集中する ・ステークホルダーの人数は考えない ・MTGのゴールのスコープを短くする ゴールが思いつかない場合はメンバーに相談するのもいいかもしれない (最後のやつは「解のない会議のゴールってどうやったら決めたらいいんですかね?」とXXXさんに聞いて教えてもらった手法の一つです) 【その2】 兼務しすぎて一杯一杯 会議のファシリテーター・書記・議事録を一人で兼務したらすごく一杯一杯になりました。 <なぜ起こったか> 偶発的に起こってしまった、ではなく会議前から 意図的に兼務しようと思っていました 。なぜなら、自分が言い出しっぺで施策をはじめたわけで、他の人に会議を参加してもらって、その上なにかをお願いするのは図々しいと思ったからです。 <次はどうすれば解決する?> 議事録は 会議の音声を録音する ことで解決しそうです。 もしかしたら 書記は誰かに任せてもよいのかも しれません。次やるときは誰かにお願いしてみようかと思います。 よかったこと 【その1】 余裕をもってスケジュールをセッティングできた 参加者全員のスケジュールを確認して、余裕をもってMTGをセッティングできたことはよかったです。 6/19 XX終了後、参加者にオフラインで機能Aの改善MTGを開催したい旨を伝える 6/19 slackでメンションをつけて通知 6/19 slackで関係者にメンションを付けてアジェンダを共有している図 7/1 前日にメンションをつけてリマインド 7/2 MTG開催 【その2】 事前にデータを用意できた データがないことで議論が止まる/曖昧になることを避けたかったので事前に調査してQiitaを書きました。自分がデータサイエンスチームに所属していて出来ることの一つでもあったので、それを生かせたのはよかったかなと思います。 まとめ 反省から学んだ、次PMになったときMTGで気をつけること3つ - MTGのゴールのスコープを短くしてでもよいので適切なゴールを決めること - 議事録は音声をとること - ファシリテーターと書記の兼務は難しいので、書記はどなたかに協力してもらう 明日はデザイナーのredさんが「デザイン負債を返済する - クラシルのデザインの展望2020」というタイトルでアップ予定です! 最後に告知です、delyではエンジニアを絶賛募集中です🙌 ぜひお気軽にご連絡ください。 https://www.wantedly.com/projects/329047
アバター
  こんにちは! dely 株式会社で iOS を担当している nancy こと仲西です。   本記事は dely Advent Calendar の11日目の記事です。   qiita.com adventar.org   昨日は小林さんが「UI デザイン × PdM で広がるデザインの可能性」というテーマで書いています。 https://dely.design/n/nfbad0dcdec77 dely.design UI デザイナーが PdM をやると何がいいのか、 どんな点を心がけるべきなのかといったことがまとまっているので、気になる方はぜひご覧ください! ちなみに、記事内でチラッと私も登場しています(笑)     はじめに  本記事では iOS 13以降で使用できる Apple 純正のフレームワークである Combine と、 Combine と似た機能を有する OSS フレームワークである RxSwift を比較してみたいと思います。   現状だと多くの方が RxSwift の方を使用しているかと思います。 いざ Combine に移行する際にどう書き換えたらいいのかなどの点で参考になれば幸いです。 Combine とは developer.apple.com/wwdc19/722 Combine とは、Apple が WWDC 2019 で発表された、 UI イベントやネットワーク通信の非同期イベントなどを処理するためのフレームワークです。 以下の動画で概要が、 developer.apple.com 以下の動画ではコーディング例が紹介されています。 developer.apple.com   iOS 13から導入されたフレームワークであるため、iOS 12以下では使用できないのでご注意ください。     RxSwift とは RxSwift とは、 Rx という Reactive Programming ができるライブラリの Swift 版で、 こちらも主に UI イベントや非同期のイベントを受け取る際などに使用されています。 元々は .NET 用のフレームワークだったようですが、今では Swift だけでなく Java や JavaScript などにも移植されているオープンソースのフレームワークです。 比較してみる それでは実際に Combine、RxSwift 両方のコードを書いてみて比較してみたいと思います。 例として、Qiita の記事取得 API( https://qiita.com/api/v2/items )を使用し、記事のタイトル、公開日を取得してみたいと思います。 以下のような struct を定義し、JSON からデコードします。 struct Article : Codable { let title : String let url : String } Publisher と Observable Publisher、Observable を用いて API 通信完了後に取得結果を表示するまでの処理を比較してみます。 Combine: Publisher   var cancellables = [AnyCancellable]() func fetchArticles () -> AnyPublisher <[ Article ], Error > { let url = URL(string : "https://qiita.com/api/v2/items" ) ! let request = URLRequest(url : url ) return URLSession.shared .dataTaskPublisher( for : request ) .map({ $0 .data }) .decode(type : [ Article ] . self , decoder : JSONDecoder ()) .eraseToAnyPublisher() } fetchArticles() .receive(on : DispatchQueue.main ) .sink(receiveCompletion : { completion in switch completion { case .finished : print ( "finished" ) case .failure( let error ) : print (error.localizedDescription) } }, receiveValue : { response in print(response) }).store( in : & cancellables) Publisher を購読するために sink というメソッドを使用しています。 sink メソッドでは、イベントの購読が完了した際に実行される処理、値が通知された際に実行される処理をクロージャとして渡すことができます。 イベントの購読完了には、正常に終了したのか(.finished)、エラーで終了したのか(.failure)の2パターンあります。 RxSwift: Observable var disposeBag = DisposeBag() func fetchArticles () -> Observable <[ Article ]> { let url = URL(string : "https://qiita.com/api/v2/items" ) ! let request = URLRequest(url : url ) return Observable < [Article] > .create({ observable in let task = URLSession.shared.dataTask(with : request ) { data, response, error in if let error = error { observable.onError(error) } do { if let data = data { let articles = try JSONDecoder().decode([Article]. self , from : data ) observable.onNext(articles) } } catch ( let e ) { observable.onError(e) } } task.resume() return Disposables.create() }) } fetchArticles() .subscribe(onNext : { response in print(response.first) }, onError : { error in print(error.localizedDescription) }, onCompleted : { print( "Completed" ) }, onDisposed : { print( "Disposed" ) }).disposed(by : disposeBag )   この辺りは RxSwift を触ったことがある方であれば比較的簡単に理解できそうですね。 RxSwift で言う Dispose は Combine の Cancellable にあたるようです。 var cancellables = [AnyCancellable]() を定義しておき、 sink した際に .store(in: &cancellables) のようにしておくと、 cancellables が解放されたタイミングで sink で購読してた処理もキャンセルされるようです。 Future と Single 上記の比較では Observable を用いて Combine と比較しましたが、 Single を Combine で実装するとどうなるのかという比較も行いたいと思います。 Combine: Future Future は 値を1度通知する エラーを通知 のどちらかを行うことができます。 RxSwift の Single と同じように API 通信などの1度限りの処理などに使用できそうです。 func fetchArticles () -> Future <[ Article ], Error > { let url = URL(string : "https://qiita.com/api/v2/items" ) ! let request = URLRequest(url : url ) return Future < [Article], Error > { promise in URLSession.shared .dataTaskPublisher( for : request ) .map({ $0 .data }) .sink(receiveCompletion : { _ in }, receiveValue : { responseData in do { let articles = try JSONDecoder().decode([Article]. self , from : responseData ) promise(.success(articles)) } catch ( let e ) { promise(.failure(e)) } }).store( in : & cancellables) } } fetchArticles() .receive(on : DispatchQueue.main ) .sink(receiveCompletion : { completion in switch completion { case .finished : print ( "finished" ) case .failure( let error ) : print (error.localizedDescription) } }, receiveValue : { response in print(response) }).store( in : & cancellables) RxSwift: Single func fetchArticlesSingle () -> Single <[ Article ]> { let url = URL(string : "https://qiita.com/api/v2/items" ) ! let request = URLRequest(url : url ) return Single < [Article] > .create(subscribe : { single in let task = URLSession.shared.dataTask(with : request ) { data, response, error in if let error = error { single(.error(error)) } do { if let data = data { let articles = try JSONDecoder().decode([Article]. self , from : data ) single(.success(articles)) } } catch ( let e ) { single(.error(e)) } } task.resume() return Disposables.create() }) } fetchArticlesSingle().subscribe(onSuccess : { response in print(response) }, onError : { error in print(error.localizedDescription) }) 上記のようにすると RxSwift で言う Single は Combine の Future を使用すると置き換えられそうです ただ、 こちら の Single と Future の項目で述べられているように、少し動作が異なるようです。 They're only similar in the sense of single emission, but Future shares resources and executes immediately (very strange behavior) Single と同じようにイベントを1度通知して Finish する動作は同じですが、 Single では初めて Subscribe されたタイミングで処理が実行されるのに対し、 Future ではインスタンスが生成されたタイミングに処理が実行されるようです。 そのため、素直に書き換えるだけでは期待する動作にならない可能性があるため注意が必要です。   終わりに   本記事では Combine と RxSwift の書き方を比較してみました。 これから Combine を使用してみたいと思われている方にとって少しでも参考になれば幸いです。   また、明日は sakura818uuu さんが「初めて PM っぽいことをやって失敗した件」というタイトルで投稿します!   dely について   dely では一緒に働いていただけるエンジニアを募集しています!   dely の開発チームについて詳しく知りたい方はこちらをご覧ください! speakerdeck.com   CXOとVPoEへのインタビュー記事はこちら! wevox.io   参考 RxSwift to Apple's Combine "Cheat Sheet" Apple Documentation: Combine  
アバター
はじめに こんにちは、フロントエンドエンジニアの all-user です! これは delyアドベントカレンダー 9日目の記事です。 昨日はプロダクトデザイナーのkassyさんプレゼンツ「デザインとエンジニアリングをつなぐために重要な3つのこと」でした。 dely.design 開発現場でも直面することの多いコミュニケーションの問題と、それに対して心掛けていることについて書かれていて、うんうんとうなずきながら読んでしまいました。ぜひこちらもご覧ください! それでは、TypeScriptを使ったクラシルのフロントエンド開発の中で、思わずへ〜となったトリビアたちを紹介したいと思います。 目次 はじめに 目次 1. 循環依存のエラーを回避する方法 どうしてエラーになってしまうのか? エラーを回避する方法 どうしてエラーにならないのか? 全てのエラーを回避できるわけではない Parcel, Rollup, .mjsでも有効 vuex-smart-moduleのModuleをinternal.tsでまとめる これTypeScriptのトリビア? 2. 自身を循環参照する型の書き方 自身を参照する型をtypeで定義するとエラーになる(TS3.6まで) interfaceだとエラーにならない どうしてエラーにならないのか? 3. document.querySelectorは引数の型から返り値のElement型を判定してくれる どうやって判定しているのか? 4. 存在しないプロパティの存在チェックをする方法 5. String EnumsとString Literal Union Typesの使い分け String Enumsに適したケース 6. switch文を型安全に書く方法 7. 高階関数の型から返り値の型を取り出す方法 8. ネストした配列の中身の型を取り出す方法 9. プロパティを持つ関数オブジェクトの型の書き方 複数の型を組み合わせて表現する Callableで表現する 10. オーバーロードの型のみを定義する方法 さいごに 1. 循環依存のエラーを回避する方法 ES ModulesやCommon JSにおいて、あるファイルから別のファイル、そのファイルから更に別のファイルへと依存を辿っていった際、その依存グラフの中に再度自身のファイルが登場してしまうような依存関係を循環依存と呼びます。 特定のケースではこの循環依存が原因で実行時エラーが発生してしまう場合があります。 また、循環依存はTypeScriptの型の上では問題にならないため、コンパイルが通ってしまう点にも注意が必要です。 . ├── a.ts ├── b.ts └── index.ts // a.ts import { b } from './b' ; class A { getB () { return b ; } } export const a = new A (); // b.ts import { a } from './a' ; type A = typeof a ; class B { a: A ; constructor( a: A ) { this .a = a ; } } export const b = new B ( a ); // index.ts import { a } from './a' ; console.log ( a ); console.log ( a.getB ()); console.log ( a === a.getB () .a ); console.log ( a.getB () === a.getB () .a.getB ()); このファイルをwebpackでバンドルして実行すると以下のようなエラーが発生します。 Uncaught TypeError: Cannot read property 'getB' of undefined どうしてエラーになってしまうのか? index.ts が a をimportします a は getB というメソッドの中で b を参照するため、 b をimportします b はコンストラクタの引数に a を受け取るため、 a をimportします この時の a.ts モジュールは初期化が完了していないため空のオブジェクトになります a.ts が空のオブジェクトのためnamed exportされた a は undefined になります a は undefined のため new B(a) は new B(undefined) となります a.getB().a は undefined になります a.getB().a.getB() は undefined に対する存在しないメソッド呼び出しになるためエラーが発生します エラーを回避する方法 internal.ts というファイルを作り、依存するファイルのexportを全て一つにまとめます。 . ├── a.ts ├── b.ts ├── index.ts └── internal.ts // internal.ts export * from './a' ; export * from './b' ; a.ts , b.ts をそれぞれ直接importするのではなく、 internal.ts からimportするように変更します。 // a.ts import { b } from './internal' ; class A { getB () { return b ; } } export const a = new A (); // b.ts import { a } from './internal' ; type A = typeof a ; class B { a: A ; constructor( a: A ) { this .a = a ; } } export const b = new B ( a ); // index.ts import { a } from './internal' ; console.log ( a ); console.log ( a.getB ()); console.log ( a === a.getB () .a ); console.log ( a.getB () === a.getB () .a.getB ()); webpackでビルドして実行してみると今度はエラーが起きません。 どうしてエラーにならないのか? このトリビアは以下の記事で紹介されていました。 medium.com internal.ts を経由することで、 b.ts が a をimportするタイミングで internal.ts の a が事前に初期化されているようにうまいこと調整されています。 index.ts が a を internal.ts からimportします internal.ts が a.ts をexportします a は getB というメソッドの中で b を参照するため、 b を internal.ts からimportします この時の internal.ts は初期化が完了していないため空のオブジェクトになります internal.ts が空のオブジェクトのためnamed exportされた b は undefined になります b は getB が呼ばれるまで遅延評価されるため、この時点での b の undefined は評価されません internal.ts が b.ts をexportします b はコンストラクタの引数に a を受け取るため、 a をimportします この時の internal.ts モジュールには、すでにexportされた a が存在します a を参照することができるため、 new B(a) は成功します a.getB().a は a と同一になります a.getB().a.getB() は b を返します 全てのエラーを回避できるわけではない 上記の仕組みを考えてみると、 internal.ts が空のオブジェクトであることが許容できないケースではエラーを回避できないことが分かります。 index.ts と internal.ts を以下のように書き換え、 new B(a) されるタイミングで internal.ts に a が初期化されていない状況を意図的に作ると、やはりエラーが発生します。 // index.ts import { b } from './internal' ; console.log ( b ); // B {a: undefined} console.log ( b.a ); // undefined console.log ( b.a.getB ()); // Uncaught TypeError: Cannot read property 'getB' of undefined // internal.ts export * from './b' ; // bを先に読み込む export * from './a' ; // b.ts import { a } from './internal' ; type A = typeof a ; class B { a: A ; constructor( a: A ) { this .a = a ; } } export const b = new B ( a ); // internal.ts が空オブジェクトのため a が undefined になる Parcel, Rollup, .mjsでも有効 全ての方法で循環依存のエラーを回避できました🙌 Parcel, Rollupでビルドした場合でも、 .mjs 拡張子のファイルを直接ブラウザで読み込んだ場合でも、webpackと同様に internal.ts を経由することで循環依存を解決することができます。 ひとつだけ異なる点として、 .mjs 拡張子のファイルを直接Chromeで読み込んだ場合、初期化が完了していないexportは undefined になるのではなく、その値を評価した時点でエラーになります。 vuex-smart-moduleのModuleをinternal.tsでまとめる クラシルではTypeScript + Vue + Vuexを使っていて、当初Vuexの型は自前で書いていましたが、現在はvuex-smart-moduleというライブラリを導入し、快適に型の恩恵を受けられるようになりました。 github.com 通常VuexではActionのtypeを文字列ベースで指定してdispatchするため、Vuex Module同士の静的な依存関係は生まれません。(だから型付けも難しいのですが) vuex-smart-moduleでは、state、getters、dispatch、commitなどを参照する先の(vuex-smart-moduleが提供する)Moduleクラスインスタンスをimportし、そのモジュールのcontextと呼ばれるオブジェクトにStoreの実体をDIすることで、そのModuleのactionsなどを呼び出す仕組みになっています。 型安全にVuexを利用できて、記述もスッキリして良いことばかりなのですが、Module同士の依存関係がimportを通じて行われるために循環依存が発生しやすくなる、という側面があります。(好ましくない依存を見つけやすいというメリットでもあります) このトリビアを使い、Moduleのインポートをすべて internal.ts 経由にすると、循環依存のエラーを回避することができます。 これTypeScriptのトリビア? このトリビアはTypeScriptというよりES Modulesやバンドルツールのトリビアだなと今更ながら気づいてしました。 先行きが不安ですがお付き合いください😂 2. 自身を循環参照する型の書き方 このトリビアはTypeScript 3.7でRecursive Type Aliasesが利用できるようになったおかげで、現在は必要なくなったようです。 TypeScript、本当にすばらしいですね。 www.typescriptlang.org ということで、すでに使い所がなくなったトリビアですが、めげずに紹介していきたいと思います。 自身を参照する型をtypeで定義するとエラーになる(TS3.6まで) type Json = | string | number | boolean | null | { [ property: string ] : Json } | Json [] ; interfaceだとエラーにならない interfaceでは型の解決が遅延されるらしくエラーになりません。 type Json = | string | number | boolean | null | JsonObject | JsonArray ; interface JsonObject { [ property: string ] : Json ; } interface JsonArray extends Array < Json > {} どうしてエラーにならないのか? このトリビアは以下のstack overflowの回答で知りました。 stackoverflow.com また、エラーにならない理由についてのissueコメントのリンクが貼られています。 github.com interfaceではプロパティの型と継承元の型の解決が遅延されるので、このような回避策が実現できるようです。 3. document.querySelectorは引数の型から返り値のElement型を判定してくれる document.querySelector の返り値の型は、引数の型によってどの要素なのかを判定してくれます。 どんな型でも判定できるわけではなく、セレクタが単一の要素名(タグ名)の時のみ有効です。 どうやって判定しているのか? querySelector の型定義を覗いてみると、オーバーロードされた定義がありました。 HTMLElementTagNameMap というinterfaceに全ての要素名と対応する要素の型がマッピングされていて、キーに一致するString Literal型を引数に取ると、返り値の型が確定するという仕組みになっているようです。 もちろん、 document.querySelectorAll にも対応してくれています。 素敵です。 4. 存在しないプロパティの存在チェックをする方法 存在しないプロパティに対し、ドットアクセスや添字でアクセスしようとするとコンパイルエラーになってしまいます。 type SomeObject = | { a: string ; } | { b: number ; } | { c: boolean ; } ; declare const someObj: SomeObject ; if ( someObj.c ) { // エラーになる console.log ( someObj ); } in 演算子を使用するとプロパティの有無をチェックすることができます。 if ( 'c' in someObj ) { console.log ( someObj ); // someObj は { c: boolean }型 } ちゃんと型の絞り込みもできます。 素敵です。 5. String EnumsとString Literal Union Typesの使い分け String EnumsはString型で定義できるEnumsです。 enum EnumSomething { a = 'a' , b = 'b' , c = 'c' , d = 'd' } declare const someVar: EnumSomething ; if ( someVar !== EnumSomething.a ) { // someVar の型は EnumSomething.a | EnumSomething.b | EnumSomething.c に絞り込まれる } switch ( someVar ) { case EnumSomething.a: case EnumSomething.b: case EnumSomething.c: // someVar の型は EnumSomething.b | EnumSomething.c | EnumSomething.d に絞り込まれる break; default : // // someVar の型は EnumSomething.d に絞り込まれる break; } String Enumsは多くの場合、String Literal Union Typesで置き換えが可能です。 type LiteralSomething = 'a' | 'b' | 'c' | 'd' ; declare const someVar: LiteralSomething ; if ( someVar !== 'a' ) { // someVar の型は "b" | "c" | "d" に絞り込まれる } switch ( someVar ) { case 'a' : case 'b' : case 'c' : // someVar の型は "a" | "b" | "c" に絞り込まれる break; default : // someVar の型は "d" に絞り込まれる break; } Enumsはコンパイル後にオブジェクトが生成されますが、String LIteral Union TypesはTSの世界で完結しているため、コンパイル後のコードには残りません。(Enumsもconstを付けるとオブジェクトの生成を抑止できます) 可読性の面でも優れていますし、VS Codeのオートコンプリートも働くので、基本的にはString Literal Union Typesを使うと幸せになれます。 String Enumsに適したケース そんな便利なString Literal Union Typesですが、String Enumsが適しているケースもあります。 先程の例では someVar が LiteralSomething 型なので、オートコンプリートが効き、型の安全性も担保されていましたが、比較対象が string 型の場合、オートコンプリートが効かず、型の安全性も担保されません。 declare const mightA: string ; if ( mightA === EnumSomething.a ) { // Enumsの場合オートコンプリートが効く & EnumSomething型であることが担保される // doSomething(); } if ( mightA === 'a' ) { // String Literalの場合オートコンプリートが効かない & LiteralSomething型であることが担保されない // doSomething(); } タイプミスで aa と打ってしまった場合、Enumsではエラーがでますが、String Literalでは比較対象がstring型のため、コンパイルが通ります。 String Literal Union Typesを期待している比較対象の型がstring型になってしまう場面の一例として、ライブラリの型定義がstring型だけど、利用する側では特定の型に限定したい、というような場面があります。 たとえば、クラシルではvue-routerの route.name の定義にString Enumsを使用していますが、これはvue-routerのライブラリ側が期待する route.name のstring型に対し、型安全に自分たちで定義した型を渡せるようにするためです。 export enum SomethingRoutes { foo = 'foo' , bar = 'bar' , baz = 'baz' } const routes = [ { name: SomethingRoutes.foo , path: '/foo' , component: Foo } , { name: SomethingRoutes.bar , path: '/bar' , component: Bar } , { name: SomethingRoutes.baz , path: '/baz' , component: Baz } ] ; アプリケーション側では route.name に渡す型を、 routes で定義したString LIteral型のみに限定したいわけですが、vue-routerのAPIでは route.name はstringで定義されています。 Enumsを使うことでstring型を期待するAPIに対し、アプリケーション側が期待する型を安全に渡すことができます。 this .$router.push ( { name: SomethingRoutes.foo // name は string型を受け取るが,SomethingRoutes型であることが担保される } ); 6. switch文を型安全に書く方法 switch文を使うと型の絞り込みを行うことができますが、そこに登場するcase節が、取り得る全ての値を抜け漏れなく記述できているかを、型で検査する方法です。 type SomeType = 'a' | 'b' | 'c' | 'd' ; declare const doA: () => void ; declare const doB: () => void ; declare const doC: () => void ; declare const doD: () => void ; const someFunc = ( value: SomeType ) => { switch ( value ) { case 'a' : return doA (); case 'b' : return doB (); case 'c' : return doC (); case 'd' : return doD (); default : { const _: never = value ; console.error ( `${_} is unexpected value` ); } } } ; 取り得る全てのcase節が記述され、適切にbreakやreturnが行われている時、default節ではvalueの型がnever型になります。 このことを利用し、default節でnever型の変数にvalueを代入しておくことで、default節でnever型以外の型が代入される(case節の記述が漏れている)ケースを防ぐことができます。 試しに case 'd': をコメントアウトすると、never型の _ に 'd' 型のvalueを代入しようとしてエラーになります。 7. 高階関数の型から返り値の型を取り出す方法 Vuexのgettersのように、ある値を返す関数、もしくはある値を返す関数を返す関数(高階関数)を受け取るようなAPIは様々なフレームワークでよく見かけるパターンです。 このような関数かどうか分からない、また、いくつ多段にネストしているか分からない型から、最終的な戻り値の型を取り出す方法です。 まずは高階関数もしくはその返り値を表す型を定義します。 type HOFOrValue < T > = ( ...args: any ) => HOFOrValue < T > | T ; 次に高階関数から返り値の型を取り出してみます。 type HOFReturnType < T extends HOFOrValue < any >> = T extends HOFOrValue < infer U > ? U : never ; 型パラメータとして高階関数 T 型を受け取ります( T extends HOFOrValue<any> ) T を HOFOrValue<any> 型に代入可能かを検査します その際に infer U で HOFOrValue の型パラメータ(高階関数の返り値の型)を推論します U を返します const hof: HOFOrValue < string > = () => () => 'a' ; type R = HOFReturnType <typeof hof >; // R は string型 上手くいっているように見えますが、予め型パラメータに与える変数の型を HOF 型に変換しておく必要があります。 const hof2 = () => () => () => () => 2020 ; const hof3: HOFOrValue < number > = hof2 ; type R2 = HOFReturnType <typeof hof2 >; // R2 は () => () => () => number型 type R3 = HOFReturnType <typeof hof3 >; // R3 は number型 クラシルではVuexの型を自前で書いている時によく使いましたが、現在はvuex-smart-moduleへと移行し、使用する機会はほとんどなくなりました。 こういうトリビアを知っておくと、いざというときに型を諦めずに書くことができるので良いです。 8. ネストした配列の中身の型を取り出す方法 高階関数から返り値の型を取り出すのと同じ要領で、ネストした配列からも型を取り出してみたいと思います。 もうお分かりですね、だいぶネタが尽きて来ています。 まずネストした配列もしくはその中身を表す型を定義します。 type NestedArrayOrValue < T > = NestedArrayOrValue < T > [] | T ; 次にネストした配列から、中身の型を取り出してみます。 type NestedArrayType < T extends NestedArrayOrValue < any > > = T extends NestedArrayOrValue < infer U > ? U : never ; 型パラメータとしてネストした配列 T 型を受け取ります( T extends NestedArrayOrValue<any> ) T を NestedArrayOrValue<any> 型に代入可能かを検査します その際に infer U で NestedArrayOrValue の型パラメータ(ネストした配列の中身の型)を推論します U を返します これも事前に型を NestedArrayOrValue 型に変換しておく必要がありますが、ちゃんと中身の型を取り出せます。 const a1: NestedArrayOrValue < string > = [ 'a' , [ 'b' , [ 'c' , 'd' , [ 'e' ]]] , 'f' , 'g' ] ; type A1 = NestedArrayType <typeof a1 >; // A1 は string 複数の型を混ぜることもできます。 const a1: NestedArrayOrValue < string | number | boolean | Promise < string >> = [ 'a' , [ 2020 , [ true , 'd' , [ Promise.resolve ( 'e' ) ]]] , 'f' , 'g' ] ; type A1 = NestedArrayType <typeof a1 >; // A1 は string | number | boolean | Promise<string> あらかじめ NestedArrayOrValue 型で定義した配列を組み合わせて行くこともできます。 const a1: NestedArrayOrValue < string > = [ 'a' ] ; const a2: NestedArrayOrValue < string | number > = [ a1 , 2020 ] ; const a3: NestedArrayOrValue < string | number | boolean | null > = [ a2 , true , a1 , null ] ; const a4 = a3 ; type A4 = NestedArrayType <typeof a4 >; // A4 は string | number | boolean | null 型パラメータを重複して書いている部分を、 NestedArrayType で取り出した型に置き換えてみます。 const a1: NestedArrayOrValue < string > = [ 'a' ] ; const a2: NestedArrayOrValue < NestedArrayType <typeof a1 > | number > = [ a1 , 2020 ] ; const a3: NestedArrayOrValue < NestedArrayType <typeof a2 > | boolean | null > = [ a2 , true , a1 , null ] ; const a4 = a3 ; type A4 = NestedArrayType <typeof a4 >; // A4 は string | number | boolean | null 若干無理やりトリビアをひねり出している感じは否めませんが、次行きます。 9. プロパティを持つ関数オブジェクトの型の書き方 JavaScriptの関数はObjectを継承しているので、関数にもオブジェクトと同じようにプロパティを生やすことが出来ます。 では、このようなプロパティを持つ関数の型を表現するにはどうすれば良いでしょうか? いくつか方法があります。 複数の型を組み合わせて表現する 関数の型とプロパティを持つオブジェクトの型を別々に定義し、それを組み合わせます。 type SomeFunc = ( a: string , b: number ) => void ; type SomeProps = { c: boolean ; d: null ; } ; type SomeFuncWithProps = SomeFunc & SomeProps ; const someFuncWithProps: SomeFuncWithProps = Object .assign ( ( a: string , b: number ) => { console.log ( a , b ); } , { c: true , d: null } ); interfaceを使って定義することもできます。 interface SomeFuncWithProps extends SomeFunc , SomeProps {} ちなみにこの Object.assign で関数にプロパティを生やすトリビアは@uhyo_さんのTweetで知りました。 Object.assignを使うとわりときれいにいく(ぇ https://t.co/fNdviq5j1d https://t.co/sjNzxvtuNz — 🈚️うひょ🤪✒📘 (@uhyo_) 2019年11月20日 Callableで表現する TypeScriptには関数のような呼び出し可能なオブジェクトを、そのものずばり表現するための記法があります。 type Callable = { () : void ; } ; 関数のオブジェクトとしての側面がうかがえる記法ですね。 この記法を使って先程の SomeFuncWithProps を定義してみます。 type SomeFuncWithProps = { ( a: string , b: number ) : void ; c: boolean ; d: null ; } ; 無駄な型定義を作らなくて済むし、スッキリと分かりやすくなりました。 10. オーバーロードの型のみを定義する方法 関数のオーバーロードを利用すると、引数の組み合わせを複数定義できます。 これは関数の実装とセットで書く例です。 function someOverloadFunc ( a: string ) : string ; function someOverloadFunc ( a: number ) : string ; function someOverloadFunc ( a: string | number ) : string { return typeof a === 'string' ? a : a + '' ; } では、この関数の型のみを表現するにはどうすればよいでしょうか? そうです、プロパティを持つ関数オブジェクトの型の書き方で紹介したCallableを使うと、オーバーロード付きの関数の型を定義できます。 type SomeOverloadFunc = { ( a: string ) : string ; ( b: number ) : string ; } ; const someOverloadFunc: SomeOverloadFunc = ( a: string | number ) => typeof a === 'string' ? a : a + '' ; これもVuexに自前で型を書いていた時に、Storeを継承したクラスを用意して、dispatch、commitをラップしたtypedDispatch、typedCommitというメソッドを定義する際に使用していましたが、今はvuex-smart-module(略) さいごに TypeScriptは使うほどに新しい発見があって楽しいですね。 そしてまだまだ進化を続けているので、今後がとても楽しみです。 こうやって振り返ってみると、ここで挙げたトリビアのほとんどはVuexに型を付けるために使っていたんだなと気付きました。 記事中でも紹介させていただいたvuex-smart-moduleは、TypeScript + Vue + Vuexを使っている方にとてもおすすめです。 明日はプロダクトデザイナ × プロダクトマネージャーのこばさん( @kazkobay )が「UIデザイン×PdMで広がるデザインの可能性」というタイトルでアップ予定です! dely.design 最後に告知です、delyではクラシルのフロントエンドを盛り上げてくれる仲間を絶賛募集中です🙌 ぜひお気軽にご連絡ください。
アバター