TECH PLAY

BASE株式会社

BASE株式会社 の技術ブログ

590

こんにちは。BASE BANK 株式会社にて、 Engineering Manager をしている東口( @hgsgtk )です。この度 スクラムフェス三河2021 にて 振り返りを積み上げて自分たちのプラクティスとして昇華•体得していくための仕組みと考え方 というテーマで登壇しました。 スクラムフェス三河とは スクラムフェス三河 はスクラムの学びの場として愛知県の三河で開催されたスクラムに関するカンファレンスイベントです。 スクラムフェス三河はスクラムの学びの場です。 スクラムの初心者からエキスパートまで現場を少しでも良くしようと考えている様々な人々が集まります。 この2日間にわたる魅力的なセッションからはスクラムやアジャイル開発に関する多くのヒントを見つけることができ、現場の力をより一層高めることにつながります。 弊チームでは自分たちのチームが目的・理想とするプロダクト開発の姿を目指すため、スクラムのフレームワーク並びにプラクティスを土台としつつ、日々試行錯誤を重ねています。今回は振り返り(レトロスペクティブ)に関して、その試行錯誤の結果をスクラムコミュニティにシェア・意見交換できれば良いなと思いプロポーザルを提出いたしました。 そして、ありがたいことに採択いただきまして今回の登壇の場をいただきました。 提出したプロポーザル 提出したプロポーザルは 「振り返りを積み上げて自分たちのプラクティスとして昇華•体得していくための仕組みと考え方」 という内容です。 https://confengine.com/conferences/scrum-fest-mikawa-2021/proposal/15589 具体的にはブログ下に公開しておりますスライドの冒頭に転載したとおり、 振り返りで得られたチームの気づき・知見がフロー情報で流れていってしまう ことに対する課題感です。 スライド内で言及した課題意識 スクラムを開発スタイルとして取り入れているチームはスクラムのフレームワークを土台として日々のプロダクトづくりをするわけですが、あくまでそれは汎用的なものなので自分達に合わせて最適にチューニングする必要があります。別のプラクティスを採用したり新規に自分達のプラクティスを生み出すこともあるでしょう。 そんな組織ローカルなプラクティスの連なり・一連をいかにストック情報として積み上げることが出来るか、弊チームが実践した内容を発表させていただきました。 発表資料 当日発表した資料はこちらです。 この発表資料は今年の年始に公開したブログ『 振り返りで積み上げた開発プラクティス(2020年総まとめ) 』のその後の姿ともいえます。 https://devblog.thebase.in/entry/bank-practices-2020 当日のセッション中に聴講いただいた方々との Q and A もスライド内に掲載させていただいています。 Q and A(1) Q and A (2) 振り返りを起点としたチームの成長を考える方々の参考になれば幸いです。 おわりに 今回発表の機会を頂いて、Discord上でリアルタイムに聴講いただいている方々とコミュニケーションができ、「たしかにそういう視点もあったな」など気づきが得られてとても有意義な時間でした。 オンラインと現地会場のハイブリッドシステムでしたが、不自由なくオンライン上で参加できました。運営の皆様誠にありがとうございました。
こんにちは! Process Engineeringグループのセキネです。 入社してからQAという言葉が社内で頻繁に使われることが多いのですが、どうも違和感を感じて今回の内容を社内で共有したところ結構反響があったので紹介したいと思います。 そもそもQAって言葉どういう意味で使っていますか? 実は結構前から社内では人によってQAという言葉の認識が異なっているように思います。 QAという便利な言葉によって認識の齟齬が起きているような話をちょくちょく聞くようになりました。 誤った認識や、認識の齟齬が起こらないようにしていく必要があります! QA(Quality Assurance) 日本語訳的には品質保証 QA ≠ テストです!!! (テストも含んではいますが、イコールではないです) もし、 QA=テストとして使っている人がいるのであれば少し認識を改めていただいた方がよいかもしれません。 品質保証の内容は以下の多岐にわたります。品質をよくするための活動全般をさします。 設計段階での機能仕様の確認・評価 テストケースの設計・テスト実行 (←QAってここだけじゃないよ テスト結果と不具合の分析 テスト自動化の検討 開発プロセスの見直し・改善 などなど、品質向上に繋がる活動 コード書いた人に自信を持ってリリースしてもらうための支援という表現が近い気がします。 QAのイメージとしてはこんな感じ ※以下、解説資料( JSTQBシラバス 1.2.2から抜粋) 品質保証(または単に QA)という用語がテストを意味するために使われることがある。しかし、 品質保証とテストは、関連してはいるが同じではない。 品質保証は、一般的に、品質が適切なレベルに確実に到達するよう、適切なプロセスを遵守することに重点を置く。 プロセスを適切に遂行すると、これらのプロセスで作成される作業成果物は一般的に高い品質を持つ。 これにより、欠陥の混入を防止できる。 さらに、根本原因分析を使用して欠陥の原因の検出と除去を行い、プロセスを改善するために振り返りミーティングでの発見事を適切に利用することが、品質保証の有効性を高めるために重要である。 品質コントロールには、テスト活動を含め、適切なレベルの品質を達成するために役立つさまざまな活動が含まれる。 テスト活動は、ソフトウェア開発またはメンテナンスプロセス全体の一部を構成する。 品質保証では、テストを含むプロセス全体を適切に実行することが必要となるため、適切なテストを支援する。 じゃあなんて言ったらいいの?? QAだと広義なので XXXテスト と伝えましょう テスト内でもフェーズを分けて 単体テスト(Unit Test、Component Test) コードを書いた人がモジュールレベルでテストする 結合テスト(Integration Test)←BASE社内でQAと呼ばれてテストしている大部分な気がしていました 単体テストが完了したプログラムモジュールや外部モジュールを本番環境同様に組み合わせ、正しく動作するか検証をおこなうテストです システムテスト(System Test) 開発側の最終検証工程であり、本番と同じ環境ですべての不具合が解消されていることが理想とされます 受入テスト(Accept Test)  一般的には発注者側がおこなう最終確認テストであり、このテストに合格することで納品・検収が完了します その他上記と合わせて、有用なテストタイプ(上手に組み合わせましょう) 探索的テスト (ad hocテストなどと呼ばれたりもします) ←社内ではあまり意識せずにやっている人が多いかも テスト項目は書かずに実施する 実施者の能力、経験に依存する 開発完了前でもスコープをや目的を絞ると有用 工数削減(事前に大きなバグの洗い出し)  結合テスト、回帰テストで拾えないバグの洗い出し 結合テスト後でも有識者にやってもらうと、想定外の不具合を見つけられるかも? 回帰テスト (リグレッションテスト、デグレチェック) 開発機能とは関係なく、サービス全体が正しく動作しているか確認するテスト セキュリティテスト 負荷テスト まとめ QAという言葉は便利ですがこれらを全て含んだ広義な言葉であり、人によって捉え方や認識が異なることが多いので、何を目的としたテストを実施するのか明確に関係者や、テスト実施をしてもらう方に伝えるようにしましょう。 また機能テスト、シナリオテストってよくやるけど、ここに書かれてないのでは。。。?と思われる方もいるかもです。 これらはテストタイプと呼ばれているもので、どのテストレベルでもでき、QAと近しいぐらいフワッとした表現になります。 フェーズと目的を明確にしましょう! QAエンジニア: 採用情報 / カジュアル面談
こんにちはProduct Dev Division所属の @cureseven です。8月にリリースしました「 再入荷自動通知 App 」のプロジェクトでは、ペアプロ/モブプロで開発を進めていました。 今回はその体験談を書いてみたいと思います。 なぜオンラインモブプロをやろうと思ったか 「再入荷自動通知 App」プロジェクトでは、バックエンドエンジニアの過半数がフルリモート下で入社して社歴半年以内、初めてのプロジェクト開発参加という状況でした。 また、「再入荷自動通知 App」プロジェクトでは、フレームワーク依存を脱却するため、DDDの実装パターンの導入を試みました。チームで輪読会を行い、DDDについて勉強しながらではあったものの、DDDの実装パターンの導入を実践するのが初めてのメンバーが多く、DDDの観点のすり合わせの意味も込めてペアプロ/モブプロを行いました。 オンラインモブプロの様子 BASEでは新型コロナウイルスの感染拡大を鑑み、現在はWFH(Work From Home)を推奨しており、オンラインでペアプロ/モブプロを実施することになります。 まず、メンバー全員がPhpStormを利用しているため、Code With Meという機能を使ってペアプロ/モブプロを試みました。2021年3月時点ではベータ版であり、コピーペーストができない、反映にラグがあるなどの問題にぶつかったため、zoomで画面共有をしながら実施する方法に落ち着きました。 ただCode With Meでは手元にドライバーの変更を即時反映しつつ別のファイルがどうなっているかなどを確認しながらナビゲータをすることができる、交代が楽などの利点がありました。zoomで実施した際には、細かくcommitしドライバーを交代しました。 Code With Me以外にも、VScodeのLive Shareという拡張機能を用いるなどの方法があります。チームのエディタ勢力を鑑みてツール選定していくと良さそうです。 ペアプロとモブプロの違いはナビゲータが1人か複数人かです。ドメインモデリングとドメインモデルの実装は認識を揃えるためにモブプロで行いました。 実装が進んでいったタイミングで、認識を合わせたい部分の実装はモブプロ、分担して行った方がいいものはペアプロと、使い分けるようになっていきました。 モブプロを通して学んだことや感じたこと プロジェクトを終えて、ペアプロ/モブプロの開発手法がどうだったかを振り返り、以下の内容を話し合いました。 PRレビュー時間の削減とコードの品質の向上 一人でコーディングしている時よりも、考慮漏れが少なくなりました。また、色々な仕様や認識を都度合わせて進めるので、結果的にレビューコストが少なくなりました。プルリクエストを出した段階で既に複数人のチェックが終わっています。プルリクエストでは、細かい指摘が減り、より本質的な議論ができたように感じます。 調査時間の減少 既存機能の仕様や、再現方法、PHPの挙動など、ドライバー自身が知らないことで調査が必要なシーンでは、それぞれが持っている知識や観点を集約し、素早く問題を解決できました。 入社したばかりの新人がモブプロに参加して進めるのは適している 開発着手当時入社して2ヶ月しかたっていなかった私ですが、ペアプロ/モブプロを実施することで助けられた場面がとても多かったです。 エディタの便利機能やデバッグ方法など、実装内容以外の部分でもアドバイスをいただき、今後の開発効率を上げることができました。 また、入社してすぐはわからないことも多く手詰まりになりがちですが、タスクも進捗し、かつ毎日学びを得て満足感の高い日々を過ごしました。 ドライバーはドライバーに専念する 1週間ごとに振り返りの時間を設けて、開発の進め方の改良を重ねていきました。そこで出た議論もご紹介します。 ドライバーは基本的にはナビゲータの言う通りに作業をする人になります。ドライバーに意思が芽生えてしまうと、先々進んでしまいナビゲータが置いていかれそうになります。ドライバーがどうしても意見したい場合は、ドライバーを交代し、ナビゲータとして発言するようにすることを意識するようにしました。 モブプロするべきタスクとしないタスクは分けるべき タスクの難易度に応じてモブプロが適しているタスクとモブプロじゃなくてもよいタスクは必ず存在すると思います。例えば、文言修正や前回のモブプロ/ペアプロで共通認識が取れて、あとは直すだけ!といったような内容のものはソロ(一人)で十分です。私たちは一概にモブプロしていこう!と決めずに、細かくタスクを分割し、以下の時に絞って複数人で作業するようにしました。 ドメインモデルの実装 一旦ソロで作業したが他の観点が欲しくなった時 モブプロ・ペアプロのメリットは多々ありますが、どうしても複数名の時間を奪うことになります。「流れで」「ついでに」モブプロ/ペアプロをするのではなく、「ここからは自分でやれそうなのでやっておきます」のようなコミュニケーションができると、効率よく開発が進められました。 また、モブプロ、ペアプロでタスクに着手する前に、参加者全員で「今日はこれをここまでやる!」という認識を共有しておくことも有効でした。ゴールからそれた内容は別の時間で集まるか、ソロでやってしまうかを判断します。ゴールに集中することで、疲れが見えにくいオンラインでのコミュニケーションを不必要に長引かせないように心がけました。 全員のスケジュール合わせるのが難しい ドメインモデリングや実装方針のすり合わせは、なるべく全員で行いたいものです。特に今回はメンバーが慣れていないDDDの実装パターンを適応したため、参加していないメンバーの解釈が違った場合、手戻りが起こることがありました。 極力作業に集中できるように普段のミーティングは精査しているものの、6名のエンジニアのスケジュールを合わせるのは難しかったです。毎週末の振り返りのタイミングで、来週はここでモブプロしよう、と仮でスケジュールを押さえておくことで、集まりやすくなりました。 1回あたりの作業は連続でやる場合も50分作業+10分休憩を意識してスケジューリングしました。 今後のペアプロ/モブプロとの付き合い方 チームメンバーとの交流の時間にも使えますし、オンボーディングの時や、新しいことに挑戦する時の認識合わせに有効な開発手法だなと感じました。 通話をずっと繋ぎっぱなしは辛い人もいるかと思うので、人に合わせて実施検討することが大切です。また、気軽に相談できる人・環境があることも大切です。今回の場合、プロジェクトの開発に直接入っていないマネージャとの1on1の時間が毎週あるので、客観的な意見を聞くこともできました。モブプロ/ペアプロはあまりない取り組みでしたが、挑戦したことが評価される文化も安心して取り組める要因になりました。 開発に詰まった時の一手段としていつでも実施できるように、普段からSpeak Openly, Be Hopefulでいたいです。
はじめまして!フロントエンドエンジニアのがっちゃん( @gatchan0807 )です。 9月7日にBASE主催で「 BASE Engineer 座談会 〜BASEの若手エンジニアがそれぞれの今と未来を語る!〜 」というイベントを実施したのですが、その中のLTパートで発表した「 Changelogを読んで自分のエンジニアキャリアを作る 」でお話しきれなかった部分があったので、そこも含めてお話した内容を記事にまとめなおしました! 当日ご参加いただいてLTを聞かれた方も、そうでない方も、ぜひご覧ください! 「Changelog」と「エンジニアキャリア」がどう繋がるのか 早速この記事でお伝えしたいことから書いてしまうのですが、 Changelogは最新機能だけでなく、未来のビジョンも含めたプロダクトの情報の宝庫で、それらをうまく使えばエンジニアキャリアづくりに活かせる! ということを記事を読んでいただいた皆さまには持って帰っていただければなと思っております。 ここからは、私がどういう経緯でChangelogを読むようになったか、Changelogを読むことでどんなメリットがあるのかについて、具体的に紹介していきます。 なぜChangelogを読み始めたのか ことの始まりはdependabotくんの対応から BASEにはショップオーナー向け管理画面で利用する社内用コンポーネントライブラリ「BBQ」があります。 今年2月頭頃、そのリポジトリに dependabot という、依存しているライブラリのバージョンアップ情報を調べ、アップデートするPull Requestを自動的に作成してくれるシステムを導入したところから話は始まります。 私は今年1月に入社したのですが、オンボーディング研修やたくさんのオリエンテーション、BASE内のコードリーディングを行っていた1ヶ月がちょうど過ぎた頃に、上記のような話題が上がり、実際に導入されることになりました。 ちょうどBBQのコードを読むきっかけにもなりそうと思って、空いた時間で参加させてもらうことになりました。 実際の dependabot の対応自体は難しいものではなく、自動的にOpenされるPull Requestを確認し、必要に応じて動作確認を行うだけです。 dependabot が設定した頻度で package.json / yarn.lock (Node.js関係のライブラリの依存関係情報が載っているファイル)をチェックする アップデートが公開されているライブラリがあった場合に、そのライブラリのバージョンを変更した( $ npm upgrade が実行された)Pull Requestが自動的にOpenされる 「マージ待ってるよ~」状態になるので、それをチェックして問題なければマージする 実際にOpenされるPull Requestの例 上記対応の3つ目の「マージして問題ないかのチェック」の中で、BBQのコードリーディングついでに 該当ライブラリの利用箇所の確認 と 該当ライブラリのアップデート内容が書かれたChangelog確認 を行っていたのですが、BBQのコードに詳しくなれるのはもちろんのこと、副次的に BBQ以外にも知見を得れていること に気が付きました。 dependabot で見つかるもの以外にも世には色んなChangelogがある 少し話は変わって、それまではプロダクトのアップデート情報やChangelogを見ても、流し見するだけで真剣には読んでいなかったのですが、 dependabot の対応でライブラリのChangelogを読むようになってから自然と世の中のプロダクトのアップデート情報が気になるようになりました。 いくつか読んだ中で印象的だったものとしては下記2つで、これらは社内ドキュメントツールにざっくり翻訳してまとめた記事を上げてみたりもしました。 https://hasura.io/blog/announcing-hasura-graphql-engine-2-0/ DBにかぶせたらGraphQLのエンドポイント生やせる君のHasuraに大型アップデートが入るアナウンス記事 複数DB接続できたり、使えるデータソースがPostgreSQL以外も対応するようになった https://developer.chrome.com/blog/new-in-devtools-90/ Chrome 90でのバージョンアップ内容がまとまった記事 flexboxがDevTools内でデバッグしやすくなったり、CWVが見れる機能がついた やってみてわかったChangelogを読むメリット LTの中では下記3つでまとめていましたが、時間の都合上1つ目の「変更点から、プロダクト・ライブラリがどう変化しようとしているのか気づくきっかけができる」についてしか触れることができませんでした。 しかし、個人的には2つ目、3つ目のメリットも非常に大きいと感じているため、こちらの記事でご紹介できればなと思います。 Changelogをフックにプロダクトを深ぼる リリースされているフレームワークやライブラリ、アプリケーションなどはすでに一定プロダクトとして成立する形で出来上がってる構造になるので、どこから見ていいのかわからなくなることも多いです。 しかしながら、Changelogには多くの場合、アップデートされた内容とともにアップデートされた理由と関連するユースケースの情報(こういう使い方を想定して機能を追加しましたみたいなこと)が書かれているので、アップデート対象そのものの知識をつけるのに効率が良いというメリットがあります。 これが こうなる これは2つ目の「アップデートからその対象の全体的な構造・関連性について知るきっかけができる」というメリットに大きく関わる部分で、Changelogからアプリケーションやライブラリの構造を深ぼっていく糸口になりますし、どういった機能が関連しあっているのかに気づくきっかけにもなり得ます。 また同時に、Changelogに書かれている想定ユースケースから「そんな機能があったのか!」と気づく3つ目のメリットにも繋がります。 実際に、ChromeのChangelogを読んだ際にDevToolsの知らなかった機能をいくつも発見して驚いた記憶があります。(意外と深い階層に便利機能があって気づいてないことが多々ある) 技術力向上にはアウトプットが重要と言うけども また、Changelogを読むだけでなく、(Changelogが英語記事の場合に)ざっくり翻訳して記事にまとめるという活動は自然とアクティブラーニングになって良い勉強になることを実感したので、合わせて紹介しておきます。 私は、学生時代に「知識の定着のためにはインプットだけではなく、アウトプットまで行うことが大事」と聞き、技術力の向上のために下記のような形で学習を行ってきていました。 とにかく勉強会に参加して知らない単語を知る(とりあえず情報に体当たりして、出てくるキーワードのインデックスだけ貼る) ↓ そこで知った単語をあとでググって調べてみたり、ちょこっとデモ環境を作って肌感覚を得る ↓ ☆(機会があれば)実践投入して更に自分の血肉にしたり、一定ラインを超えてきた辺りで手を動かして得たしくじりや知識を登壇・発表側に回る このやり方は☆の発表まで持っていくところが肝要で、 登壇・発表側に回ることによって 、自分の知識が整理されるし、理解があやふやなところを再度調べ直して知識の深さが高まったりするので 得られるものが沢山ありました 。 しかしながら、 常に身の回りに登壇・発表する場があるわけでもない のと、 発表する場の期待値のコントロールであったり表現方法を考えたりしないといけなかったりで意外と1回のカロリーが高い という問題をずっと感じていました。 アウトプットの方法としては、登壇・発表まで行かずとも経験した内容をブログ記事にする学習方法なども選択肢にありますが、上述したとおり Changelog記事自体が構造的にとっつきやすいようになっているため、初級者を脱してさらに技術力に磨きをかけたい!という人にとって効率よく知識を吸収できる という点で効率的だと感じたので、個人的におすすめできるなと思いました! 最後に… ここまでお読みいただきありがとうございました! 私がChangelogを読むようになった経緯と、実際にやってみて感じたメリットをつらつらと書かせていただきました。 読んでいただいた中で、もし少しでもここは良さそうだなと思うポイントがありましたら、ぜひ一度実践してみていただけると嬉しいです! これからも価値あるプロダクトを作り続けられるようにするためにも、自分の技術力アップのためにも、色々なChangelogを読んで行ければなと思います! それに、みんなでChangelogを読んでみよう会みたいなのを開催できたらなぁ…とか妄想したりもしています…! 最後に、イベントでお話したときの資料( https://speakerdeck.com/gatchan0807/changelogwodu-ntezi-fen-falseensiniakiyariawozuo-ru )と当日のアーカイブ動画( https://www.youtube.com/watch?v=mslmwMd2_rM )のURLを記載しておきます! イベントでは座談会という形で社内の雰囲気なども感じていただけるかと思いますので、よければご覧ください! 参照 世界のJavaScriptを読もう @ 2014 What's New In DevTools (Chrome 90) Announcing Hasura GraphQL Engine 2.0!
BASE BANKでエンジニアをしている @budougumi0617 です。 先日行われたNew Relic User Group Vol.0でGoでNew Relic APMを活用するためのOSSを紹介するLT発表をさせていただきました。 New Relic User Group Vol.0 New Relic User Group(NRUG)はNew Relicを活用するユーザーの集いです。NRUGは ヌルグ と読むとのことです。 コロナ禍後初開催となるVol.0は2021年9月15日にオンライン形式で次のコンテンツが行われました。 New Relic One 最新機能紹介 Nerd Life Talk (LT) ネットワーキング 関連ハッシュタグは #NRUG です。 https://twitter.com/hashtag/NRUG BASE BANKを含めBASEグループでも2020年9月よりNew Relic Oneを利用した開発・運用体制を敷いており、今回LT発表の機会をいただくことができました。 発表資料 私の当日の発表資料は次のスライドになります。 私の発表では、BASE BANKのGoアプリケーションでNew Relic Oneを活用するために私が開発し、OSSとして公開している3つのツールについて紹介しました。 当日紹介したOSS スライド中で紹介したOSSは次の3点です。 https://github.com/budougumi0617/nrseg https://github.com/marketplace/actions/action-newrelic-segment-lint https://github.com/budougumi0617/nrzap github.com/budougumi0617/nrseg nrseg コマンドはGoアプリケーションの関数・メソッドにNew Relic APMでSpanを生成するためのNew Relicライブラリの呼び出しを自動挿入するツールです。 GoアプリケーションへNew Relic APMを導入する際、Goでは実行時間を計測したい関数・メソッド全てで次のようなライブラリの呼び出しをする必要があります。 defer txn.StartSegment( "mySegmentName" ).End() nrseg コマンドは指定ディレクトリ配下の全てのGoのコードに含まれる関数やメソッド 1 にスパンを生成するために必要なコードを自動挿入します。 import ( "context" "fmt" "net/http" + + "github.com/newrelic/go-agent/v3/newrelic" ) func (f *FooBar) SampleMethod(ctx context.Context) { + defer newrelic.FromContext(ctx).StartSegment("foo_bar_sample_method").End() fmt.Println("end function") } func SampleFunc(ctx context.Context) { + defer newrelic.FromContext(ctx).StartSegment("sample_func").End() fmt.Println("end function") } func SampleHandler(w http.ResponseWriter, req *http.Request) { + defer newrelic.FromContext(req.Context()).StartSegment("sample_handler").End() fmt.Fprintf(w, "Hello, %q", req.URL.Path) } // nrseg:ignore ignoreコメントをしておけば無視します。 func IgnoreHandler(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "Hello, %q", req.URL.Path) } 既存アプリケーションの関数やメソッド全てに上記のような呼び出しを手動で挿入するのは手間なのでこちらを利用しております。 内部実装的にはASTから関数名を取得したり関数の位置を読み取って先頭行にコードを挿入したりと楽しい静的解析になっております。 action-newrelic-segment-lint actions action-newrelic-segment-lint はNew RelicのSpan対応漏れがある関数・メソッドがPull Request(PR)に含まれていたときに警告コメントをするGitHub Actionsです。 既存コードに対しては nrseg コマンドでNew Relicの対応ができます。 しかし、我々のGoアプリケーションは機能追加が活発に行われており、毎日関数やメソッドが追加されています。 新しく追加された関数やメソッドがNew Relicの対応をしていなかった時警告コメントをしてくれます。 PR上でNew Relic対応漏れの関数にコメント github.com/budougumi0617/nrzap nrzap はNew Relicの Trace ID や Span ID を uber-go/zap のログに埋め込むためのヘルパーです。 New Relicには分散トレースとログを紐付けるLogs in contextという機能があります。 しかし、New Relicの公式Goライブラリは logrus というロガーパッケージしか現在はLogs in contextに対応していません。 弊社のGoアプリケーションで利用しているロガーパッケージは uber-go/zap です。 Logs in contextに必要なNew Relicの Trace ID や Span ID を uber-go/zap のログに埋め込むためのヘルパーが nrzap です。 GetNrMetadataFields 関数を使うと context.Context オブジェクトにNew Relic関連情報が含まれていれば uber-go/zap のログに適切なJSONキーでNew Relicの Trace ID などの情報を含めることができます。 func ExampleHandler(w http.ResponseWriter, r *http.Request) { logger, _ := zap.NewProduction() defer logger.Sync() // nrgorillaなどでcontextから*newrelic.Transactionが取れる前提 + nrfs := nrzap.GetNrMetadataFields(r.Context()) logger.Info("failed to fetch URL", nrfs...) } 会に参加して 会ではNew Relicのコンサルタントの皆様よりPixieなどのNew Relic Oneの最新機能を紹介していただきました。 また、他社の皆様には開発組織へのNew Relic導入活動のコツを教えていただいたり、本番ダッシュボードを実際に拝見させていただけました。 懇親会では紹介したOSSを実際に利用してくださっている方と話しかけていただき、とてもモチベーションが高まりました。 どのOSSもまだまだバギーなところがあるので時間を見つけて改善を重ねておこうと思います。 終わりに NRUGの次回開催は12月を予定されているようです。今回とても有意義な時間だったのでぜひまた参加する予定です。 New Relicの最新機能、現場の生の声を聞ける貴重な会なので現在活用中の方、導入を検討されている方とも12月にお会いできればと思います。 正確には引数に context.Context オブジェクトまたは *http.Request オブジェクトを持つ関数/メソッドです。 ↩
BASE株式会社 Owners Experience Frontend チームのパンダ( @Panda_Program )です。 BASE では社内用コンポーネントライブラリ「BBQ」を使ってフロントエンドの開発をしています。 BBQ は Vue2 + Storybook v5 で作成されていましたが、 TypeScript Compiler API と社内のフロントエンドエンジニアの有志たちのおかげで Storybook のバージョンを最新の v6.3 にする対応が完了しました。 以前執筆した 「Vue2 + Storybook v5 のコンポーネントを v6 向けに書き換える」 という記事で、Storybook v5 から v6 の書き方である Component Story Format(CSF) への変更手順を確認しました。 この記事では、TypeScript Compiler API を使って前回の記事で紹介した書き換えをスクリプトで自動実行した話を紹介します。 前置き 率直な意見を書くと、TypeScript Compiler API(以下、Compiler API)はとっつきにくいです。なぜなら、初めて出会う単語や概念がたくさんあるからです。さらに、それをコードで操作するので慣れるまでは書きにくいし読みにくいです。 記事の途中で出てくる単語の意味が分からなくても、途中でコードの input と output の例を多めに掲載するので文章は読み飛ばしてもらっても大丈夫です。 TypeScript Compiler APIとは何か TypeScript Compiler API(以下、Compiler API) とは、TypeScript の Compiler を操作するための API です。 つまり、TS/JS のコードを解析したり、TS を JS に変換したり、コードの文法エラーを検出するといったコンパイラの機能を利用するための API です。 Compiler API の機能は多岐に渡りますが、今回利用したのは大きく分けて AST(Abstract Syntax Tree。抽象構文木) の走査(AST という木構造のデータを深さ優先探索で順番に見て回ること)、ファクトリ関数によるコード生成、visitor によるコードの部分的書き換えの3点です。 Compiler API で実現できること 百聞は一見に如かずです。CSF への書き換えに用いたスクリプト(以下、「書き換えスクリプト」または「置換スクリプト」)を使って、 「Vue2 + Storybook v5 のコンポーネントを v6 向けに書き換える」 で紹介した storiesOf で記述しているコンポーネントを置換してみましょう。 こちらが変換元のコンポーネントです。 // bbq/stories/v5/elements/button-group.stories.js import { action } from '@storybook/addon-actions' import { number, select, text, withKnobs } from '@storybook/addon-knobs' import { storiesOf } from '@storybook/vue' import { withInfo } from 'storybook-addon-vue-info' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import README from '../../elements/buttonGroup/README.md' import { devices } from '../../values/Devices' // Storybook コンポーネント名 const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories // addon-knob .addDecorator(withKnobs) // addon-info .addDecorator(withInfo) .add( 'ButtonGroup' , () => { return { components: { ButtonGroup } , // Vue Template template: ` <div : class = "'theme-'+device" > <p> <h2>デフォルト</h2> <div>アイコン+ラベル</div> <bbq-button-group :tag= "tag" :items= "iconsAndLabels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>アイコン</div> <bbq-button-group :tag= "tag" :items= "icons" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ラベル</div> <bbq-button-group :tag= "tag" :items= "labels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> </p> <p> <h2>カスタムUI</h2> <bbq-button-group :tag= "tag" :items= "['a','b', 'c', 'd']" @change= "({index}) => change(index)" :selected= "selected" :width= "width" > <template v-slot= "{items, change}" > <button v- for = "(item, index) in items" @click= "change(index)" > {{ item }} : {{ index }} </button> </template> </bbq-button-group> </p> </div> `, // Data data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , // Props props: { tag: { default : text( 'tag' , 'ul' ) } , } , // Computed computed: { icons() { return [{ icon: 'list' } , { icon: 'grid' }] } , labels() { return [{ label: 'ドラッグで並び替え' } , { label: '数値で並び替え' }] } , iconsAndLabels() { return [ { icon: 'list' , label: 'リストで並び替え' } , { icon: 'grid' , label: 'グリッドで並び替え' } , { icon: 'attentionCircle' , label: '念で並び替え' } , ] } , } , // methods methods: { change: action( 'change' ), } , } } , // Parameters { notes: README, } ) 置換スクリプトを実行することで、このコンポーネントが以下のように書き変わります。 // bbq/stories/v6/elements/button-group.stories.js import { action } from '@storybook/addon-actions' import { number, select, withKnobs } from '@storybook/addon-knobs' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import { devices } from '../../values/Devices' import README from './README.md' export default { title: "V6/Elements/ButtonGroup" , component: ButtonGroup, parameters: { notes: { README } , docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } } , argTypes: { device: { options: devices, control: { type: "select" } } , width: { options: [ "" , "full" ] , control: { type: "select" } } } } ; const Template = (args, { argTypes } ) => ( { components: { ButtonGroup } , props: Object .keys(argTypes), template: ` <div : class = "'theme-'+device" > <p> <h2>デフォルト</h2> <div>アイコン+ラベル</div> <bbq-button-group :tag= "tag" :items= "iconsAndLabels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>アイコン</div> <bbq-button-group :tag= "tag" :items= "icons" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ラベル</div> <bbq-button-group :tag= "tag" :items= "labels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> </p> <p> <h2>カスタムUI</h2> <bbq-button-group :tag= "tag" :items= "['a','b', 'c', 'd']" @change= "({index}) => change(index)" :selected= "selected" :width= "width" > <template v-slot= "{items, change}" > <button v- for = "(item, index) in items" @click= "change(index)" > {{ item }} : {{ index }} </button> </template> </bbq-button-group> </p> </div> `, methods: { change: action( "change" ), } } ); export const Default = Template.bind( {} ) Default.args = { device: select(`device`, devices, "pc" ), selected: 0, width: select( "width" , [ "" , "full" ] ), selected: 0, tag: "ul" } ; プロパティの重複や改行の不足もありますが、あえてありのまま記述しています。 Compiler API を使ったスクリプトを実行することでコードを劇的に書き換えられます。 これでCompiler API の威力を実感していただけたかと思います。 Storybook の書き換えに Compiler API を使った動機 さて、書き換えスクリプトの詳細に入る前に Compiler API で書き換え対応した理由を説明します。 想定読者はエンジニアなので早めに技術の話に入りたいのですが、Compiler API での書き換え実行に至った背景は普遍的なはずなので共有する価値があると考えています。 以下、Storybook の書き換えに Compiler API を使った理由を箇条書きで記述します。大きな目的は「Storybook の v5 から v6 へのバージョンアップを楽に実行すること」ですが、Compiler API を採用したのは以下のような背景があります。 書き換え作業にまとまった時間と人が確保できないこと( 人海戦術ができない ) BBQ というコンポーネントライブラリの保守・メンテは数人の有志者(以下、メンテナー)がスキマ時間に行なっている 体制として、例えば20%ルールなどの負債解消の時間を設けていない Storybook のバージョンアップは喫緊の課題でもないのでプロジェクト化されない ファイル数が多いこと BBQ の Storybook のコンポーネントは60ファイル BBQ の他にも Storybook を利用しているレポジトリがあるため、置換スクリプトを書くと再利用できる 簡潔に書くと、手作業による書き換え作業コストはファイル数に応じて比例しますが、スクリプトを適用できるのであればコストのほとんどはスクリプト作成だけで済むため、トータルコストが抑えられると判断して Compiler API での書き換えを採用しました。 またメンテナーに書き換えタスクを割り振って手作業で対応する場合は、新しい書き方を決定したり、書き換え方がわからないケースの調査結果の共有やコードレビューといったコミュニケーションコストが発生します。さらにそれらは往々にして同期的なものです。 このようなコミュニケーションコストを最小限にするために、書き換え方や気をつけるポイントを自分でドキュメント化して他の方に共有しました。これによりコミュニケーションを非同期化することができ、各自のタスクに集中できます。 書き換えの作業コストのイメージを可視化すると以下のようになります。 一人がスクリプトを書くほうが全体的にコストが少ない 書き換えスクリプトを使うと、最も簡単なケースでは、あるファイルを置換した後 Storybook で動作確認を完了するまで1分もかかりませんでした。 置換スクリプトで除外したケース ただし、スクリプトで細かいケースまで全部対応するのは実装コストが高くなるだけです。先程紹介した重複プロパティを削除する、などちょっとしたところであればあえて自動化しないという局所的な判断もしています。 また、1つのファイル内で forEach を記述して複数の storiesOf を実行しているようなパターンもスクリプトでの対象外としています。 [ 'blue' , 'red' , 'green' ] .forEach((color) => { const component = storiesOf(...) } ) このケースは、コードを書くより人の手で書き換えた方が早いと判断してメンテナーに割り振って対応してもらいました。 結果、60ファイル中置換で対応できたのが40ファイル、人の手で書き換えたのが15件ほどでした (15件のうち半分は TSX で書かれており、また残り5件は元から CSF でした)。 書き換えのスケジュール スケジュールは以下のように進みました。 1,2週間目: スキマ時間でCompiler API、Storybook v6 対応調査、書き換えスクリプト作成と調整 3週間目: スクリプトを実行して一括置換し、自分を含むメンテナに微調整タスクを割り振り 4週間目: PRレビュー依頼、コメント対応 5週目: 動作確認、Storybook v6 化リリース 最初の2週間は自分一人で動き、3週間目からはメンテナーの有志たちの力を借りました。量としては一人でも対応できるものだったのですが、v6 化にあたって知識の俗人化を避けるためにレビューも含めみんなで対応しました。 以上、置換スクリプトで「何ができるか」「なぜ Compiler API を使ったのか」「どのような背景があったのか」はお伝えできたかと思います。 Compiler API を使ったファイル書き換えの流れ 早速置換スクリプトの中身に入っていきたいところですが、 まずは Compiler API を使った処理の大まかな流れをお伝えします。 Compiler API でファイルを書き換える処理自体は簡単です。大まかな流れは以下のようなものです。 ファイルをメモリに保持する AST を解析する 特定の箇所を書き換える 書き換えた値をメモリからファイルに出力する これをコードで表現するためにミニプログラムを作成します。 ここでは説明のために「読み込んだファイルの import 文 だけ抜き出す」というシンプルな処理だけをしています。 import * as ts from "typescript" import * as fs from 'fs' // 1. ファイルを入力してメモリに保持する const code = fs.readFileSync ( './input.js' , 'utf8' ) const outputFilename = './output.js' const sourceFile = ts.createSourceFile ( outputFilename , code , ts.ScriptTarget.Latest ) const imports: string [] = [] // 2. AST を解析する function printRecursive ( node: ts. Node , sourceFile: ts.SourceFile ) { const text = node.getText ( sourceFile ) // node の解析結果を出力する(後述) const syntaxKind = ts.SyntaxKind [ node.kind ] const textWithSyntaxKind = ` ${ syntaxKind } : ${ text } ` console .log ( textWithSyntaxKind ) // 3. 特定の箇所を書き換える if ( ts.isImportDeclaration ( node )) { imports.push ( text ) } node.forEachChild ( child => { printRecursive ( child , sourceFile ) } ) } printRecursive ( sourceFile , sourceFile ) // 4. 書き換えた値をメモリからファイルに出力する fs.writeFileSync ( outputFilename , imports.join ( '\n' )) このコード例では文字列を抜き出しているため、Compiler API をご存知の方は少し変だと感じるかもしれません。なぜなら、eslint の plugin などでは「ASTを解析し、特定のパターンのASTを書き換えた後、printer.printNode で文字列に変換する」処理が一般的だからです。 ただ、storiesOf の書き方と CSF では構造が大いに異なるため、置換スクリプトではコードの場所に応じて使い分けています。 例えば、import 文では上記のように配列と文字列で扱い、 export default { title: 'SomeComponent' } のようなオブジェクトの箇所では AST を操作する形にしています。 Compiler API を使ったことがない方が大半だと思うので、今回は2つのうち簡単な方をコード例に挙げました。 ミニプログラムを実行する さて、このミニプログラムを実行してみましょう。入力と実行結果は以下のようになります。 入力 // input.js import * as path from "path" import * as fs from 'fs' const exists = fs.existsSync(path.resolve() + '/input.js' ) console.log(exists); 実行結果 // output.js import * as path from "path" import * as fs from 'fs' また、プログラムの途中に仕込んだ console.log(textWithSyntaxKind) の出力は以下のようになります。 このままでは読みにくいと思うので、今回抜き出しの対象とした import 文があるところにコメントを追加します。 # より右は出力結果はではなく追加したコメントです。 SourceFile: import * as path from "path" # ソースファイル全体 import * as fs from 'fs' const exists = fs.existsSync(path.resolve() + '/input.js') console.log(exists); ImportDeclaration: import * as path from "path" # import 文 ImportClause: * as path NamespaceImport: * as path Identifier: path StringLiteral: "path" ImportDeclaration: import * as fs from 'fs' # import 文 ImportClause: * as fs NamespaceImport: * as fs Identifier: fs StringLiteral: 'fs' FirstStatement: const exists = fs.existsSync(path.resolve() + '/input.js') VariableDeclarationList: const exists = fs.existsSync(path.resolve() + '/input.js') VariableDeclaration: exists = fs.existsSync(path.resolve() + '/input.js') Identifier: exists CallExpression: fs.existsSync(path.resolve() + '/input.js') PropertyAccessExpression: fs.existsSync Identifier: fs Identifier: existsSync BinaryExpression: path.resolve() + '/input.js' CallExpression: path.resolve() PropertyAccessExpression: path.resolve Identifier: path Identifier: resolve PlusToken: + StringLiteral: '/input.js' ExpressionStatement: console.log(exists); CallExpression: console.log(exists) PropertyAccessExpression: console.log Identifier: console Identifier: log Identifier: exists EndOfFileToken: 深さ優先探索で AST の node を一つずつ辿っている様子が分かります。 例えば、 console.log(exists) という箇所は以下のように解析されています。 CallExpression: console.log(exists) PropertyAccessExpression: console.log Identifier: console Identifier: log Identifier: exists 深さ優先探索でツリーの探索順の説明 また、この出力結果を見ると、なぜ ts.isImportDeclaration(node) というメソッドで import 文を判定できるかわかると思います。 それは、 import * as path from "path" が ImportDeclaration であると判定されるからですね。 これで大まかな動きはイメージしてもらえるかなと思います。 Compiler API を使った置換スクリプトの中身を紹介します では、実際に置換スクリプトの中身を紹介しようと思います。 ただし、書き捨てのコードであるため書きやすさ重視でコード量が多く、極力整理はしたもののあまり洗練されていないコードになっています。 そこで、置換スクリプトと Compiler API のエッセンスが伝わるような箇所を2つ抜き出して紹介しようと思います。 import 文の書き換え 1つ目は、元ファイルから import 関連の文字列を抜き出して少し書き換える処理です。 まずは変換前と変換後を紹介します。 // 変換前 import { action } from '@storybook/addon-actions' import { number , select , text , withKnobs } from '@storybook/addon-knobs' import { storiesOf } from '@storybook/vue' import { withInfo } from 'storybook-addon-vue-info' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import README from '../../elements/buttonGroup/README.md' import { devices } from '../../values/Devices' // 変換後 import { action } from '@storybook/addon-actions' import { number , select , withKnobs } from '@storybook/addon-knobs' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import { devices } from '../../values/Devices' import README from './README.md' 変換ポイントは以下の通りです。 @storybook/addon-knobs から import している text を削除 storiesOf 、 withInfo の import を削除 README の import パスを書き換える これを実現するためのコードは以下の通りです。説明のために処理を先程のミニプログラムに組み込みます。 import * as ts from "typescript" import * as fs from 'fs' const code = fs.readFileSync ( './input.js' , 'utf8' ) const outputFilename = './output.js' const sourceFile = ts.createSourceFile ( outputFilename , code , ts.ScriptTarget.Latest ) const imports: string [] = [] let hasReadme = false function printRecursive ( node: ts. Node , sourceFile: ts.SourceFile ) { const text = node.getText ( sourceFile ) if ( ts.isImportDeclaration ( node )) { imports.push ( text ) if ( text.includes ( 'README' )) { hasReadme = true } } node.forEachChild ( child => { printRecursive ( child , sourceFile ) } ) } printRecursive ( sourceFile , sourceFile ) // import 文を書き換える function getFilteredImport ( imports: string [] , hasReadme: boolean ) : string { // filter で不要な import を削除し、 // concat で必要な import を追加する let filtered: string [] = imports .filter ( item => ! item.includes ( 'storiesOf' )) .filter ( item => ! item.includes ( 'storybook-addon-vue-info' )) // README が存在したらパスを書き換える if ( hasReadme ) { filtered = filtered .filter ( item => ! item.includes ( 'README' )) .concat ( `import README from './README.md'` ) } return filtered.join ( "\n" ) .replace ( 'withKnobs,' , '' ) .replace ( "import { withKnobs } from '@storybook/addon-knobs'\n" , '' ) // knob は `preview.js`で読み込むので不要 .replace ( 'text,' , '' ) // knob の text を削除 } const filteredImport = getFilteredImport ( imports , hasReadme ) // ファイルとして出力する const newFile = ` ${ filteredImport } ` fs.writeFileSync ( outputFilename , newFile ) 変数 imports は string の配列なので、 concat と filter を使って追加と削除をしています。 この処理は、元ファイルの import 文をほぼ流用できるのでこのような愚直な実装にしています。次は、元ファイルの情報を使い回せない場合の処理方法を紹介します。 export default の追加 次に、AST を生成する方法を紹介します。CSF ではコンポーネントのメタ情報をオブジェクトとして default export します。これは storiesOf の書き方とは全く似ていません。 // storiesOf const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) // v6 の書き方 export default { title: "V6/Elements/ButtonGroup" , component: ButtonGroup , } 何か流用できるものはないかと探しながら前後を見比べると、ストーリー名だけは利用できることがわかります。 変更前のファイルからストーリー名を抜き出してV6という接頭辞をつけてストーリー名を差別化し、コンポーネント名はストーリー名から抜き出せれば OK です。 これをコードで実現していきます。前出のコードに追記していきます。 import * as ts from "typescript" import * as fs from 'fs' const code = fs.readFileSync ( './input.js' , 'utf8' ) const outputFilename = './output.js' const sourceFile = ts.createSourceFile ( outputFilename , code , ts.ScriptTarget.Latest ) const imports: string [] = [] let hasReadme = false // 以下を追加 let prevText = "" let title = '' let component = '' // SB コンポーネント名 const isPrevStoriesOf = ( text: string ) => text === 'Identifier: storiesOf' const includesStorybook = ( text: string ) => text.includes ( 'storybook' ) function printRecursive ( node: ts. Node , sourceFile: ts.SourceFile ) { const text = node.getText ( sourceFile ) const syntaxKind = ts.SyntaxKind [ node.kind ] const textWithSyntaxKind = ` ${ syntaxKind } : ${ text } ` // import 文 if ( ts.isImportDeclaration ( node )) { ... } // Storybook コンポーネント名を抜き出す(後述) if ( isPrevStoriesOf ( prevText ) && ! includesStorybook ( text )) { // クォーテーションを削除 title = text.substring ( 1 , text.length - 1 ) } // 一つ前の node を文字列でメモする prevText = textWithSyntaxKind node.forEachChild ( child => { printRecursive ( child , sourceFile ) } ) } printRecursive ( sourceFile , sourceFile ) // import 文を書き換える function getFilteredImport ( imports: string [] , hasReadme: boolean ) : string { ... } const filteredImport = getFilteredImport ( imports , hasReadme ) // コンポーネント名を抜き出す component = title.split ( '/' ) .pop () || '' if ( ! component ) { throw new Error ( 'Component 名を取得できません。' ) } // 以下の2つの関数を組み合わせて export default の AST を作成する function getExportAssignmentProperties ( title: string , component: string ) { const properties: ts.ObjectLiteralElementLike [] = [ ts.factory.createPropertyAssignment ( ts.factory.createIdentifier ( "title" ), ts.factory.createStringLiteral ( title ) ) ] properties.push ( ts.factory.createPropertyAssignment ( ts.factory.createIdentifier ( "component" ), ts.factory.createIdentifier ( component ) ) ) return properties } function createExportAssignmentAst ( node: ts.ObjectLiteralElementLike [] ) { return ts.factory.createExportAssignment ( undefined , undefined , undefined , ts.factory.createObjectLiteralExpression ( node , true ) ) } // 元の Storybook コンポーネント名と区別するために V6 でディレクトリを分ける const storybookComponentTitle = `V6/ ${ title } ` const properties = getExportAssignmentProperties ( storybookComponentTitle , component ) const exportAssignmentAst = createExportAssignmentAst ( properties ) // printer const printer = ts.createPrinter () const printNode = ( node: ts. Node ) => printer.printNode ( ts.EmitHint.Unspecified , node , ts.createSourceFile ( '' , '' , ts.ScriptTarget.Latest ), ) const exportAssignment = printNode ( exportAssignmentAst ) // ファイルとして出力する const newFile = ` ${ filteredImport } ${ exportAssignment.normalize( 'NFC' ) } ` fs.writeFileSync ( outputFilename , newFile ) import 文の処理より複雑なので、ポイントを解説していきます。 直前に出てきた要素を prevText にメモしておく コンポーネント名だけを抜き出すために、 printRecursive という再帰関数の中で prevText という変数に直前の node の名前を保存するようにしました。 以下のコードを解析するとその下のような結果になるため、直前の node の情報が必要なのです(関係のある箇所だけ抜き出しています)。 const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) CallExpression: storiesOf('Elements/ButtonGroup', module) Identifier: storiesOf StringLiteral: 'Elements/ButtonGroup' もしストレートに実装するなら「 storiesOf の第一引数を抜き出す」という方法で良いのですが、少し試してもうまく取得できなかったので直前の要素を参照する形にしています。 今回抜き出したいのはコンポーネント名なので StringLiteral: 'Elements/ButtonGroup' だけ使えれば良いです。 ただし、StringLiteral はただの文字列であるため import 文の from '@storybook/vue' のように他の場所でも出てくるので、 isStringLiteral だけでは判定条件が不足してしまいます。 このため、 storiesOf 関数の第一引数は必ずストーリー名の string であり、AST の node を辿る順番は深さ優先探索であることを利用しようと考え、試行錯誤した結果以下のような記述方法になりました。 const isPrevStoriesOf = ( text: string ) => text === 'Identifier: storiesOf' const includesStorybook = ( text: string ) => text.includes ( 'storybook' ) if ( isPrevStoriesOf ( prevText ) && ! includesStorybook ( text )) { // クォーテーションを削除 title = text.substring ( 1 , text.length - 1 ) } isPrevStoriesOf は関数名の通り、直前が storiesOf であるか判定します。 includesStorybook はその node が以下のような import 文ではないことを判定しています。 import { storiesOf } from '@storybook/vue' 以上が、 storiesOf('Elements/ButtonGroup', module) からコンポーネント名を抜き出す方法です。 factory.createExportAssignment でオブジェクトを default export する AST を作成する 次は default export するオブジェクトの作成です。 文字列操作でオブジェクトを作成するのはとても複雑です。このため、Compiler API の factory 関数を使いますが、factory 関数はたくさんあるため使い方を一つずつ知るのは大変です。 ここで裏技を使います。作成したいオブジェクトは以下のようなものだとわかっていましたね。 export default { title: "V6/Elements/ButtonGroup" , component: ButtonGroup , } このコードから factory 関数を生成する手段があるのです。 TypeScript AST Viewer を使えば関数の自動生成を実現できます。 サイトにアクセスし、左上の入力欄に作成したいコードを書き込めば左下に factory 関数が生成されます。 TypeScript AST Viewer の画面 今回は以下のようなコードが出力されています。 [ factory.createExportAssignment ( undefined , undefined , undefined , factory.createObjectLiteralExpression ( [ factory.createPropertyAssignment ( factory.createIdentifier ( "title" ), factory.createStringLiteral ( "V6/Elements/ButtonGroup" ) ), factory.createPropertyAssignment ( factory.createIdentifier ( "component" ), factory.createIdentifier ( "ButtonGroup" ) ) ] , true ) ) ] ; このコードを一度見てしまえば、変更すべき箇所は明白ですね。 プロパティはそのままでいいので、ストーリー名 V6/Elements/ButtonGroup とコンポーネント名 ButtonGroup を変数化すればいいと分かります。 先述のコードで読みにくい関数が出てきたと思いますが、これはストーリー名とコンポーネント名を変数にする関数を作り、factory 関数をラップしているだけです。 function getExportAssignmentProperties ( title: string , component: string ) { const properties: ts.ObjectLiteralElementLike [] = [ ts.factory.createPropertyAssignment ( ts.factory.createIdentifier ( "title" ), ts.factory.createStringLiteral ( title ) ) ] properties.push ( ts.factory.createPropertyAssignment ( ts.factory.createIdentifier ( "component" ), ts.factory.createIdentifier ( component ) ) ) return properties } function createExportAssignmentAst ( node: ts.ObjectLiteralElementLike [] ) { return ts.factory.createExportAssignment ( undefined , undefined , undefined , ts.factory.createObjectLiteralExpression ( node , true ) ) } createExportAssignmentAst の返り値である AST を printer で string に変換すれば、オブジェクトの default export が出力できます。 // printer。AST を string に変換する const printer = ts.createPrinter () const printNode = ( node: ts. Node ) => printer.printNode ( ts.EmitHint.Unspecified , node , ts.createSourceFile ( '' , '' , ts.ScriptTarget.Latest ), ) const exportAssignment = printNode ( exportAssignmentAst ) 処理をまとめて書くと、以下のような単純なものだと理解できます。 const exportDefault = printNode ( createExportAssignmentAst ( getExportAssignmentProperties ( 'V6/Elements/ButtonGroup' , 'ButtonGroup' ) ) ) 実際は argTypes や parameters をオブジェクトに追加するのでもう少し複雑になっていますが、基本的な考え方は以上の通りです。 なお、以下のようにオブジェクトをネストさせる書き方は今回は解説しません。詳しく知りたい方は 実際のコードをご覧ください。 export default { // ... argTypes: { width: { options: [ "" , "full" ] , control: { type : "select" } } } } ; 記述量は多いものの結局はプロパティと値の組み合わせなので、組み合わせ方さえわかってしまえば作成自体はそれほど難しくはないです。 終わりに ここまで読んでくださってありがとうございました。コードを追うだけでも大変だったと思います。 実際に使った変換スクリプトは GitHub で公開しています。 コードコメントもそのまま残しているので生々しいと思います。 このスクリプトの目的は「BASE 社内の Storybook + Vue のコンポーネントを CSF に書き換える」ものなので、あまり一般化していません。 そのため、そのままスクリプトを流用しようとすると齟齬が出るかと思いますが、処理の流れを追ったり実装で詰まったところの対処法の参考にはしていただけるかなと思います。 TypeScript Compiler API の記事はほとんどなく、あったとしても記事内容が古かったり書き換えの全体像が見えにくかったり、再帰関数の中で部分的に書き換えるようなものが多かったです。 このため、公開しているコードは手探りの中で実装したコードであり、より良い書き方があるだろうということは書き添えておきます。ただ、拡張できるように綺麗に完璧に書くことは当初の目的から除外していたこと、全部 CSF に書き換わったので目的を達成して役目を終えたので、このようなコードもありかなと思います。 この書き換えスクリプトは私が入社して1~2ヶ月目の時で、アサインされたタスクをこなしつつ空いた時間で作成したものでした。試しに小さいスクリプトを書いて、これを応用すれば労力をかけずに Storybook のコンポーネントを置換できるとわかったタイミングでマネージャーに相談して、そのまま Go サインを出してもらったため完遂できたことだと思います。 転職してまだ日が浅かった上に、自分は慣れていない少しレベルの高い手段で、解決できるかわからない課題にチャレンジさせて貰えた環境とマネージャーの決断に感謝しています。 BBQの週次定例でメンテナーのみんなに置換スクリプトを披露し、「便利ですね」といってもらったことで方向が間違っていないことを確認でき、しっかりやり切ろうというモチベーションに繋がりました。 TypeScript Compiler API はうまく使うと人海戦術を避けられる強力な武器になるため、興味のある方は課題解決の手段としてぜひ試してみてください。 参考 TypeScript AST Viewer AST Viewer です。コードを貼り付ければ AST やそのコードを生成するためのコードが表示されます Compiler API (TypeScript) TypeScript Compiler API の使い方がコードとともにわかりやすく紹介されているページです。 TypeScriptのcompiler APIをいじる TypeScript CompilerAPI - 創出の落書帳 - TypeScript Compiler API を解説した書籍 TypeScript Compiler APIを使って型を中心に実装を自動生成する
Customer Product Dev Groupの北川です。 直近では主にショッピングアプリ「BASE」のiOSアプリの開発をしています。 私たちモバイルアプリエンジニアの所属するNative Application Teamでは、 『チームで育てるAndroidアプリ設計』 の社内読書会を行いました。 『チームで育てるAndroidアプリ設計』について peaks.cc この書籍は2021年3月30日にPEAKSより出版されたもので、公式のHPによると対象読者として以下を挙げています。 アプリ開発をこれから始める方 チーム開発をよりよくしたい方、技術的なチームビルディングに興味がある方 アーキテクチャの選択、設計の維持に難しさを感じている方 新規開発/継続開発などチーム開発に関わる方 引用: https://peaks.cc/books/architecture_with_team ショッピングアプリ「BASE」はiOSアプリは2013年、Androidアプリは2015年にリリース以降、機能を追加してきました。アプリ開発を取り巻く技術トレンドも変化していく中で、設計パターンも数世代が混在しているという現状でした。 チームメンバーも増えていく中で、こうした秩序の綻びはアプリ開発のスピードの障害になると感じており、アーキテクチャの選択や改善をチームでどう行取り組んでいくか、という課題解決のヒントとして本書をチームで読むことにしました。 読書会の進め方 チームでTandemというコミュニケーションツールを導入しており 、こちらを使ってリモートで開催しました。各章ずつ読み進め、参加者は事前に読んでおいて、当日は読んでいて気になった部分について議論する形式で進めました。 読書会の記録は1つのGoogleドキュメントで管理し、話したいトピックを事前にコメントしたり、議論で出た観点、実際に取り入れられそうなアクションを記録したりできるようにしました。 読書会によって得られたもの 本書には既存のプロジェクトであっても取り組みやすい実践的なプラクティスが多く、読書会での議論をきっかけに実際に取り入れた改善がいくつかありました(前半が新規プロジェクト向け、後半が大規模プロジェクト向けという構成になっています)。 採用しているアーキテクチャを明文化する 序盤の3章を読み、最初に取り組んだのはアーキテクチャの明文化です。 どのような書き方や設計が推奨されるのかを改めて整理しました。指針が明文化されていることで、新しく参加するメンバー、レビューをする既存のメンバー双方の負担を下げることができます。 例えば、プロジェクトで採用しているライブラリについての説明として、RxJavaはKotlin Coroutineに移行中のため新たに利用するのは非推奨であることなどを記載しています。そのような異なる書き方が混在している状態と対処については、第3章「アーキテクチャの浸透と改善」で詳しく解説されています。 GitHub Issueでアーキテクチャについて検討する また、取り入れたい設計パターンや、技術課題についてGitHub Issueで議論するようにしました。 これまではSlackなどで議論が完結することが多々ありましたが、技術的な意思決定の経緯をGitHub Issueの形で残しておくと、その時にいないメンバー含めて後から意思決定を追いやすくなり、チームの齟齬を減らすことができます。 読書会を進めている期間中にもKotlin Coroutineへの移行、Jetpack Composeの導入、マルチモジュールの導入などいくつかの技術課題について検討を進めていたのですが、GitHub Issueに議論を集約することができました。 終わりに このように平易なものではありますが、読書会をきっかけにチームの開発フローの改善することができました。 『チームで育てるAndroidアプリ設計』は、チームでアプリ開発に取り組む際に最初に検討すべきことや設計を取り入れる具体的な進め方などがまとまっています。 実際にプロジェクトに取り入れた改善はもちろん、普段のミーティングの場では見えづらい、チームの課題や改善点について議論できたことが社内読書会を開催して最もよかったことの一つだと感じました。 今後もプロダクトを継続して成長させられる、よりよいチーム開発を目指してさまざまな取り組みにチャレンジしたいと思います。 また、BASEではiOSアプリエンジニアを絶賛募集中です。 少しでも興味を持っていただけましたら、お気軽にご連絡いただけると嬉しいです。 iOS: 採用情報 / カジュアル面談
8/21(土)に開催された ISUCON 11 予選に BASE から4チームが参加しました。 参加者の感想をお届けします! チーム「牡蠣に当たる時の効果音→カキーン」 BASEバックエンドチームの @cureseven です。このチームはエンジニアコミュニティのメンバーから募ってできたチームであり、BASEからは私のみ参加しています。 ISUCON11では31位 92336点でした。ISUCONに向けて 16回の練習 を重ねてきましたが、予選敗退という結果に終わり非常に悔しいです。 やったことの詳細は 個人ブログ をご覧ください。 BASEはPHPのプロダクトですが、 弊チームはGoで出場し、ISUCON練習のためにGoを書いてきました。BASE BANKではGoを利用しており、2週間に1度 Go Code Reading Party を開催しているので、参加してISUCON練習で困ったことを聞いたりしました。 去年も同じメンバーで出て本戦出場したのですが、今年は当時よりもずいぶん早く、去年よりもいろんな施策を打てるようになりました。ISUCON練習を通して成長を感じられたので有意義な大会でした。 またISUCONを通してDB周りの知識が身についたので、業務でも実行計画はどうか?より軽い実装はどのようなものか?有効なindexが貼れているか?などを意識して仕様検討・実装できるようになりました。 来年こそ本戦出場したいです。また、3年連続アプリケーション担当として参加しているので、今度はインフラ担当もやってみたいかもと思っています。 ISUCON12の準備は来月あたりから始めます。 個人ブログ @cureseven: 「ISUCON11に参戦しギリ敗退しました!!!!!!!!!悔しい!!!!!!!!!!!!!!!!!!!!!」 チーム「example.com」 川口 CTOです。 去年から前職の先輩である横山さんと、前職の超優秀だけど現在無職のエンジニアと出場しています。 案の定他メンバーのほうがスコアが良くて悲しい限りです、CTOたる威厳がない。 雑にdeployの仕組みを作ったりtrendのキャッシュとかを組み込んだりとかをしていました。 limit 1とか入れてたけど別修正とかも合わせて入れていて整合性チェックでfailして諦めたりしたので、もっと細かく手数を作れるようにならないとなと反省しきりです。 次こそは本戦、という気持ちで1年いますが来年もまた無準備になりそうな気がしています。 運営の皆様お疲れさまです。 横山 BASE所属の横山です。去年に引き続きCTOとニートエンジニアと3人で参戦しました。 初速だけはよく開始1時間ぐらいまでは10位内に張り付いていたのですがそこからの伸びがなく最終スコアは43,684点で90位でした。 自分は主にMySQLまわりを触り、1台をレプリカにして参照クエリをレプリカに投げる、みたいなことをやっていました。 レプリカ化とかはじめてやってみましたができてよかったです。 スコア的な伸びはなかったんですが・・・。 朝から仕様を頭に叩き込むのは辛かったですが、終わってみればあっという間でした。 また参戦します。 チーム「Speed of Sound」 松雪(@applepine1125) BASE BANK 所属の budougumi0617 と applepine1125 で「Speed of Sound」というチームを組みエントリーしました。 2人とも今回が初参加だったので、競技終了時に0ptにならない&100位以内という目標を掲げ、当日は無事? 最終スコア47,995点で79位をマークしました。 スロークエリを発見してindexを貼ったりbulk insertを活用できれば初参加でも100位以内に食い込めるんだなと思いつつ、それ以上を目指そうと思ったときにはもっといろいろな武器を持っていないと上位入賞は遠いな・・・と悔しい限りでした。 来年も是非参加したいと思います。運営の方々、準備から開催までありがとうございました!本戦出場される方も頑張ってください! 清水(@budougumi0617) 今回初の個人スポンサー、初の参加をしました。 以前から「いつか出るぞ出るぞ…」と思っていたので予選参加600チームに潜り込めてよかったです。 事前準備として環境構築スクリプトなどを作成して臨みました。最初の各種計測の設定は準備のおかけでスムーズにできてよかったです。 しかし、その後の実際のパフォーマンス改善はなかなか難しかったです。 「計測結果が悪いけど直し方がわからない」「たぶん良くなるからいじってみる」というような振る舞いが多くなってしまい、計測データに基づいた原因分析、修正対応ができませんでした。 分析力・対応力をもっと身につけて次回は本戦に出場したいと思います。 環境構築はCloudFormationで一瞬終わりましたし、ベンチマークは気軽に何回も実行できて快適な競技環境でした。ありがとうございました。 詳細な当日の様子は、それぞれの個人ブログの記事に記載しています。ぜひそちらもご覧ください! 個人ブログ @applepine1125: 「ISUCON11予選に参加した(47995点で予選落ち)」 @budougumi0617: 「ISUCON11予選に参加した(最終スコア47,995点 79位) #isucon」 チーム「賢者のつどい」 BASE のフロントエンドチームの @Panda_Program です。 大学時代の友人である 0daryo が声をかけてくれたので「賢者のつどい」という2人チームでエントリーしました。 私は BASE でフロントエンドエンジニアをしているため、普段の業務ではサーバーのコードを書いたりインフラを触ることがありません。 前職では業務で PHP も書いていたものの、これまで特にパフォーマンス向上の業務はしたことがなかったのでいい機会だと思ったのがエントリーの理由です。 ISUCON は初参加で右も左もわからなかったところ、幸い社内の Slack に ISUCON に挑戦する人たちのチャンネルがあったので参加してみました。 エントリーの期限や参加後に対応が必要なことのアナウンスやリマインド、また他の方もそれぞれ予選に向けて事前準備をしているという発言を見ることで ISUCON 参加の実感が湧いてきたり、自分も準備をしなければというやる気をもらいました。 予選当日の様子は 個人ブログ に記載しているのでそちらをご覧いただければと思います。 最終スコアは16680(598組中249位)と BASE の他のチームの戦績に比べて結果は芳しくなかったです。 ただ、事前準備では普段業務で触らないところが勉強できたこと、パフォーマンス向上の鉄則である「推測より計測」を実際に体験できたため参加した甲斐がありました。 昼休憩で PC から離れた時に肩があまりにも凝っていたことから、集中と緊張のあまり体が強張っていたのだなと後から気づき、なんだか大会らしいなと感じたのはいい思い出です。 ISUCON の結果は事前準備でほとんど決まるのだなと思いました。来年はしっかり準備して予選に臨む所存です。 個人ブログ @Panda_Program: 「ISUCON11に参加しました」 最後に ISUCON 運営の方々、とても有意義な大会を開いていただきありがとうございました。 また来年も参加したいと思います!
BASE開発担当役員の藤川です。 BASEのYoutube動画で、EC系SIerからBASEに転職してきてくれて4年半を経過したタイミングでの宮村さんにBASEでの働き方についてインタビューをしました。今回は前編と後編の2本立てになります。 前編動画:スタートアップの成長過程の風景について www.youtube.com 2017年に転職してきて、今では技術責任者と呼ばれる開発プロジェクトの設計や技術責任を牽引する役割を担ってもらっていて、SIerからスタートアップに転職したリアルな話を聞いてみました。 動画の中で思い出のプロジェクトとして携帯キャリア決済の実装についてお話いただきました。当時のBASEはクレジットカード決済、後払い、銀行振込等は実装されていて、そこに携帯キャリア決済が実装されました。 キャリア決済を実装したことがある人ならわかると思いますが、相応に決済フローが複雑で情報もわかりにくい上に3キャリア毎に挙動の癖があります。実際リリースした後に、想定しないタイムアウトエラーが発生したり、フロー制御にも気をつけなくてはいけなかったりしました。しかし、先方の実装が見えるわけでもないし、相手は超大企業ということもあり、開発ドキュメントの理解力が問われます。 また、SIerや受託出身の方は開発ドキュメントの作成や管理に慣れているので、当時、まだまだスタートアップ初期の内輪な状態から人数も増え、いわゆる組織として変化させていく中で、ドキュメント整備を通じて開発ナレッジの整理を自然としてくれていたというのが印象的でした。 よくスタートアップとSIerを比較した時にドキュメントが整備されていなかったり、いろんなことが曖昧であることが転職するメリット・デメリットとして挙げられることがあります。 とりわけ初期のスタートアップでは、明日をも知らぬフェーズで仕事をしていく必要があるわけで、実装面、ドキュメント面共々、5年後10年後を見据えた活動をしているよりは、今を生き抜くことが優先されます。 しかし、ある程度サービスが成功し安定成長を開始してからは、人材採用は未来を作るための最重要課題となり、常に実装力、ドメイン知識に関する初心者の方が毎月入社してくる流れにシフトします。それ故に、先輩の方々からドメイン知識や実装に関する情報が適切に共有されていく流れは重要です。 今回はそういうところでの心構えなども聞いてみました。 大手企業に新卒で入った人たちは、各社のドメイン知識を吸収し成長をお膳立てされるプロセスが当たり前に存在しています。入社後数年は昇給が横並びの大企業も少なくないですが、その間は教育期間として考えられていることが理由だと思います。 しかし実際に企業が急成長するタイミングを目の当たりにすると、そういうことが当たり前に存在するのではなく意図して作っていくものだということを再認識されられます。それがスタートアップが大きくなっていく時に必要とされるスキルです。 後編動画:リモートだからこそ重視されるアジャイル設計 後半の動画では、BASEの現状の開発状況についての話を聞きました。 www.youtube.com その中でも昨今のリモートワーク下での新入社員の適切なオンボーディングは非常に重要な課題です。まだ心理的安全性も適切に構築できてない状態で、オンラインコミュニケーションを強制されるコロナ禍において、入社いただく方に如何に馴染んでもらって活躍いただくかを常に注視しています。その中で設計や開発プロセスに業界標準のノウハウを取り入れているという話をしました。 開発はGitHubフローを活用していますが、設計面でもドメイン駆動設計、アジャイル開発などと言ったノウハウを我々も導入しています。組織としては、まだまだ試行錯誤中ですがプロジェクト毎にチャレンジをしている状況です。 このような勉強会も随時行っています。 devblog.thebase.in スモールチームだったショップが成長し続けることにしっかりついていく 群雄割拠のEC支援事業の市場でBASE社が存在し続ける理由としては、個人やスモールチームにフォーカスした機能を提供することでビジネスが成立するんだよという、それまで言語化されていなかった独特な領域に市場を見出したことだと思います。そういった初心者にも使いやすいサービスはこれまでいくらでもあったにも関わらず、そこに特化した意思決定を続けてきたことがBASEが成長した鍵だと考えます。 一方で、BASEで成長したショップさんがBASEの機能では物足りなくなって別のサービスに移動されてしまうということは避けなくてはなりません。EC市場の中で、「お店を作ってみたい」という入り口を押さえているからこそ、成長したショップさんがずっとBASEを使い続けていただくというのは最重要課題として、常に存在し続けています。 BASEのプロダクトデザインとしては初心者の方がすぐにお店が作れますというシンプル性を担保しつつ、成長したショップさんが使い続けることができる機能やスケーラビリティを提供し続けるという部分で、ある種の二律背反的な要素も存在します。宮村さんがインタビューで話しているように、実は以前の方が限られたリソースの中で割り切ったデザインをしていたのに対して、最近は、大きくなったショップさんの組織構造等も踏まえて、しっかり全体を考えていくことが求められており、それらを担保し続けられる人材が不可欠です。 もちろん、それは当社の開発経験の中で培っていけばよいので、これまでのBASEがどのようにサービスを成長させてきたかをしっかりトランスファーしながらになりますが、そのようなドメイン知識を通じてBASEの作り手として活躍する人をもっと増やしていかないといけません。 エンジニアの働き方はBASEの成長を生み出すことと連動する 今回、改めて宮村さんと話したことを反芻してみると、中途採用である個々人がこのチームに投入されるノウハウというのが、BASEというサービスの成長の中でうまく溶け込んでいるということを再認識させられました。 我々はまだまだ発展途上のチームであり、一人ひとりの経験が新しい視界を生み出すことに繋がります。開発のプロセスにおけるアジャイルの導入も決してCTOのビジョンで先導したのではなく、それの先導者がいて、フォロワーとしてついていく人たち、それぞれの前向きな姿勢があってこそできていることです。CTOがやったのは、そういう人達の考え方を聞き、開発の進め方としてオーソライズしたということです。 一人ひとりが有名な人とか、そういう人たちを優先する採用はしていなくて、このチームを全体で良くしていくんだという期待が持てる人しか採用していないので、是非、一緒に成長していきたいという人がいたら、是非、お話させてほしいです。
はじめに こんにちは! BASE 株式会社 Customer Product Dev で Android エンジニアをしている小林です。 ショッピングアプリ「BASE」のAndroid版アプリの開発を担当しています。 最近、フォロー中タブ追加というアプリのトップ画面を大きく変えるリリースを行いました。 その際、RecyclerViewの実装でPagingライブラリの3.0.0を導入してページング処理を実装してみたのでその話をしていきたいと思います。 Pagingライブラリとは? データの追加読み込みやメモリに保持するデータ量の管理をやりやすくしてくれるライブラリです。 2021年9月時点ではPagingライブラリの最新バージョンは3.0.1になっています。 https://developer.android.com/topic/libraries/architecture/paging/v3-overview 2021年5月にメジャーバージョンが上がり、v2の時とは色々と実装方法が変わっています。 Paging 3ライブラリを使うと、以下の機能がサポートされます RecyclerView Adapterと組み合わせることで、自動で次のデータを取得してくれるページング機能 Kotlin コルーチンやFlowを使ってのデータの取得や反映 リフレッシュやローディング中、エラー時のUIの切り替え 今のアプリ内のPaging処理の現状と問題点 今のアプリはScrollListenerを使って独自にページングを実装しています。 スクロール時に何件まで商品を表示したかをチェックして、ある程度読み込んだら次のページを読み込み始める形です。 この実装はJavaで書かれていてKotlin コルーチンとの相性が悪く、今のアプリの構成に合わない実装になっていました。 Pagingを使おうとした理由 上記の問題点を解消するために、現状のページング処理のKotlin化&アーキテクチャ適応も検討しました。 しかし、以下のことを考慮した結果、今回実装する新規画面にPaging 3ライブラリを導入することにしました。 機能的にPaging 3ライブラリが優れていること 特に、次のデータを自動で取得してくれるページング機能はコード量を大きく減らせると期待できる 実装 公式ドキュメントを参考に、ViewModelにFlowを定義して、それをFragmentで購読する形で実装していきました。 https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data?hl=ja#pagingdata-stream ViewModel MVVMにおけるViewModelです。AACのViewModelを継承しています。 Pagerクラスを生成して、remoteMediatorを設定します。 class ItemsViewModel( //... ) :ViewModel() { // 商品情報を取得するRepository @Inject lateinit var itemsRepository: ItemsRepository // お気に入り情報を取得するRepository @Inject lateinit var favoriteItemsAllRepository: FavoriteItemsRepository // APIで取得したリストの情報をキャッシュするDatabase @Inject lateinit var itemDatabase: ItemDatabase // 1画面に表示される商品数 // pagingの先読み商品数の設定のために定義している private val viewIngItemNum = 15 @OptIn (ExperimentalPagingApi :: class ) val flow = Pager( config = PagingConfig(pageSize = viewIngItemNum, initialLoadSize = viewIngItemNum), remoteMediator = ItemRemoteMediator( itemDatabase, itemsRepository, favoriteItemsRepository ) ) { ItemDatabase.ItemDao().pagingSource() }.flow.map { pagingData -> pagingData.map { it.convert() } }.cachedIn(viewModelScope) //... } RemoteMediator Repository経由でAPIから商品情報を取得しています。 後述するお気に入り情報の更新のために、ローカルキャッシュとしてRoomのテーブルクラスに格納しています。 色々書いていますが、やっていることは以下のとおりです APIから読み込むページ数を決める リフレッシュならRoomのテーブルを削除する APIから取得する 取得した情報をRoomのテーブルに格納する Roomのテーブルに情報が更新されたことをViewModelのFlowが検知して、Adapterに反映されます。 @OptIn (ExperimentalPagingApi :: class ) class ItemRemoteMediator( private val database: ItemDatabase, private val itemsRepository: ItemsRepository, private val favoriteItemsRepository: FavoriteItemsRepository ) : RemoteMediator< Int , ItemEntity>() { override suspend fun load(loadType: LoadType, state: PagingState< Int ,ItemEntity>): MediatorResult { val page: Int = // 略 リフレッシュ時には1,追加読み込み時はnextKeys等の読み込むページを指定している try { // itemRepository.itemsはRxJavaのSingleで返ってくるので、await()を使ってKotlin コルーチンに変換しています。 val result = itemsRepository.items(page).subscribeOn(Schedulers.io()).await().items // APIからの値をローカルDBに格納できるクラスへ変換する val entities = result.convertToEntity() entities.forEach { // 取得した商品がお気に入りしているかどうかを取得して設定する it.isFav = favoriteItemsRepository.isFavorite(it.itemId) } val endOfPaginationReached = result.isEmpty() database.withTransaction { if (loadType === LoadType.REFRESH) { // PullToRefresh等で、最初から読み込む場合はRoomのテーブルの情報を消す database.remoteKeysDao().clearRemoteKeys() database.itemDao().clearAll() } val prevKey = if (page == 1 ) null else page - 1 val nextKey = if (endOfPaginationReached) null else page + 1 val keys = entities.map { entity -> ItemRemoteKeys(repoId = entity.itemId, prevKey = prevKey, nextKey = nextKey) } // Roomのテーブルに取得した商品情報とページ数を格納する database.remoteKeysDao().insertAll(keys) database.itemDao().insertAll(entities) } return MediatorResult.Success(endOfPaginationReached) } catch (e: RetrofitException) { return MediatorResult. Error (e) } } } Fragment ViewModelで設定したflowで、変更された値をAdapterへ通知します。 class ItemsFragment : Fragment(){ lifecycleScope.launch { binding.viewModel?.flow?.collectLatest { pagingData -> //... // PagingDataAdapterのメソッド adapter.submitData(pagingData) } } } Adapter PagingDataAdapterを継承していること以外は通常のRecyclerView.Adapterと実装方法は変わらないです。 ViewModel class ItemsAdapter( //... diffCallback: DiffUtil.ItemCallback<ItemViewData> ) : PagingDataAdapter<ItemViewData, ItemsAdapter.ViewHolder>(diffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int ): ViewHolder { return ViewHolder(ViewItemBinding.inflate(LayoutInflater.from(parent.context)), listener) } override fun onBindViewHolder(holder: ViewHolder, position: Int ) { val item = getItem(position) holder.setData(item, position) } class ViewHolder( //... ) : RecyclerView.ViewHolder(binding.root) { fun setData(data: ItemViewData?, index: Int ) { //... } } } // Paging 3ライブラリが、Adapter内の商品のチェックを行うための処理 object ItemComposer : DiffUtil.ItemCallback<ItemViewData>() { override fun areItemsTheSame(oldItem: ItemViewData, newItem: ItemViewData): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: ItemViewData, newItem: ItemViewData): Boolean { return oldItem == newItem } } 実装してみての詰まったところ アプリでは、ユーザが商品をお気に入りする機能があります。 他の画面でお気に入りが変更された場合、フォロー中画面の商品リストに反映する必要があります。 しかし、Paging 3ライブラリでFlowを使って商品リストの反映を行っていたため、すでに取得/反映している商品の情報をAdapterやViewModelでは更新することができませんでした。 これはPaging 3ライブラリの仕様となっています。 一度リストの情報をPagingDataとして取得した後は、個々の情報の中身を取得したり変更することができません。 現在更新を可能にするissueが上がっている状態です。 https://issuetracker.google.com/issues/160232968 解決策 Roomを使ったローカルキャッシュを実装して解決をしました。 Paging 3ライブラリは中間にRoomのテーブルを挟んで実装した場合、Roomのテーブルを唯一の情報源として動作します https://developer.android.com/topic/libraries/architecture/paging/v3-network-db?hl=ja#basic-usage RemoteMediator 実装は、ページング データをネットワークからデータベースに読み込む際には有用ですが、データを直接 UI に読み込むためには使用できません。代わりに、アプリはデータベースを信頼できる唯一の情報源として使用します。つまり、アプリはデータベース内にキャッシュされたデータのみを表示します。PagingSource 実装(Room によって生成されたものなど)は、データベースから UI へのキャッシュ データの読み込みを処理します。 これにより、お気に入りが変更された場合に、API通信を行うこと無く情報の反映ができるようになりました。 実装では、RxBusなどで他画面からのお気に入りが変更されたことの通知を受け、changeFavを実行してテーブルの更新を行っています。 class ItemsViewModel{ // APIで取得したリストの情報をキャッシュするDatabase @Inject lateinit var itemDatabase: ItemDatabase val flow = // 略 // map = お気に入り状態を変更した商品ID=Stringと、変更後状態=Booleanが格納されている private fun changeFav(map: Map < String , Boolean >) { viewModelScope.launch { itemDatabase.withTransaction { val table = itemDatabase.itemDao() // 商品IDでキャッシュの情報を取得する table.selectByItemId(map.keys.toList()) .forEach { val beforeFav = it.isFav it.isFav = map[it.itemId] ?: false if (it.isFav != beforeFav) { // リスト表示用のキャッシュに、お気に入り変更を更新している table.update(it) } } } } } まとめ Paging 3ライブラリでのリスト表示処理の実装と、他画面からの通知を受け取ったお気に入り変更処理の実装について書いてみました。 Paging 3ライブラリはリスト表示についてどんなシチュエーションでもカバーできるようになっていて、一見難しいです。 でも、使いこなせるようになれば自分で実装するよりも手間もコード量も少なく済むのでとても便利なライブラリだと思います。 おわりに 最後になりますが、BASEではAndroid/iOSのエンジニアを募集しています。 カジュアル面談もありますので、興味のある方は気軽にご連絡ください! Android: 採用情報 / カジュアル面談 iOS: 採用情報 / カジュアル面談
こんにちは!New Owners Dev GroupにてEngineering Managerをしている植田です。 皆様の会社・組織ではSlackを導入・活用されていますか? 私は2021年4月にBASEへ入社したのですが、色々と驚いた点のうちの一つに 「Slackを全社をあげて活用しまくってる!」 というものがあります。 BASEではただのチャットツールのみならず、あらゆる場面でSlackを活用し業務を効率化していることに大きな驚きを覚えました。エンジニアは当然のことながら、非エンジニアもSlackをフル活用しているので今回はそれを大公開してしまおうと思います! ※実際社内で利用しているSlackのキャプチャを掲載しているため、画像にはマスクを施しています BASEでのSlack活用例 今回、記事を書くにあたり社内の関係者に声をかけ、どんな場面でSlackを活用しているかヒアリングをかけたところ、ありとあらゆる場面でSlackを活用していることがわかり、図にまとめてみました。 ご覧の通り、各組織でSlackを活用していることがおわかりいただけると思います。では、それぞれの代表的な活用事例を細かくみていきましょう。 ①コラボレーションツールとの連携 BASEでは、社内Wikiツール、タスク管理ツール、カレンダーツール等、いわゆるコラボレーションツールとSlackとの連携がなされています。特に、WikiツールであるKibelaとの連携は強力で、新しい記事が作成されたり、記事にコメントが追加されると通知されるチャンネルがあるため、記事の見逃しがなくコラボレーションを後押ししています。 Kibela通知の例 また、Googleカレンダーに登録している「機能リリースカレンダー」とも連携しているため、リリース情報を見逃すこともありません。 リリース通知の例 その他の活用事例 「Asana」との連携で、自身に割り当てられたタスクやステータスの変わったタスクが通知されます(Asana=タスク管理ツール)。 「Redmine」との連携で、SQLレビューチケットの通知がされます。※BASEではSQLのレビューをRedmineのチケットで管理しています。 ②エンジニアリングの中でのSlack活用例 現状Slackなくして、エンジニアリングは回らないといっても過言ではないほどエンジニアリングとSlackは密接に紐付いています。 GitHubと連携することで、プルリク通知やIssue通知をSlackで受け取っています。 プルリク通知の例 また、ChatOpsを利用したデプロイといった連携もしておりとても便利です。 デプロイ連携の例 Looker を利用したデータモニタリング(データ通知)も仕組み化されています。SlackとLookerを連携し、デイリーで主要KPI実績をSlackチャンネルに流しています。こうすることで全社員が数値、KPIに対する意識を高めています。KPI情報ということでキャプチャを載せられないのが残念です。 その他の活用事例 「Mackerel」や「Sentry」との連携で、メトリクス情報を通知 「Geekbot」を利用し日報の登録 CRONの正常稼働を通知 ③総務・人事・労務におけるSlack活用例 BASEでは総務・人事・労務といったバックオフィスチームもSlackを活用していますよ! 例えば、健康診断の日程調整といったフローも、Slackのワークフローで完結されています。入社して地味にすごいなと思った一つです。 健康診断ワークフローの例 また、労務関連のお問い合わせはやはりワークフロー化されています 労務ワークフローの例 こちら の記事で紹介されている通り、BASEでは自社サービス利用サポートの福利厚生があります。「BASE」で購入した商品を申請し、共有用の回答欄に商品URLを入力すると、該当のチャンネルでどんな商品を購入したかが通知されるようになっています。他の社員が購入した商品やショップが分かり、素敵なショップを知るきっかけになります。 利用補助の投稿例 その他のSlack活用事例(一部) 慶弔・特別休暇の申請をワークフローで受け付け、労務メンバーのみのチャンネルに通知 人事発令情報を必要関係者へ通知 Twitterなどで都やメディアからのコロナ関連のニュースがあったら該当のチャンネルに通知 Twitterなどで気象・地震情報が発信された場合に該当のチャンネルに通知、震度5以上の地震発生で必要関係者へ通知 コロナ関連や、天災情報をSlackに通知するというところに総務、労務らしさが出ていますよね。 ④採用におけるSlack活用例 採用チームでは各種採用サービスを利用していますので、Slackと連携し採用サービス側での通知をSlackへ連携し、スピーディな採用活動を実現しています。 採用サービス通知の例 その他の活用事例 ワークフローにて、お知り合い紹介フォームを構築(リファラル採用フォーム) Slackのワークフローにて、採用会食申請フォームを構築 BASEテックブログ のTweetをSlackチャンネルへ通知 ⑤法務におけるSlack活用例 続いては法務でのSlack活用です。 いわゆる法務チェックはワークフロー化されています。 法務チェック申請フォーム 法務では クラウドサイン を利用しており、締結が完了するとSlackへ通知されるようになっています クラウドサイン通知連携 こちらは後述する「リアク字チャンネラーアプリ」を使った活用例です。 Slackチャンネル上のメッセージに対して「法務check」リアクションをすることで、任意のチャンネルへメッセージを連携することができます。 法務チェックのリアク字チャンネラーアプリ使用例 ⑥CSにおけるSlack活用例 Zendeskと連携し、購入者さんからのご意見をSlackへ通知し、プロダクトの改善に繋げています。 Zendeskとの連携例 また、ショップオーナーさんからのご意見もSlackへ通知し、こちらもプロダクトの改善に繋げています。 オーナーさんからのフィードバックの例 BASEでは こちらの記事 の通りCS対応を当番制で運用しています。CSチームからエンジニア向けに調査依頼が投稿されると、自動的に過去の似たようなお問い合わせを拾ってきて、調査のヒント度案内してくれるbotも構築されています。 CS_q_bot ⑦情シスにおけるSlack活用例 社員からのヘルプデスクをワークフローを使って受け付けています。 ヘルプデスクの例 その他の活用事例 情報セキュリティ事象・事項の報告をワークフローにて受付 翌営業日に物理出社するかのアンケート(リモートワーク期間中の運用) ⑧その他の活用例、Slackの便利機能 Slackには無数のアプリや便利機能がありますが、ここではBASEが利用している便利機能の一部をご紹介します。 Unipos連携・・・BASEではピアボーナスである Unipos を導入しているのでSlack上で感謝の気持ちを送り合うことができます。 Colla連携・・・ Collaアプリ を利用すると、毎日botがメンバーにインタビューをし、指定したチャンネルでメンバー紹介をしてくれます。お互いのことを知ることができコミュニケーションの円滑化に役立ちます。 新規チャンネルを通知・・・Zapierというアプリを利用すると、スペース内で新しいチャンネルが作成されたら通知することができます。新しいチャンネルの存在を見過ごすことも減りますね。 新規絵文字を通知・・・同じくZapierを利用すると、新しく絵文字が追加されたら通知することができます。 話題のツイートをSlackへ通知・・・例えば新しい機能をリリースした際に、Twitterで反応があったらSlackへ通知するようにしています。こちらは3月に Amazon Pay 対応をした時の反応ツイートの通知の例です。 リアクションで任意のチャンネルにメッセージをコピー・・・「 リアク字チャンネラーアプリ 」を使うと、パブリックチャンネルに投稿されたメッセージにリアクションすることで、別のチャンネルにメッセージをコピーすることができます。例えば「これはあのチャンネルの話題だ」と思った時にリアクションすることで、話題の集約といったことができます。 おわりに いかがでしたでしょうか。おそらくご存知のものもあれば、「こんな使い方があったのか!」という発見もあったのではないでしょうか。手作業でやっているルーチン業務などもSlackの機能をうまく使うことで自動化できたり業務改善ができるかもしれません。ぜひチャレンジしてみてください! お知らせ BASE 株式会社では、「BASE」の開発を一緒に盛り上げてくれる仲間を募集しています。 一緒にかっこいい開発チームを作りましょう! ぜひよろしくお願いします! open.talentio.com
こんにちは!! BASE株式会社 SRE Group Managerの富塚( @tomy103rider )です。 以前、以下のSREの求人票についての記事を公開してから多くの方とカジュアル面談でお話をさせていただく機会が増え、カジュアル面談の中でも「求人票ブログの記事見ました!」という声をいただき嬉しい限りです。ありがとうございます。 devblog.thebase.in そんなカジュアル面談の中でよくいただく質問があり、今回はその中から最近のSREチームについて、 チームで大事にしていることは? チームの働き方は? チームの業務は? この3つについて紹介したいと思います。 チームで大事にしていることは? 現在SREチームでは、 「信頼性=ユーザの期待値を超え続けること」としてこれを維持し続ける BASEの機能等々の価値を高めるための時間を多く作っていけるようにする が大事であると考えています。 この2つの言葉は、以前チームのミーティングの際に、 「そもそもBASEのSREチームとして今の時点ではどういうチームを目指しているの?」 「BASEのSREは何を大事にしているのか?」 という話題になり、「自分たちが考えるBASEのSRE像とは?」をチーム内で持ち寄って話し合った結果を言語化したものです。ちなみにBASEのSREチームから見たユーザとしては、前者は「オーナーズ(ショップオーナー)とカスタマー(購入者)」の方々、後者は社内でサービスに関わる(SRE含む)人たちを指していて、BASEに関わる全てのユーザ対しSREとしての活動をしていくことが大事であることを表しています。 チームの働き方は? 昨今のコロナ禍ということもありBASEのSREチームにおいても現在は基本的にリモートで業務をしています。そんな中で、日常的なコミュニケーションとしては基本的にはSlackでテキストコミュニケーション、ミーティングはZoomで行っていて、最近はSlackハドルミーティングも活用してより気軽に音声だけでのコミュニケーションを取るようにもしています。 またSREチームの1週間のイベントとしては大きく2つで「朝会」と「SyncSRE」があります。 朝会 - 月〜金 12:00〜12:30 - リリースや大きな作業などのカレンダー確認など - 各自の進捗 - 共有/相談タイム 朝会の様子 朝会はタスクを進めている中での困りごと/相談の時間を特に大事にしていて、より話を掘り下げる必要があるときは30分を超えることもあります。また当然ながら何かあればZoomやハドルミーティングで朝会以外でも都度相談しあっています。 SyncSRE - 隔週金曜 17:00〜 - フリーテーマでチームみんなで会話をする時間 SyncSREは朝会とは異なり、特に議題は決めず主に最近起きたことや感じたことなどをざっくばらんに話してチーム内の認識や意識を合わせる時間にしています。これを始めたのは、リモートワークが始まる前は日常的に自然発生していた会話がリモートワークとなり減ってしまっていることに危機感を感じ、このような時間を意図的に作りました。チーム内での意識/認識をSyncする良い時間になっていると思っていて、実際に1つの事例として最初に書いた「チームで大事にしていることは?」で紹介した2つの言葉を話し合うキッカケになったのはこのSyncSREでの話題からでした。 チームの業務は? ここについては色々ありますが、今回は最近注力している業務や各開発プロジェクトとの関わり方について簡単に紹介したいと思います。 トイル削減/サービスの運用効率化 現在BASEのサービス拡大に伴ってインフラリソースの作成/変更/運用の機会が増え続けており、日常の運用負荷であったり環境の再現性の担保という点において課題があると認識しており、今後の様々な改善を加速していくためにもここを最優先で改善する必要があると考えています。そのため、今更ではありますがInfrastructure as Codeの実践というところでインフラ環境のコード化+断続的な構成レビューのフロー整備を試行錯誤しながら進めているところです。このあたりはある程度形ができた段階でチームのメンバーがブログを書いてくれることを期待しています。 他チームや開発プロジェクトとの関わり 他チームや各開発プロジェクトとの関わりという点では、例えば必要となるAWS上のインフラに関わる業務があります。ただ、単に機能要件を満たすだけの作業をするだけではなく、各開発プロジェクトにおいて機能要件以外に「非機能要件」という観点でもっと事前に考慮しておくべきことはないか?など、リリース後に小さく積み重なっていくと重い課題になりがちな懸念点をSREからも伝えていけるようにしています。 まとめ いかがでしたでしょうか。少しでもBASEのSREチームの雰囲気が伝わればと思います。 さて、引き続きSREチームでは成長するサービスを一緒に支えていっていただける仲間を募集しています! BASEのサービスとSREチームに少し興味が出てきたのでもっと話を聞いてみたい! 似たような試行錯誤をしているのでカジュアルに話してみたい! サービス成長とチームの成長に対して一緒にチャレンジしてみたい! など、少しでも興味を持っていただけましたら、悩み辛みの情報交換でも構いませんし、ここに書ききれなかった話など、ざっくばらんにカジュアル面談でお話ができればと思いますので、お気軽に以下までご連絡いただけると嬉しいです! → カジュアル面談はこちらまで! ← ※参考 open.talentio.com 余談 まとめを書いたあとに余談ですが、最近以下の記事を読みました。 「これでよいのか: SREチームの成熟度評価について考える」 cloud.google.com ここに書かれている「このグループはこの職務をSREと呼ぶために必要な原理と実践の適用において十分なレベルに達しているでしょうか?」という問いに対し、記事内の4つの原理と自分たちがSREとして進めていっている活動を照らし合わせると、方向性としては間違っていないとは思いつつ、実際SREとしての振る舞い/行動ができているかというとまだまだであろうという感想を持ち、この記事については近々SyncSREで改めて話題にしてみようと思います。
はじめまして、フロントエンドエンジニアの @rry です。 BASE では社内勉強会として「 Frontend Weekly LT 」を毎週開催しています。 今回は Vite 特集で LT をしたので、Frontend Weekly LT の紹介も兼ねて内容を発信していきたいと思います。 Frontend Weekly LT とは BASE はフロントエンドエンジニアが、業務委託の方も合わせて約20名ほどいます(7/16現在)。 普段は別チームに分かれており、プロジェクトもそれぞれ別で開発を進めているのですが、この勉強会ではそんなフロントエンドエンジニアが一堂に集まってわいわい LT するイベントです。 LT イベントは基本的に毎週2名ずつ登壇して、 新しく BASE でリリースされた機能の紹介 いま気になっている&プロダクトで使用している技術について 自己紹介 LT などなど、毎回さまざまな内容で LT しています。 7/16 は Vite 特集の LT 会 Vue.js v-tokyo オンライン Meetup#13 - connpass 最近の v-meetup でも Vite 特集があったりと、最近 Vite がとてもアツいです。 BASE ではまだプロダクションでの利用はありませんが、Vite v2 が Vue や React などライブラリに限らず利用できるというのもあり、期待を寄せているビルドツールです。 今回の LT 登壇者は @rry と @gatchan0807 がそれぞれ発表する流れでした。 次世代のフロントエンドビルドツール Vite 登壇資料はこちらです。 最初は Vite x React で何か作ってみようと思っていたのですが、Vite について調べていくうちにそもそものビルドツールの特徴についてのほうが気になってしまい、結果としてこのような内容の LT になりました。 登壇してみて Vite のドキュメントを改めて全部読んだり、Vite に関係する記事をいくつか読んだりできたので良かったです。 今回参考にした記事: https://speakerdeck.com/kazupon/native-esm-powered-web-dev-build-tool https://speakerdeck.com/kazupon/vue-with-vite https://ics.media/entry/210708/ https://tech.recruit-mp.co.jp/front-end/post-21250/ Frontend Weekly LT はみんなの思い出作り 最近のテックブログで @fshin2000 さんがこんなことを書いていました。 そもそも、オンラインコミュニケーションにおいては何が足りないのだろうか?と思っていたのですが、一言でいうと、 良い思い出を作るイベントが圧倒的に少ない ではないか?という事を考えています。 リモートワークの弊害は職場でのよい思い出が作りにくいこと - BASEプロダクトチームブログ この Frontend Weekly LT は今年の 1/15 から始まった取り組みです。 コロナ禍でコミュニケーションがオンラインになる中、こういったみんなで集まってわいわいできるようなイベントを毎週開催するのは、良い思い出作りになっているのではないかなと考えています。 これからも継続して Frontend Weekly LT を盛り上げていきたいなと思います! 💪 おわりに いつものやつですが、BASE では一緒にフロントエンドを盛り上げていく仲間を募集しています。 Vue や React など新しい技術で EC サービスを支えていきたい!という方のご応募お待ちしております ☺ 1.BASE_フロントエンドエンジニア - BASE株式会社
Owners Experience Backend Group で Engineering Manager をしています、炭田( @tac_tanden )です。 BASE では決済などの複雑な事業ドメインに立ち向かうために、ドメイン駆動設計(DDD)を使った開発を進めているチームがあります。 そんな中で先日、DDD の有識者として松岡幸一郎( @little_hand_s )さんをお招きし、DDD モデリングデモ会社内で開催しました。 その模様をレポートします。 開催の経緯 BASE では日頃から DDD に関する質問ができるように、松岡さんを Slack チャンネルにお招きし、開発や DDD の勉強会で出てきた困りポイントや疑問を質問させていただいています。 そんな中で「DDD の勉強をしているけれど、モデリングを実際にどうするのか見てみたい」、「自分でモデリングにトライしてみたけれど、このやり方でよいのかいまいちピンとこない」などの声が上がっていました。 なので、実際にモデリングをどうするのかのイメージを掴み、今後 DDD での開発に取り組む際のモデリング作業に活かせるよう、この度 DDD のコーチとして日頃お世話になっている松岡さんをお招きして BASE の決済領域のモデリングデモ会を開催することになりました。 参加者 BASE の決済領域のモデリングということで、エンジニア以外のメンバーにも有意義なのではと思い、Slack の #general にて開催の告知をしました。その甲斐あって、当日はエンジニアメンバー以外にも、プロダクトマネジメントチームやデザイナーチームのメンバーの方たちにも参加していただくことができました! Zoom での開催だったので、気軽に多くの方が参加できたのもあり、夜の遅い時間だったのですが当日は 40 名近くの方に参加いただきました。企画者として嬉しかったですね。ありがとうございました。 当日 デモ会当日は、BASE からは長年決済領域の開発に携わってきた Payment Group のエンジニアメンバーがドメインエキスパートとして参加しました。 ドメインエキスパートの知識をもとに、松岡さんが画面上にモデリングをしていくのですが、複雑だと考えていた決済領域の関係が次々に明らかになっていく過程は壮観でした。 当日のデモによって出来上がったドメインモデル図をブログ上でお見せできなのは残念ですが、各概念の関係性が分かりやすく表現されていて、これがあれば実装に入れそうだなと自然に思えるような完成度で驚きました。 また、モデリングの過程も、具体的にどういう手順でモデリングをしているくのかステップバイステップで説明/デモをしていただいたので、終わったあとには「自分でもできそう」と思えるようになりました。 2 時間という短い時間でしたが、松岡さんやドメインエキスパートの方の協力もあり、とても有意義な時間になりました。ありがとうございました。 参加者の感想 エンジニアメンバーのお二人の感想をお届けします。 ドメインエキスパートとして参加 今回ドメインエキスパートとして参加した Product Dev Division 所属の島田です。 脳内にあった抽象的な全体像をドメインモデリングを行ったことでより具体的にできました。 そのおかげで概念を言語化したり図にすることができ自分以外の人でも共通認識できるようになりました。 共通認識ができるようになるとドメインに対して色々な意見が出るようになりドメインの精度が上がったように思いました。 聴講者として参加 同じくProduct Dev Divisionに所属している 大津 です。 今回のドメインモデリングを見て、「ドメインモデリング自体がドメインを理解する行為である」ということが腹落ちできました! 当日の Slack の一連の流れを見ても今回のテーマである BASE の決済に対して理解を深められた参加者が多く、現状の認識のすり合わせとしてドメインモデリングが有効ではないかと感じました! 参加者で記念撮影 最後に 改めて、モデリングデモ会を通じてモデリングとはこんな風にやるのかと具体的なイメージを持つことができました。講師としてモデリングのデモを行っていただいた松岡さん、本当にありがとうございました! BASE では複雑な事業領域に正面からタックルしてユーザーのためにサービスを一緒に開発してくれる仲間を募集しています! まずはカジュアル面談でお話できることを楽しみにしております!よろしくお願いします! open.talentio.com open.talentio.com
こんにちは。BASEの藤川です。 緊急事態宣言も続く状況下で、当社もリモートワーク(Work From Home)中心の仕事の進め方をしています。ネット系企業は、幸いにしてVPN、Slack、GitHubやドキュメント管理ツール、その他仕事に必要なSaaSやZOOMがオンライン化しているため仕事の作業そのものは、それほど違和感なく自宅からでもできているのではないかと思います。 でも、仕事というのは作業だけで済むものではありません。業績を上げるための作業を生み出す活動を始めとする考えるタイミングであったり、不確実なものを埋めていくためにお互い議論するタイミングなど、曖昧なプロセスの先に、決定をして作業の的を絞り込んでいくプロセスが不可欠で、ここで複数人のチームワークが不可欠です。 今、一緒に仕事をしている仲間においては、コロナ以前から社内で人間関係を構築済みの人と、コロナ禍においてリモートだけで人間関係を構築している人と二分されています。当社の採用活動でも緊急事態宣言が出てからは、オンラインコミュニケーションのみで内定を出しておりますた、他部署の方などで最短で退職されてしまうと、せっかくそこまで大きくない会社にも関わらず一度も会うことなく去ってしまう人もでてくる可能性があるという状況です。 そのため一日でも長くいただくために、入社される方へのオンボーディングおよび心理的安全性、チームの信頼関係の構築に対するケアは、マネジメントにおいても最重要項目として常に議論をしています。 そもそも、オンラインコミュニケーションにおいては何が足りないのだろうか?と思っていたのですが、一言でいうと、 良い思い出を作るイベントが圧倒的に少ない ではないか?という事を考えています。 オンラインコミュニケーションで起きていたことが職場でも起きる時代 僕は高校一年生の頃からパソコン通信をやっており、オンラインのコミュニケーション経験は長いです。オンラインではパソコン通信でも、その後のインターネットの世界においても、新しいネットワークが大きくなる時に、必ず見かけてきた風景として、 オフ会によって人間関係の距離が変わる オフ会を開催して、誰かと誰かが仲良くなって重鎮化していき、ある種の既得権みたいなのが生まれ、また新しい人が入ってきて新しい関係性が生まれる、常に人間関係のシャッフルが生まれ、コミュニティの歴史が長くなってくると、必ず起きる問題が、 新参者が入れない問題 というのが起きます。 よく組織設計の話で、ダンパー数というものが語られることがあります。 ・5~9人=「社会集団(クリーク)」…最も親しい友人やパートナーの数 ・12~15人=「シンパシー・グループ」…ほぼどのような状況下でも心から信頼できる人の数 ・30~50人=「一団(バンド)」…危険な国を安全に往来できる小さな団体 ・150人=「フレンドシップ・グループ」…共同体の中で一緒に暮らすのに最適な人数 ・500人=「部族・種族(トライブ)」…出会うと会釈する程度の顔見知りの人数 ・1500人=「共同体(コミュニティ)」…人間の長期記憶の情報数の限界、頭の中で名前と顔が一致する人数 出典:チームを成功へ導く魔法の数字たち 「7」「30~50」「150」 https://www.sankeibiz.jp/smp/workstyle/news/190401/wsa1904010700003-s4.htm ダンパー数は組織の人数規模を考慮する時に参考にする数字なのですが、肌感覚としても一致はしていて、小規模のコミュニティにおいては、お互いの距離感は均等に短く、何もしなくても親密になりやすいです。これが人数が30〜50人であるとか、100人を超えてきて、なおかつ参加したタイミングがバラバラともなれば、相互の距離感が不均衡になりがちです、そして、不均衡が生まれれば生まれるほど、既に存在する人間関係や情報の非対称性の中で孤独感を感じる人が出てきます。 特に何十人の規模のオフ会ともなると、人脈を持つものと持たざるものの差は大きく、また、それを切り開くだけのコミュ力や、そのコミュニティで名を売るんだぞ!という根性を持ち合わせていないと、家に帰った後にどっと疲れて、このコミュニティは内輪ばっかりで、疎外感に満ちあふれていたなどの言説が駆け巡ってトラブルになることを、この目で散々見てきました。 こう言ったことが、SlackとZOOM主体のリモートワークの働き方においても適用されるのではないかと考えています。 以前入社した100人ぐらいの会社が、実は1000人以上のグループの一社だったということを入社日に参加したグループ全体の定例会議で知ることになり、なんか大企業に来ちゃったなぁということに疎外感を得たことがあります。自分のアクセス可能なネットワークと、手の届かない全体感の規模感のギャップというのはストレスを感じるという経験をしたことがあります。 そのような会議の後に、心の支えになるのは隣の席の同僚だったり、同期入社した人だったり、即座に小さなコミュニティを構築し、雑談をすることが心のクッションになったりします。 しかし、コロナ禍においてフルリモートのような状態で働くと、自分の部屋でパソコンを閉じても、隣の人と雑談をして共通見解などの意見を交わして、息を抜く所がありません。まして真面目に自粛していて、外にも全く出歩かない独身の方だと、プライベートのネットワークが補完することもできず、どうにも仕事モードを発散できないような人もいるのではないでしょうか。 職場で「何も考えずとも」得られていたもの そういう時に起こりうる疎外感や寂しさと言った無用な感情を埋めてくれる存在は、同じ部署のメンバーやプロジェクトのメンバーなのですが、まだ人間関係ができていないと難しいですし、仕事の中で培われるというよりは、一緒にランチに行く時に路上やエレベーターで会話するシーンや、タバコ部屋の議論とか、仕事の隙間に培われていいた何かというのは確実にあったような気がします。 このようなタイミングで培われるものを「遊び」と定義してみます。 仕事は何かの目的を持って、無駄なく最適化された活動をすることが生産性が高いと表現されます。それが有能な会社員の行動であれば、無駄を許容することはその逆になるので、一人でやるのは難しいです。 Slackで雑談するにしても、相手にしてもらえる人がいればこそ。まして、一緒にタバコを吸いに行くとか、コーヒーを飲みながら雑談するとか、仕事終わりに飲みに行くなど、最適化された活動以外のものは、概ね気の合う数人レベルのマイクロコミュニティとして行われるものではないでしょうか。 ZOOMの活用で、会議室の移動は一瞬でできるようになり、仕事そのものは大変効率化されました。時間ギリギリまで会議していても、10秒後には次の会議室に入ることができ、我々は仕事マシンとして仕事をし続けられるようになりました。その一方で、移動のタイミングで生まれていた、アジェンダのない会話 = 「遊び」は消え去りました。そこで副次的に得られていた相手との心理的安全性や、仕事をする意義みたいなものを学ぶために、実は大切なものではなかったのか?というのが仮設になります。 更に、この会社で働いてよかったこと、自分の成長につながったなぁという手応えは、なんだかんだと新機能のリリースイベントであったり、会議室での議論でのお互いの顔の表情であったり、ホワイトボードに書いたものという手触り感、プロジェクトの打ち上げでの会話、メンバーの笑顔など視覚情報によって脳内に記憶が定着され、それ故に実感を得られていたのはないか?と思うことがあります。 この思い出こそが、10年後20年後に振り返った時に脳の中に残っていることが人生にとって大切なのではないか? 残念ながら、これはZOOM上のバーチャル背景の集合体では得られにくいのではないでしょうか?この1〜2年で働いていたことは人生の記憶やストックとして積み上がるのだろうか。 このような実感を得られるイベントを、オンライン主体の変わりなき日常風景の中で、どうすればみんなが実感できるのだろうなぁというのを常に考えています。何か工夫をされている会社さんやマネージャの方がいたら是非教えてほしいです。 オンラインで「仕事の思い出」は生み出せるのか コロナ以前であれば、合宿、社内イベント、飲み会などは、それらを補完し、思い出を業績に変えていく装置だったように思えます。打算的に言うと、あえて遊びを作ることで、仕事の生産性を生み出す装置であったという見方ができます。 組織を語る表現において、歯車というのはネガティブな表現とされますが、歯車と歯車の間には遊びは必要だし、それらが適切に回って全体が駆動されなければ、そもそも良いプロダクトは作れないのです。 こう言った遊びの構築を、それをオンラインでは補完できるのだろうか? 間違っても、その処方箋はZOOM飲みではないなとは個人的な感覚としてはあるのですが、無駄や行間をあえて作って、そこから何かを生み出していくというのは、全員に高度なネットリテラシーが問われるようにも思えます。 もしかしたらヒントは、ツイキャスや17LIVEのようなライブ配信の関係性にあるのかもと思わなくもないのですが、とりあえず今回は、Youtubeの動画を作ってみて、こういう考えを深めるきっかけを作ってみました。 こちらの動画は、採用候補者の方にBASEを検討する際に見ていただきたい動画として、BASE社におけるPHPの現在・過去・未来というタイトルで作っています。今回は、当社の社員として活躍いただいているyakkunへのインタビューという形で収録したのですが、更に、最後のサイドトークに今回のリモートワークについての話にも言及しています。 www.youtube.com 今後も当社のメンバーとこのような会話を公開用のコンテンツとして作っていくことで、1on1とはまた違った思い出が作れないかと思って作っています。全社員できるかな?! 今回の内容は「最近は他の言語がメインにしてきた人がBASE社でPHPを使う気持ち」という長く続くことに成功したサービスに携わる人達であれば共通の経験がある話をしています。 ラジオ感覚で聞ける話ですので、もしよかったら是非見てみてください。
こんにちは。Product Dev Divisionに所属している 大津 です。 PHPカンファレンス2021 のトーク募集が始まりましたね。 僭越ながら、私は過去にPHPカンファレンス 2019 と 2020 に2回登壇したことがあり、今年もトーク応募をしてカンファレンスを盛り上げていこうと思っています! そこで今後登壇する人のお力に少しでもなればと思い、トークするまでの流れを私なりにまとめてみました。 ぜひ参考にしてみてください! 話したい内容を軽く決める まずは話したい内容をなんとなく決めます。 これは普段から思っていることでも良いですし、社内向けドキュメントとして書いた内容でも良いです。 はたまた、誰も話してなさそうなテーマだから自分が話してみよう!みたいな動機でも良いです。 大事なのは自分がトーク準備の過程を楽しめそうなテーマであるか?ということです。 ターゲットを決める どんな人に向けてトークをするのか具体化しましょう。 例えばWebエンジニアがターゲットだとしても、初心者〜上級者、フロントエンド〜バックエンドなど色んなセグメントがあります。 私はトークを聴く人のペルソナのようなものを、なんとなく作っています。 どのくらいの知識を持っているか どんな課題感を持っているか 自分が話したいことを知ることでどんな状態になるか 例えば私が過去に話した PHPerのためのテストコード入門 では、以下のようなペルソナを考えていました。 参考にしてみてください。 ex.「PHPerのためのテストコード入門」ターゲット1 職業プログラマなりたての新人エンジニア テストコードを言われるがままに描き始めているが、テストコードを書くモチベーションが湧かない 発表を聞いてテストコードを書く理由を知ることで、意味があることをしているんだとモチベーションを高めたい ex.「PHPerのためのテストコード入門」ターゲット2 そこそこコードを書いているエンジニア とりあえずテストコードを書け!と言っているが、何故テストコードを書かなければいけないんですか?と言われると言葉に詰まる 発表を聞いてテストコードを書く意義を伝えられるようになりたい ターゲットに合わせた登壇内容を決める ターゲットが決まったら、ターゲットの達成したい目的を果たせるような登壇内容にしてみましょう。 例えばターゲットが職業プログラマなりたての新人エンジニアであれば、一部のマニアしか分からない内容を書く必要ありません。 逆にベテラン向けの話であれば、初歩的なことはある程度省いても良いでしょう。 この段階で、話すこと話さないことが決まるので、トークの最初に話すこと話さないことをスライドに載せても良いかもしれません。 プロポーザルを出す トーク内容が決まったらプロポーザルを出しましょう。 プロポーザルとは、自分が話すトークの概要です。下記のようなトークの募集が始まった際に、プロポーザルを出します。 PHPerKaigi 2021 PHPカンファレンス 2021 例として僕が今までに出したプロポーザルのリンクをいくつか貼ります。 拙筆ではございますが、どれも採択されたプロポーザルです。参考にしてみてください。 テストピラミッドを意識したテストコード実装戦略 PHPerのためのテストコード入門 リーダブルコミットのすゝめ よくある採択されないプロポーザルについては、こちらのスライドが良くまとまっているので読むと良いでしょう。 プロポーザル・アンチパターン - Speaker Deck プロポーザルの構成についてはこちらのスライドが参考になります。 登壇の可能性をあげる!カンファレンスプロポーザルの書き方のススメ - builderscon::blog また、過去に採択されたトークを見て分析するのも良いでしょう。 トークの採択をするのはカンファレンススタッフですが、スタッフも人間である以上カンファレンスが盛り上がるかということやそれぞれの好みを軸に選ぶはずです。 例えば、10月に開催されるPHPカンファレンス 2021では、採択に関するルールで 「採択者はPHPプログラマー歴が長い人から2年程度の短い人まで、過去登壇してくれた方を中心にお願いしております。」 と書いてあります。 余裕があればこの辺りも見てみましょう。 トーク内容と構成を練る 無事採択されましたら、いよいよトークの内容を考えていきます。 私からトーク内容を練る上で、重要な点をいくつかまとめます。 十分に時間を割こう 目安ですが、私はスライド作成や発表練習などトークの準備時間を最低でもトークする時間 × 120分用意しています。 例えば、25分の発表であれば、25分 × 120分で50時間準備時間に費やしています。 ※LTはこの限りではありません 気をつけて欲しいのは、ここにはデモなどの環境を用意する時間は含まれていません。 作成する内容に合わせて、追加で時間を設けると良いでしょう。 想定外を考慮する 資料を作っていると、以下のようなことに陥ることがあります。 プロポーザルに書いた(想定していた)内容と違う点を見つけてしまった 調べれば調べるほどよく分からなくなり、何もわからんという状態になる 今伝えたいことが、今回のトークの時間だけでは足りないことに気づいた この辺りはプロポーザルを書いた時点ではわからない不確定要素です。 できるだけ不確定要素は取り除いた方が良いものの、想定外はあるという前提で資料作成のスケジューリングをした方があとあと自分が楽になるかと思います。 1回トークのレビューをしてもらう 資料がある程度完成したら、一度他の人の前でトークしてFBをもらいましょう。 誤字脱字などがないか スライドに書いてある内容が本当に正しいか 聞いている人が理解しやすいような構成、流れになっているか トークの時間に間に合う分量か 喋りのスピードは適切か 一番良いのは、トークのターゲットにあたる人と今回のトークテーマについて詳しい人へレビューをお願いすることです。 企業に所属している人は、スライドのレビューもしてもらう スライドは、トーク中だけでなくトーク後のことも考えて作成しなければなりません。 基本的にトークで使ったスライドは、発表後も多くの人の目に触れます。 カンファレンスがトークの録画をしていて、後日カンファレンスに参加していない人や別のトークを聴講していて見れなかった人が動画を見る 公開したスライドのURLが拡散されて、カンファレンスに参加していない人がスライドを見る 内容が企業にとって不都合がないか、広報へ確認の依頼を出しましょう。 また企業によっては、指定したスライドテーマを使うよう指示されることもあるかと思います。 その際には、作成したスライドが企業のブランドポリシーに反していないかのチェックをお願いしましょう。 カンファレンス当日にトークをする いよいよカンファレンス当日です。 トークをするにあたって、考えておきたいのは質疑応答です。 事前に「こんな質問が出そうだから、その時はこう回答しよう」とかある程度考えておくと良いでしょう。 また、答えられない質問やいわゆるマサカリが来たときは、素直にわからないということを伝えましょう。 この辺りの話は、以下のスライドへ譲ります。 LT・登壇でマサカリに備えるたった一つの心得 トーク終了後 登壇お疲れ様でした!トーク後にぜひやってほしいことが2点あります! スライド共有サービスにアップロードしよう トークで使ったスライドを、slideshareやspeakerdeckなどでアップロードしましょう。 スライドのURLをツイート等すると、トークを聞いてくれた人だけでなくトークを聞いていない人の目にも触れるチャンスになります。 参考:大津のspeakerdeck また、自分が登壇した内容のスライド一覧は、自分のポートフォリオの一部として活用できます。 ぜひ自身のブランディングのためにもアップロードしていきましょう。 懇親会にはできるだけ参加しましょう 懇親会では、スピーカーは比較的声をかけてもらえやすい立場にあります。 話す内容はトークのFBだったり、質疑応答だったり…トークした内容について貴重なご意見をもらえるチャンスでもありますので積極的に参加しましょう。 ただし体調不良だったり先約がある場合は、そちらを優先しましょう!(無理せず!) 前日まで徹夜でスライドを作っていたりすると、この懇親会がしんどくなり良いチャンスを逃してしまうかもしれません。 懇親会の参加も含めて、余裕を持ったスライド作りのスケジューリングをしましょう。 最後に トークするまでの流れについてご紹介しました! ちなみに弊社のSlackでは、外部カンファレンスへの登壇についてワイワイする #iikanji-conference-toudan というチャンネルがあります。 今後も外部カンファレンスへの登壇に向けてワイワイしていきます!
この3ヶ月で行ったBDIの内容を紹介します こんにちは、デザイナーの河越です。 BASEのデザインチームが月2回行っている社内勉強会「BDI」。リモートワークになってからも継続的に開催しています♪ 今回は4月~6月に開催したBDIの内容をご紹介したいと思います! BDIとは? 『BDI』は「BASE Design Inspiration」の略。 2018年の秋頃から活動している、デザイナーがやりたいことを持ち寄って、 デザインに関する幅広い知見をみんなで楽しく学ぶことを目的とした任意参加の社内勉強会です。 BASEのデザイナーであれば、デザイナーだけでなく誰でも参加することができます。 Inspirationの名の通り新たなひらめきにつながる新しいトピックを取り上げることも多くあります。 1月~3月の振り返りはこちら devblog.thebase.in BASEのデザイナーがどんな活動をしているのか気になっている方に読んでいただきたいのはもちろん、 リモート環境での社内勉強会のネタ探しにもぜひご活用くださいね! 4月 発想メソッド「マンダラート」でアイディアを湧き出させる会 4月最初のBDIは、アイディア出しのフレームワーク「マンダラート」を実践で使う企画からスタートしました。 マンダラートとは、一つのキーワードに対して8つのマス目を作り、そのマス目一つ一つにアイデアを書き込むことで目標を達成するためのアイディアを見つける手法。 あの大谷翔平選手も目標設定のために使っていて、現在注目が集まっているフレームワークの一つだそうです⚾️(私は初めて聞きました!) BDI用にアレンジしたマンダラートでは、デザインリサーチPJメンバーのショップオーナーインタビューの報告をもとに、マンダラートを使って『オーナーズが持っている課題に対してどのような解決策があるのか』をアイディア出ししました! デザイナー以外も参加して楽しめるように、Miroを使ってワイワイと。 最終的には出てきたアイディアを発表し合うことで、オーナーズへの知見が溜まるだけでなくメンバーへの理解も深まった会でした。 社内で「ものづくり」をしている人のお話を聞くLT会 「今年のGWもおうちにいる時間が長そうだね」ということで、ゴールデンウィーク直前に実施した社内LT会。 おうち時間を楽しく過ごすヒントをもらうべく、社内でものづくりをしている3名の方にお話してもらいました! 3名はそれぞれ、ボードゲーム・ペーパーアイテム・家具を作っていて、 なぜものづくりをしているのか どうやって作っているのか 今後の展望は? と、いう角度からのお話を聞きました。 ものづくりへの熱い思いのこもったLTで「売上をやみくもに上げるより、自分の作品を好きになってもらいたい」「角度にこだわり抜いた座椅子を作って自分の生活を豊かにしたい」といった色々な価値観を知ることができました。 今回の3名だけでなく、オーナーさんが持っている考えとも通じるところがありそうで、ユーザーへの理解が深まる会となりました。 何よりLTをしてくれた3名のこだわりを知れて、作品のストーリーをより深く知ることができ、それぞれの作品のファンになりました...! GWでものづくりをしてみようかな、と考える方が増えたBDIになったかなと思います! 5月 Ownersになってみよう第2弾! ブランドのロゴを作ってみよう🎨 3月に開催した『Ownersになってみよう企画』をシリーズ化し、第1段で制作したブランドのコンセプトを元に、ロゴ作りのワークショップをやってみました。 ロゴはどんなプロセスで作られて、どんな所で使われるかのLT ロゴが使われるシーンをブレストしながら、コンセプトのブラッシュアップ ロゴのラフ案をたくさん作成するモクモク会 2回分のBDIの時間を使って、架空のブランドのロゴやシンボルマークを作成しました。 丁寧なLTがロゴ作成未経験者にもわかりやすい内容で、ブランディングの大切さ/大変さを実感できる回となりました。 フォント決めやロゴタイプのモチーフ決めには議論が活発化して2時間で納めるのは大変でしたが、複数人で集まって一つのMiroボードにモクモク作業する工程で「みんなでデザインするって楽しいな」と改めて感じました。 次回以降の『オーナーズになってみよう企画』ではパッケージや商品のデザインを進めていきます! 6月 「ページ追加 App」リリースまでのあれこれを聞く座談会 5月末にリリースされた「ページ追加 App」について、PJメンバーにリリースまでの道のりを語ってもらう座談会を開催しました。 https://baseu.jp/20203 PJを担当したPMやデザイナーからリリース前に悩んだことの振り返りや、リリース後のユーザーからの反応を聞きました。 機能を利用しているショップの事例を紹介するパートでは、想像以上に工夫して使ってくださるショップを知れてみんなで盛り上がりました! 事前に全社からPJメンバーに聞きたいことを募集して質問を選んでおいたので、より内容の濃いウラバナシを聞くことができたし、発表するメンバーの負担も少なくできたのではないかと思います! デザインツール「Figma」を使いこなそう!!!〜基礎編〜 BASEのデザインチームでも利用しているデザインツール「Figma」。 デザイナー以外のメンバーに向けて、Figmaの基本的な使い方や、Auto Layoutの組み方を学ぶ実践型のワークショップを開催しました。 Figmaマスターの渡邊さんがFigma特有の「Frame」という概念や、「Auto Layout」の組み方を丁寧に説明してくださいました。エンジニアからは「Flexboxに似ていてわかりやすい!」という声も。 資料作成にFigmaを使っているPMや、Figmaでデータを受け取るエンジニア陣から大好評の企画となりました! お互いの仕事についての理解が深まることで、よりスムーズなコミュニケーションが取れるようになりそうです◎ まとめ 月2回ペースで開催しているBDI。4~6月はLTや座談会を多く取り入れてみました。 結果としてデザイナー以外のメンバーもROM専で参加しやすくなり、参加者が徐々に増えて大盛況となりました。 PJやチームが違っても、こういった機会でコミュニケーションが取れる機会があるのは良いなと感じています。また、実況コメントを流しながらのLTなど、オンラインでもLT会を盛り上げる知見も溜まってきたので、参考になったら嬉しいです。 これからも楽しくてためになる勉強会を開催していきます。7月以降もお楽しみに!
こんにちは!! BASE BANK 株式会社 Dev Division にてSoftware Developerをしている永野( @glassmonkey )です。 普段はGo/Python/PHPを主に生業に開発・運用から何でもござれの精神でフルサイクルエンジニアをしています。 現在、自分たちのプロダクトである YELL BANK の分析基盤を構築しています。 その際に、BigQueryで扱っているデータをGoogle App Script(以下GAS)、Googleスプレッドシートとデータポータルで簡易CRMをビジネスサイドのメンバーである猪瀬 ( @Masahiro_Inose )と協力して作ったのでそのご紹介です。 いざ実施してみるとハマってる点もそこそこ多く、意外とGASやBigQueryの連携している情報が少なかったので、誰かの助けになれば幸いです。 thebase.in 簡易CRMツールをスプレッドシートベースで作成した背景 弊社で開発している「YELL BANK」では、プロダクト改善のためにユーザーインタビューなど定性情報を顧客に直接聞く形で得ていくスタイルを積極的に実践しています。 その記録を一元管理する場所がなかったこともあり、CRMツールが必要な状況でした。 CRMツールとしてはSalesforceなどが有名ではありますが、様々なデータがBigQueryに集約をする状況でありました。 今回は以下の2点で慣れているスプレッドシートベースのシステムの採用をしました。 プロダクトとして仮説検証フェーズなので見たい内容は定まってないこと データ分析を主として実施しているビジネスサイドのメンバーが自身でカスタマイズ容易なこと 謝辞 一部改変をしておりますが、コードなどは 【GAS/BigQuery】日付に応じて異なるクエリを実行 から大部分を引用させていただきました。 この場を借りてお礼を申し上げます。 構成 GAS周りの基盤を私が作成して、集計のスプレッドシート構成からダッシュボードの構成まではビジネスサイドのメンバーで分担して作成しました。 簡易CRMの構成図 準備 GASのメンテは基本的には普段コードを書かないビジネスサイドのメンバーが扱うので、都度追加するコードはシンプルである必要がありました。 そこで、OAuth認証に関連するところやAPI経由でBigQueryを実行する処理は共通処理として切り出すことにしました。 GCPプロジェクトの準備 GASからBigQueryをOAuth経由で実行するので、その設定が必要です。 今回は内部的に利用するので、UserTypeは内部を選びます。 0auth同意画面 アプリ名とサポートメールとデベロッパーの連絡先情報を入力します。内部用なので適当で良いです。 Oauth設定画面(メールアドレスなど) BigQueryのAPIが有効になっているか確認します。 このあとAPIを利用するので、もし無効になっていたら有効にしておきましょう。 BigQuery API確認画面 GASの共通プロジェクトの設定 まず最初に共通GASプロジェクト設定します。 GASのメンテは基本的には普段コードを書かないビジネスサイドのメンバーが扱うので、都度追加するコードはシンプルである必要がありました。 API経由でBigQueryを実行する処理は共通処理として切り出すことにしました。 認証用のOAuth設定を予め共通プロジェクトとの連携で行っておくことで、ビジネスサイドのメンバーが権限周りを考慮せずにすむようにもしました。 各種画面のスクリーンショットは2021年6月時点のものになります。 1. 新しいプロジェクトから共通プロジェクトを作成する 新しいプロジェクトを作成します。 新しいプロジェクトその すると下記のように無題のGASのエディターが開きます。 GAS エディター 2. ライブラリを追加する GASからスプレッドシートのアクセスを簡易的にしてくれる、gas-underscoreを入れます。 詳細は simula-innovation/gas-underscore を参照ください。 READMEから"プロジェクトキー"である M3i7wmUA_5n0NSEaa6NnNqOBao7QLBR4j を使うと良いとありますが、新エディターの場合使えませんので注意ください。 github.com 旧エディターを利用する場合 (非推奨) Add a libraryから"プロジェクトキー"である M3i7wmUA_5n0NSEaa6NnNqOBao7QLBR4j を入力します。 バージョンは最新の2を選びます。 なお、この方法はいずれ廃止になる可能性があるのでおすすめはしません。 新エディターを使う場合 左カラムのライブラリの + をクリック gas-underscoreの"スクリプトID"は 1PcEHcGVC1njZd8SfXtmgQk19djwVd2GrrW1gd7U5hNk033tzi6IUvIAV なのでそれを入力する。調べ方は後述します。 ] 調べ方としては、旧エディターで得た情報をベースにはなりますが、インストールするとGASのマニフェストファイルからスクリプトIDがわかるのでそれを利用します。 旧エディタの場合は 表示 > マニフェストファイルを表示 を選択すると見ることができます。 他にもプロジェクトキーしかわからないライブラリがある場合はこれを使うとわかるので便利です。 GASマニフェストファイルの一部 余談ですが、公式のリポのREADMEには一応 Pull Request を出しました。古いリポジトリなのでメンテされてない可能性が高いので、forkした 私のリポジトリ も記載しておきます。 github.com 3. サービスにBigQueryを追加する 今回はBigQueryのAPIを利用するので追加します。 1. 左カラムのサービスの + をクリック 2. BigQueryを選びます。バージョンは2021年6月時点で最新のv2を選択しています。 4. GCPプロジェクトと連携させる 歯車マークのアイコンからスクリプトの設定に遷移します。 プロジェクトを変更からGCPプロジェクトの連携設定を追加します。 ここでうまくいかない場合は、連携先のGCPプロジェクトのOAuth認証が正しく設定できていない可能性があります。 ここのプロジェクト番号とはGCPのダッシュボードのプロジェクト情報で確認できます。 プロジェクト情報 5. GASの共通コードを用意する。 集約したいスプレッドシートのID、シートの名前、実行したいクエリは都度変わるとのでそこを外部から渡せるように共通処理を書いておきます。 共通処理内部でBigQueryへのAPIリクエストを秘匿するようにしていので、ここはビジネスサイドのメンバーは意識しなくて済むようしておきます。 プロジェクト番号のところは、前述したOAuth設定したBigQueryを実行するgcpプロジェクト番号に読み替えください。 function run ( spreadsheetId , sheetName , query ) { /** * コンフィグ設定 **/ const projectId = 'プロジェクト番号' ; // 出力先シートSpreadsheet const sheet = SpreadsheetApp . openById ( spreadsheetId ) ; //出力するシート名 const workSpace = sheet . getSheetByName ( sheetName ) ; const request = { query : query , useLegacySql : false } ; execBigQuery ( projectId , workSpace , request ) ; } ; function execBigQuery ( projectId , ws , request ) { let queryResults = BigQuery . Jobs . query ( request , projectId ) ; const jobId = queryResults . jobReference . jobId ; //BigQuery実行 queryResults = checkOnQueryJobStatus ( projectId , queryResults , jobId ) ; //結果を取り出す const rows = getAllResults ( projectId , queryResults , jobId ) ; //結果を書き出す outputToSpreadsheet ( rows , ws , queryResults ) ; } function checkOnQueryJobStatus ( projectId , queryResults , jobId ) { var sleepTimeMs = 500 ; while ( ! queryResults . jobComplete ) { Utilities . sleep ( sleepTimeMs ) ; sleepTimeMs *= 2 ; queryResults = BigQuery . Jobs . getQueryResults ( projectId , jobId ) ; } return queryResults ; } // Get all the rows of results. function getAllResults ( projectId , queryResults , jobId ) { var rows = queryResults . rows ; while ( queryResults . pageToken ) { queryResults = BigQuery . Jobs . getQueryResults ( projectId , jobId , { pageToken : queryResults . pageToken }) ; rows = rows . concat ( queryResults . rows ) ; } return rows ; } // Clear sheet data and output BigQuery results to sheet. function outputToSpreadsheet ( rows , ws , queryResults ) { if ( rows ) { ws . clear () ; // Append the headers and return header cols. const headers = appendHeader ( ws , queryResults ) ; // Append the results and return all spreadsheet data. const data = appendResults ( rows , headers ) ; ws . getRange ( 2 , 1 , rows . length , headers . length ) . setValues ( data ) ; return data ; } else { Logger . log ( 'No rows returned.' ) ; } } function appendHeader ( ws , queryResults ) { const headers = queryResults . schema . fields . map ( function ( field ) { return field . name ; }) ; ws . appendRow ( headers ) ; return headers ; } function appendResults ( rows , headers ) { const data = new Array ( rows . length ) ; for ( var i = 0 ; i < rows . length ; i ++ ) { var cols = rows [ i ] . f ; data [ i ] = new Array ( cols . length ) ; for ( var j = 0 ; j < cols . length ; j ++ ) { data [ i ][ j ] = cols [ j ] . v ; } } return data ; } 詳細は後ほど記載しますが、各種呼び出しは以下のような形になります。 実際にシートに集計クエリを書くビジネスサイドのメンバーとしては、集計クエリをどこのシートに出力するかのみ考慮すれば良いようにしました。 function example (){ //シートID, シート名, クエリを指定する 共通プロジェクトの名前 . run ( SpreadsheetApp . getActiveSpreadsheet () . getId () , "集計結果シート" , `何か集計用SQL` ) } シートごとの設定編 共通プロジェクトのスクリプトIDをメモ 集計シートからBigQueryを実行できるように共通プロジェクト呼び出しをします。 共通プロジェクトのスクリプトIDをメモします。 スプレッドシートに紐づくGASを開く 集約用のデータを扱うスプレッドシートを用意します。 スプレッドシートの ツール > スクリプトエディタ からシートと紐づくGASを用意します。 スプレッドシートと紐付いているGASはコンテナにシート名の記載があるので、念の為確認するといいでしょう。 ライブラリの+から共通プロジェクトを呼び出しをします。 ここで呼び出しをすることで 共通プロジェクトの名前.run で共通処理を呼び出しできるようになります。 BigQueryを共通処理と同様にサービス設定に記載します。 呼び出し関数を用意する 何かの集計結果のSQLを集計結果シートに出力する場合は下記のように書くことで実行できます。 基本的にはここの実装はビジネスサイドのメンバーが書くことになるので、 集計したいクエリを書くことに専念できるような呼び出し方としました。 また、関数として切り出すとボタン配置などやデバッグがしやすいのでおすすめです。 function example (){ //シートID, シート名, クエリを指定する 共通プロジェクトの名前 . run ( SpreadsheetApp . getActiveSpreadsheet () . getId () , "集計結果シート" , `何か集計用SQL` ) } 呼び出し用のボタンを用意する gasの実行には色々方法がありますが、今回は不要な集計処理を走らせたくなかったのと必要なときに実行できるようにボタンを用意する方式にしました。 適当にボタンとなる図形を 挿入 > 図形描画 から配置します。 図形右上の︙からスクリプトの割当を選びます。 用意した関数名を入力します。 これが完了すると用意した図形を押下すると集計クエリが出力されるようになります。 結果と使用感 Google Data Studioの連携部分に関してはGoogle Data Studioで何を出すかで変わってくるので割愛します。 結果としてビジネスサイドのメンバーが作ってくれたダッシュボードの一部ですがご紹介します。 架空の数字ですが、イメージは伝わると思います。 簡易CRMのイメージ図 当初の目的通り、ユーザーインタビューなどの定性情報を得にいく時にも、顧客とのコミュニケーション記録を有効に活用できるようになったとのことでした。 過去にコミュニケーションしたショップがファネルの上位のフェーズになったといった変化を発見できるようになったとのことです。 感想 データ分析もより専門性のあるビジネスサイドのメンバーに委譲する流れができた点も良かったと考えています。このおかげで全体としては2週間ほどでCRMのシステムを構築できました。 BigQueryをGAS経由で呼び出せるといろいろ楽なので、誰かの助けになれば幸いです。 そのようなスピード感を持って一緒に開発するアプリケーションエンジニアやカスタマーサクセスのメンバーを募集中です。 open.talentio.com open.talentio.com
Owners Experience Backend Group で Engineering Manager をしています、炭田( @tac_tanden )です。2021 年 4 月末に『ユニコーン企業のひみつ ―Spotify で学んだソフトウェアづくりと働き方』という本が発売されました。 自分含め、多くのメンバーが「読んでみたい!」と話題になっていた中で、翻訳者の角谷さまのご厚意で献本いただいたので、遅ればせながらレビュー記事を書かせていただきます。 🦄 テック企業のみなさま、テックブログの記事の候補に一冊いかがでしょうか!!!q 🙏 » 🦄 書籍『ユニコーン企業のひみつ』を貴社テックブログでレビューしていただける企業さまを募集します https://t.co/TdfL58TwpS — Kakutani Shintaro (@kakutani) April 7, 2021 著者について この本の著者を見たとき驚きました。 "Jonathan Rasmusson(ジョナサン・ラスマセン)"。自分が好きで何度も読んだ『アジャイルサムライ−達人開発者への道−』の著者の、あのジョナサン・ラスマセンさんが書いた、Spotify での経験をまとめた本ということで否が応でも期待が高まりました。 実際、読み終わった後はジョナサン・ラスマセンさんがつぶさに観察した Spotify での開発チームや文化が完結にまとめられていて、読んでいてすごくワクワクしました。 この本のテーマ 冒頭の『日本の読者の皆さんへ』の章でジョナサン・ラスマセンさんは以下のように書かれています。 『アジャイルは今や「ふつう」になりました。(中略)本書を『ユニコーン企業のひみつ』と題したのは、ものすごく成功しているソフトウェア企業(Spotify、Amazon、Googleなど)は「アジャイルでいつもやっていること」を超えた先のやり方を見つけ出しているからです。』 (vii 日本の読者の皆さんへ) ものすごく成功しているソフトウェア企業(Spotify、Amazon、Google など)はアジャイルを超えたさらに一歩先を進んでいる。その一歩先を Spotify はどう歩んでいるのか。知りたくないかい?という風に自分は理解しました。 これだけでなんだかワクワクしてきますよね。自分は Web アプリケーションエンジニアとしてずっとサービス開発に携わってきた中で、アジャイルでの開発の有効性について理解しているつもりです(スクラムマスターの資格を取りに行ったくらいアジャイルやスクラムの考え方が好きです)。 アジャイルのその先の一旦を垣間見れるということで、冒頭からフツフツとテンションが上がっていき、ページ数がそれほど多くないコンパクトな書籍ということも相まって、一気に 2 時間ほどで読み終えることができました。 アジャイルのその先 〜 Spotifyモデル 〜 Spotify モデルは自分自身はこの本を読むまで恥ずかしながら知りませんでした。2012 年(約 10 年も前なのですね...)に、Henrik Kniberg と Anders Ivarsson によって "Scaling Agile @ Spotify with Tribes, Squads, Chapters & Guilds" として発表されたのが初出のようです。 https://blog.crisp.se/wp-content/uploads/2012/11/SpotifyScaling.pdf Spotify モデルについて他に詳しく解説されている記事がいくつもあるので、ここではこれくらいにとどめ、自分はこの本の中でいくつか気になった Spotify の開発組織やスタイルについて取り上げたいと思います。 スクワッド 日本語で分隊と訳されることが多い squad ですが、もともとの語源は square(四角)です。昔、銃などの武器がなかった時代に、軍隊では四角い陣形を組んで戦ったことが多かったことに由来するそうです。自分はここから、一致団結して集団としてフォーメーションを組んで開発に立ち向かう姿をイメージしました。 また、この本からスクワッドはチームというよりは小さなスタートアップに近いように感じられました。小さなスタートアップ企業として、与えられた領域の全てに責任と権限を持ち、与えられたミッションを達成するために高速に開発イテレーション(リリースと検証)を回していく。理想のチーム形態の 1 つだと思います。 Spotify ではこのスクワッドが何をするにしても基本となり開発が進むようです。色々なスキルや経験をもった良き仲間とスクワッドを作っていく過程は楽しそうですよね。 ちなみに BASE でも企画・機能の策定から動作確認、リリースまで責任をもって行う複数の役割のメンバーで構成されたチーム単位で機能の開発をしているので、近い部分もありつつ違いも多かったので、スクワッドについて解説している第3章は特に興味深く読ませていただきました。 devblog.thebase.in カンパニーベット カンパニーベットは会社で取り組みたい最重要事項を並べた ToDo リストです。 会社で最もフォーカスしたいものを、全社向けの OKR のような形で表明する場合もありますが、「ベット」(賭ける)のほうが重要度に対して生々しさを感じてネーミングとして、素敵だなと思いました。 また、この章で紹介されている DIBB(Data, Insight, Belief, Bet)も、いわゆる OODA ループ(Observe, Orient, Decide, Act)をより具体化したものになっていて、過程がはっきりイメージしやすいので、チームに取り入れやすいのではないかと感じました。 会社やチームに限らず、時間は有限ですべてのことを完了させることはできないので、どこかでやることやらないことを分ける必要がありますが、カンパニーベットと DIBB は得られる結果がとてもシンプルになるはずなので、面白い仕組みだなと率直に思います。 会社やチームだけでなく、個人でも何かの目標ややりたいことに対して試してみるのもありなのではないでしょうか? まとめ 以上が、簡単ではありますが自分が『ユニコーン企業のひみつ』で紹介されている Spotify モデルの一部です。この本では他にもスタートアップとは何かや、開発組織をスクワッドを使ってスケールさせる方法、Google や Apple などとの文化の比較も掲載されていて、単純に読み物として面白かったです。 また、説明も具体的にかかれているので、気になった部分や仕組みを一部取り入れてみることもしやすいのではないかなと感じました。 注意点 この本の英語版が世に出たのが 2020 年 3 月です。また、ジョナサン・ラスマセンさん自身はすでに Spotify を退職されているようで、この本の原型といえるようなブログポストが 2017 年 11 月投稿されていました。 The Spotify Playbook /* */ agilewarrior.wordpress.com つまり、この本で描かれている Spotify の文化や開発スタイルは少なくとも 4-5 年前のもので、2021 年の今では Spotify は別の開発スタイルになっていてもおかしくないですね。 実際、冒頭で紹介した Spotify モデルを初めてまとめた "Scaling Agile @ Spotify" にはこう書かれていました。 Disclaimer: We didn’t invent this model. Spotify is (like any good agile company) evolving fast. This article is only a snapshot of our current way of working - a journey in progress, not a journey completed. By the time you read this, things have already changed. Scaling Agile @ Spotifywith Tribes, Squads, Chapters & Guilds p.1 この記事を読んでいるときにはすでにやり方は変わっているはず。終わりのない旅なんだ。 進化のスピードへの危機感を持つとともに更に刺激を受けました。また、Spotify でもやはり試行錯誤の連続なんだなと再確認できてよかったです。 最後に 今回、翻訳者の角谷さまのご厚意で献本いただき、拝読させていただきました。 この本がきっかけとなり社内でも開発スタイルや組織についての議論が活発になり、学ぶことが多かったです。改めて感謝申し上げます。 本当にありがとうございました。 お知らせ BASE 株式会社では、「BASE」の開発を一緒に盛り上げてくれるエンジニアを募集しています。一緒にかっこいい開発チームを作りましょう! ぜひよろしくお願いします! open.talentio.com open.talentio.com
こんにちは!アプリチームのEMをしている竜口です! 今回はリモート下でチームのコミュニケーションに課題があったので、それをどう改善していったかを紹介していきたいと思います。 初手、どうありたいかを決める やったこと いきなりxxxを始めます/やってみます!!と言ってもチームメンバーとしては意図、どうありたいかがわからないと、どう行動するべきかわからないと思います。 なので、何の為に改善して、どうありたいんだっけ?って部分を明文化しメンバーに伝えました。 実際はもっと肉付けしたものですが、下記がメンバーに伝えたものです。 1. 改善、課題解決していく 2. 開発をより早く、より確実にしていく 3. チームで開発していく 正直内容としては、特に真新しいものでもなく当たり前なものだと思いますが、今どのようなチームを目指しているのかというのを、メンバーと認識合わせました。 効果 個人的には、これが一番良かったかなと思っていて、この共通認識をもつことでメンバーも意識的に改善に向かっていってもらえたかなと思っています。 ツールに頼ってみる、tandem導入 やったこと tandem を簡単に説明すると A virtual office for remote teams を実現させるサービスで、 リモートで一緒に働く上でチーム感での会話のしやすさの実現、お互いの状態を緩く把握することで一緒のオフィスで働いてる感を醸成してくれます。 youtu.be 最初はアプリチームで導入し、その後アプリ開発に関わる他のプロジェクトメンバーも含めて導入しました。 効果 Slack or Zoomでコミュニケーションとってる時と比べ、ツールとしての会話のしやすさであったり会話できそうな状態か否かがわかりやすいのもあってコミュニケーションは増えていきました。 参考までに実際のtandemの様子ですが、各部屋に誰がいるのか分かるとこや、特定の人が緑色の場合はactive/オレンジだとinactiveという風に話しかけて良さそうな雰囲気も醸成できて話しかけるハードルや他の人の話してる雰囲気が伝わったりします。 朝会の後に意味のない時間を作る やったこと 毎日、朝会と言ってやること/困りごとを共有する場を15分程設けています。 その中で朝会後にすぐ抜ける必要のない運用にしてみました。朝会が終わってすぐ抜けてもいいし、話題がなくてもただそこにいてもいいし、何か特定の人に聞きたいことあれば聞いていいし、雑談してもいい。 効果 雑談が増えたのと、朝会でみんなの時間取るほどではないしSlackで聞くほどではない疑問とかが自然と出てきてよいです。 このルールだとtandemからの抜けにくさが出るかなと思ったのですが、お昼ご飯時なのもあり抜けやすさがあり、そこもメンバーのお気持ち的にも良かったのかなと。 作業タイムを作る やったこと ただただtandemつなぎっぱなしで作業する時間を2時間/1週間入れてみました。 ただ同じ空間で働いてる感を作りたかったので、ルールとしては強制参加だけど話したりカメラ繋いだりは強制しない形でやってみました。 効果 雑談や軽い相談も出てきてよかったです。ただずっとオンラインで繋げることによる集中力下がるというフィードバックがあったり時間や運用の仕方は、まだまだ調整が必要な部分もあります。 またtandemに Crosstalk という機能があり、同じ部屋にいる特定の人と話す時、会話に関係ない人にはその会話が小さくしか聞こえない機能があり、会話の指向性がでて話題に関係ない人のストレスにもなりにくくよかったです。 まとめ 今回は比較的導入が簡単なものをやってみて、以前よりチーム内での会話が増えて、一人で問題を抱え込むような形は改善されたかなとは思います。 以下メンバーの生の声です! - 朝会の後、いま取り組んでる課題とかに自然と移行できていい - Tandemは、いまみてるGitHubのURLとかを自動で取ってきて表示してくれる機能があって、IssueやPRの話などをしやすい - 朝回の前はあえて話すことないかなーと思っていたりするけど、意味のない時間に入ると敷居が下がって話せたりする - Tandemは、いざってとき会話するハードルが低い。ちょっと確認したいけど込み入った問題で確認したいぐらいの温度感のときに会話しやすいのが助かる ただまだ改善の余地はあって、チームとしてプロダクトを成長させる為に勉強会等を今後やっていきたいと思います。 そして!今アプリチームでは一緒にプロダクトを成長させていける方を募集しています! iOS 採用情報 / カジュアル面談 Android 採用情報 / カジュアル面談