株式会社ヘンリー エンジニアブログ

株式会社ヘンリーのエンジニアが技術情報を発信します

はてなさんと合同で技術勉強会を開催しました

株式会社ヘンリーで SRE などをしている id:nabeop です。弊社では技術勉強会1を毎週開催しておりますが、このたび「はてなブログ」などの個人向けサービスや出版社向けマンガビューワの「GigaViewer」、オブザーバビリティプラットフォーム「Mackerel」、発話分析ソリューション「toitta」などを開発している株式会社はてなさんとと合同でクローズドな技術勉強会を実施しました。

当日は双方から発表し日頃の様子などが知れて良い交流ができました。

弊社の参加者の発表内容を軽くご紹介します。

CIを整備してメンテナンスを生成AIに任せる

一條さん(@hazumirr)が個人開発しているツールのメンテナスをAIを活用することでメンテナンスコストを極力かけないようにしている様子を発表しました。

ヘンリーにおける OpenTelemetry のトレース活用の様子

弊社の2つ目の発表は id:nabeop が Henry で OpenTelemetry の分散トレースを扱うにあたり、トレースに属性を追加することで調査などに活用している様子を紹介しました。

非公開資料より。スパンに付与する属性を工夫することで調査がやりやすくなります。

k1LoW/deck の紹介

飛び入りでフェローの id:Songmu さんが Markdown で記述した内容を Google Slides のスライドに変換する k1LoW/deck の紹介をしてくれました。k1LoW/deck で実際にスライドを作成しつつ、最近のコントリビュート内容の解説などしてくれてとても盛り上がりました。

宣伝

2025年7月30日 (水) に QA エンジニア向けのイベントとして「QAの理想を語らNight!」というイベントを開催します。参加者を絶賛募集中なので QA について興味のある人や QA の理想と現実のギャップに悩んでいる人はぜひ参加登録してください。

また、QA 以外でもヘンリーの理想を実現するために手伝ってくれる仲間を募集しています。弊社に興味のある方はぜひカジュアル面談させてください。

CoLab Conf(コラコン)でブース出展しました!

Henry開発者のタケハタ(@n_takehata)です

7/13(日)、ヘンリーはCoLab Conf(コラコン)にてブース出展をしました! 今回はヘンリーブースのコンテンツや写真など、会場の様子をご紹介できればと思います。

CoLab Conf(コラコン)とは?

サポーターズCoLabさんが主催された、「U35エンジニアの生存戦略を考えるテックカンファレンス」です。

supporterz-seminar.connpass.com

今回は「AI」と「キャリア」をテーマに、豪華なスピーカーの方々によるセッションや、スポンサー企業によるブース出展がありました。
ヘンリーも御縁があり、ブースを出展させていただきました。

ブースの様子

ヘンリーのブースの様子です。

スタンプラリーを開催したりと、運営の方でも盛り上げていただいたこともあり、一日中絶え間なく参加者のみなさまに来ていただきとても盛況でした!

医療業界クイズを実施しました!

今回のヘンリーブースでは、医療業界クイズを実施しました! 全問正解された方には、景品として以前技術書典でヘンリーの有志メンバーで販売した同人誌「電子カルテの開発を支える技術 ~ モダンな技術で再発明する ~」をプレゼント。

クイズは以下のような問題をご用意しました。

政府の調査にて、DX(デジタル・トランスフォーメーション)が一番遅れていると言われている産業は次のうちどれ?

- 漁業
- 宿泊サービス業
- 農業
- 医療福祉業

全国の病院に対し、政府は2030年に電子カルテの導入率100%を目指していますが、まだ電子カルテが導入されていない割合は?

- 10%程度
- 20%程度
- 30%程度
- 40%程度

次のうち、法的な「病院」の定義として正しいものは?

- 内科・外科の両方を標榜している医療機関
- 入院設備を持ち、患者を24時間受け入れられる医療機関
- 入院ベッド数が20床以上の医療機関

来られたみなさん興味を持って挑戦してくださって、問題の答え合わせをしながらヘンリーの取り組みについてお話しすることができて嬉しかったです。
クイズを通して普段は馴染みのない方にも、医療業界の抱える課題や、ヘンリー含め医療DXに取り組む事業の意義を知っていただくきっかけになったのではないかと思います。

ブースにお立ち寄りいただいたみなさん、ありがとうございました!

当日ブースにお立ち寄りいただいたみなさん、ありがとうございました!
またイベントとしても大変盛況かつ会場の混乱もなく、運営スタッフのみなさまにも大変感謝しております。

今後も機会がありましたら、ブース出展など含めイベントにも協力させていただければと思っています。

VPoEとして大切にしていることと、2025年上期に実施した施策

株式会社ヘンリーで2025年3月からVP of Engineeringを務めている戸田(id:eller)です。任命から3ヶ月経ったので現状をまとめつつ、今後の見通しなどについてまとめたいと思います。

VPoEとVPoTの違い

私は1月からVP of Technologyも務めており、現在は兼務している状況にあります。本題に入る前にこれら2つがどう異なるか、ヘンリーではどう考えているかを整理してみます。

VP of Engineering(VPoE)はエンジニアリングについてステークホルダーに説明責任を持つポジションであると定義しています。エンジニアリングとは工学ですから、VPoEは工学的アプローチ、特に組織づくりや開発プロセスを見るわけですね。ヘンリーは組織規模を拡大している最中で、またフルリモートを特徴のひとつとしている会社でもあるため、組織づくりや開発プロセスは投資する価値が高い状況です。個々人が裁量を持って自律的に働ける環境と、周囲とのシナジーを活かして組織としてのアウトカムを最大化していける環境の双方を実現することが求められています。

対してVP of Technology(VPoT)は技術についてステークホルダーに説明責任を持つポジションであると定義しています。製品としてのHenryがどう技術的な選択をしてきたか、これからどう対応を進めようとしているか、を見ることになります。Henryは病院向け電子カルテとしては前例のないクラウドネイティブ実装であり、業界に対してクラウドネイティブの強みや運用、セキュリティについての考え方などを継続的に発信していくことも求められています。

ひとことで言えばVPoEが組織体制、開発プロセスを、VPoTがサービスの実装詳細やその仕様を見ると思えばよいでしょう。いずれも製品本部に留まらず全社的に横断した機能として期待されています。直近は次のような活動をしてきています:

  • VPoE
    • 部室長向けオンボーディングないしコーチングの企画と実施
    • 行動規範, 等級制度ないし評価制度の検討
    • 大きくなったstream-alignedチームの分割
  • VPoT
    • お客様に提示するセキュリティホワイトペーパーや仕様書の作成
    • SIerやネットワーク事業者に提示する稼働環境仕様書の作成
    • noteでの情報セキュリティ関連情報の発信

本日はVPoEの部分にフォーカスして、ヘンリーが抱えている課題とその対策について述べていきます。

ヘンリーにおけるエンジニアリングの課題

私は3代目のVPoEですから、先達の学びを活かすことができます。ということで歴史の振り返りから始めましょう。

歴史的背景

私の前にも id:shenyu_cyan さんと id:Songmu さんがVPoEとして活動されています。その背景についてはダブルVPoEについてのブログ記事が参考になるでしょう。

dev.henry.jp

大きく分けて「圧倒的に強い開発組織を作り上げる」「強い開発組織の改善力を会社全体に展開する」の2つのミッションを持っていました。現在のVPoEも同様に双方のミッションを持っていますが、後者の一部は組織開発や経営企画といった他部署でも取り組まれています。このため今回は論点を単純化する目的から、後者は割愛します。

「圧倒的に強い開発組織を作り上げる」ために必要な課題は3つ、採用とロイヤリティ、そしてエンパワーメントです。そして採用が現状もっとも難しいという認識を持っています。

エンパワーメントフライホイール。「仲間を増やす」「プロフェッショナル」「チームワーク」を改善して「価値を生み出す」をくるくる回したい。

採用の強化が最優先課題である

ITシステム開発の世界では「人月の神話」に代表される重要な知見があります。人を増やしてもシステム開発は早くならないということですね。「カップラーメンを3人で作っても1分ではできない」との説明もされます。ですからプロジェクトの進捗が悪いからと言って、いたずらに人を増やすことには慎重であるべきだと考えています。

gihyo.jp

しかしこれを踏まえても、ヘンリーのエンジニア組織はやるべきことに対して小さすぎます。理想ベースで考えると7〜8チームはあってもいいだろという体感がありますが、現状は合計4〜5チームで、まだ足りません。プロジェクトの進みが遅いというよりは、人がいないのでプロジェクトを始められない状況と言えるでしょう。

せめて6チームに増やすことを年内でやりたいのですが、そうするとまだまだエンジニアやDE、QAやデザイナーなどの採用を継続的に行う必要があります。採用だけが解決ではないのは当然で他の施策も並行で進めますが、組織としてできることを増やせるのが採用の良さであり、長期的に投資をしていくべきだと考えています。

行動指針を「あたりまえ」の存在にしたい

ヘンリーでは「理想駆動」「爆速アウトプット」「ドオープン」をバリューとして掲げてきましたが、先日これをブラッシュアップした新しい行動指針が誕生しました。

行動指針を会社説明資料より抜粋

私は行動指針は周囲を巻き込むための素材であり、成長の種であり、ロイヤリティの源泉であると考えています。新しい行動指針が私たちの中で「あたりまえ」になれば、我々をひとつの組織としてまとめ社会課題に挑戦させてくれるでしょう。一方で行動指針は我々ののびしろを規定するものでもありますから、今現在はできていないところも多々あります。その事実を認知し、我々の日々の行動に浸透させて、「あたりまえ」にしていく過程を回していくことがVPoEとして重要であると考えます。

なかでも特に「燃える理想」「爆速アウトプット」がきちんとできるスピードある組織や仕組みをつくること、「自分起点」や「ワンチーム」を支える心理的安全性を醸成すること、が重要です。これには日々の1on1やブログでの発信、経営会議での議論など様々なアプローチが有効なはずで、戦略的に臨んでいく必要があります。

リーダーシップを握り合う組織の土台としてのミドルマネジメントが必要

書籍「チームワーキング」で推奨されている「目標を握り続ける」リーダーシップを推進するには、マネジメントだけではなく従業員それぞれが自分起点で行動して周囲を巻き込んでいくことが必要となります。マネジメントが率先してリーダーシップを発揮することも重要ですが、他にもチームに対して期待値を伝えたり、発言しやすい環境を整えたり、挑戦の障害を取り除いたりと様々なアプローチが効果的です。しかし製品本部ではCPOが部室長を兼務する体制が続いていた関係で、こうしたアプローチを継続的に取る余裕がない状態が続いていました。

自分起点での行動がしっかり回るとみんなのモチベーションも上がるし、結果として生産性も影響されるとは思うんですが、そのためには心理的安全性に加えて失敗を奨励する文化が必要になります。しかし、これらは資源的・時間的な余裕がないとなかなか生まれません。人が足りない・時間が足りない製品本部では特に難しいやつだという認識を持っています。

施策

ここまででVPoEとして認識している課題を述べてきました。ではこれらの課題に対し、VPoEとしてどのような打ち手を取ってきたかをご紹介します。

部室長オンボーディング

行動指針を浸透させ、心理的安全性を醸成し、失敗を奨励する文化にするためには、マネジメントが重要です。特に私たち製品本部ではつい最近まで部室長専任が存在しませんでした。ですからマネジメントが行動指針を率先して行い、チームメンバーに目を配り、多様なアプローチで組織を変えていくためには、マネジメントの育成が不可欠だと考えました。ヘンリーの従業員は自ら学べる方が多いとはいえ、経営会議が期待するマネジメントの役割を部室長にしっかりと伝えなければ、組織として目指している姿には近づけないはずだからです。

ヘンリーの経営会議メンバーは知識創造企業戦略の要諦などを読み合わせて議論の土台としていますが、この施策では知識創造企業のミドルアップダウンマネジメントを意識しました。ミドルマネジメントが機能するためには理想と現実世界の間に横たわる矛盾を解決するだけの情熱や裁量が必要になりますが、それを正しく活かすための仕組みを経営会議と管理本部とで整備してオンボーディング化しました。

部室長オンボーディング資料から、導入と目次を抜粋

ただ最初は前述の通りそもそも部室長専任が存在せず本部長が兼務していたので、まずは部室長を任命して権限委譲することの検討から始まりました。その過程で大き過ぎるstream-alignedチームを分割していますが、そのために必要なドメイン分割の難しさは以前の記事でも紹介していますのでここでは割愛します。

このオンボーディングとフォローアップが今年もっとも重要な施策です。また部室長と部室員のコミュニケーションはまだ手探りというか横断的な打ち手は存在しないので、1on1のやりかたとか心理的安全性の作り方とか失敗を奨励する文化についてとかは、これから進めていきます。

採用をドライブしていく

採用を進めるうえでの課題はいくつかあると思いますが、まず会社そのものが認知されていないこと、次に医療というドメインに抵抗感があること、そして何をやっているのか製品本部の実像が伝わりにくいことがあると考えています。いずれも体外的な情報発信を進めていくことが必要だと考えており、この技術ブログ、note、ならびに各種イベントでの情報発信を進めていきます。

採用はまさにみんなで実践するものですが、VPoEとしてはその実践を回し始めることをしっかりやろうと思っています。現在はコンスタントにブログを出すこと、発信したことのない人を無理なく巻き込んでいくことの2つに絞って活動しています。直近ではQAの方向けのイベントを企画しておりますので、関心をお持ちの方はぜひご参加いただければと思います。

henry.connpass.com

行動指針の浸透を進める

行動指針をSlackでじゃんじゃかつぶやいたり、他の従業員の書き込みに行動指針絵文字で反応したりして、新しい行動指針あったわーというリマインドをかけていっています。また弊社には経営会議から情報を共有する仕組みはもちろん、社内でベストプラクティスやグッドトライを共有するDemodayや感謝を伝えるための #all-praise チャネルなどもありますので、それらを通じても行動指針を浸透させていきます。

製品本部全員と1on1をする

いろいろと施策を打ったところで完全はないので、こまめなコミュニケーションを続けてセーフティネットとすることが欠かせないと思っています。特に燃え尽きの発見や部室長のカバーは、直接話すことでしか実現できません。

私たちのエンジニアリング組織は既に40名近い規模の組織になっていますが、少なくても四半期に1度は一人ひとりと1on1をしていきます。

まとめ

3代目のVPoEとして「圧倒的に強い開発組織を作り上げる」ために、部室長オンボーディングや採用を、行動指針の浸透を心がけています。2025年下期においてもこれらを継続するとともに、より自律できる組織を作ったり評価制度を検討したりといった施策を打っていくつもりです。

私たち製品本部には中小病院の皆さんの課題解決のため、やりたいと考えていることがたくさんあります。今いる従業員みんなに活躍してもらうことも、ファンを増やして従業員となってもらうことも、どちらも実行してエンパワーメントフライホイールをくるくる回していきます。事業やチームに関心をお持ちの方は、ぜひ採用サイトも覗いていただけると嬉しいです。

jobs.henry.jp

医療×QAエンジニアの1年間振り返り#ホリスティックテスト

🩺 入社1年を振り返る

こんにちは、ヘンリーでQAエンジニアをしている tsukiG です。

気がつけば入社して1年が経ちました。

「そろそろ何か発信しないとな…」と思いつつ、ようやく筆を取りました。

医療というまったく新しい、そして難易度の高いドメインに飛び込んだことで、 正直、なかなかスムーズにはいかず、日々の調査・理解に多くの時間を要していました。

それでも、外来体験のリニューアルという大きな節目を迎えることができました。

ようやく、 「自分たちは何をしてきたのか」「なぜそれが必要だったのか」を、少しずつ言葉にできるようになってきた気がしています。

今回はこの1年を振り返りながら、 ヘンリーという医療スタートアップが、どのように品質と向き合っているのかをお伝えしたいと思います。

🕹️ 入社前

私はこれまで約15年間、QA/テストの世界でキャリアを積んできました。

とはいえ、基本は エンタメ業界。 ゲームやMVNO、音楽配信、広告など体験重視のB2Cに関わっていました。

そんな私が自ら選んだとはいえ、医療の世界へ

当然、医療に関する知識や経験はゼロ。

分厚い診療報酬制度の本に恐れおののいた記憶も未だに鮮明です。

でもそれ以上にワクワクしていたんです。

「自分の考える品質ライフサイクルが実践できるかもしれない」と。

🔍 入社の決め手:ホリスティックテストの思想

私自身が理想とするのは「ホリスティックテスト(Holistic Testing)」です。

これは、Janet Gregoryが提唱している考え方で、 開発ライフサイクル全体にテスト活動を織り込み、“誰もがどこかの工程で品質に関与する”という文化を指していると私は解釈しています。

ヘンリーの面談でその萌芽を感じ、「この組織なら実践できるかもしれない」と思えたことが、入社の大きな決め手になりました。

ホリスティックテスト
Reference:Testing From A Holistic Point Of View

🧠 実際どうだったの?

結論から言うと、ヘンリーはホリスティックテストの実践に非常に近い状態にありました。

たとえば、こんなシーンが見られました:


  • ドメインエキスパートがQAにロールチェンジし、卓越した現場力でテストをリード

  • エンジニアが自らE2Eテストを設計・実行

  • ビジネスサイドのメンバーが週次でリグレッションテストを実施

  • ドメインエキスパートがmablで自動テストを作成

  • 代表の逆瀬川が自らテスト設計・実行を行う。QA meetupから積極的に学びを持ち帰る


入社直後の印象

ロールや所属に関係なく、社員全員が自然と品質に向き合っている。

この姿勢は、入社当初から強く印象に残っています。

そして、 時には 立ち止まり 工程を遡る そんな柔軟性も時折見ることができ、

まさにホリスティックテストというネーミング背景を体現していると感じました。

一方で、体系的なプロセスやテストに関する知識の整備という点では、まだ発展途上。
品質を“安定して”支える仕組みづくりには、多くの改善余地があると感じていました。

とはいえ、 体系的なテストの知識があるわけではなく、 プロセスや効率化、そして品質を安定して保つ仕組みにはまだ課題が依然多く残っていました。

🔧 入社後に取り組んだこと

そのような課題に対して、私は次のようなアクションを進めてきました。

🧱 テストプロセスの整備と運用

  • テストが抜けたままリリースされる…を防ぐために
    • リリースプランの運用ルールを明文化
    • チーム内で共通認識を得るための場づくり

🧭 マインドマップを活用したテスト分析

  • テスト分析にマインドマップを導入し、仕様理解を“構造化”して見える化
  • 観点のレビュー性・網羅性を向上させる土台として活用

🏗️ ハイレベルテスト設計による探索的テストの促進

  • 詳細手順に落とす前に「どの観点を検証するか?」を設計
  • 必要に応じて詳細テストケースに展開し、探索と確認のバランスをとる

🌞 1年後の変化と実感

こうした取り組みの積み重ねにより、
プロダクト開発のライフサイクル全体における品質活動が、より組織的に進化してきたと感じています。

  • Discover:顧客インタビュー、価値仮説の明確化
  • Understand:BDDや実例マッピングの導入
  • Build:開発中からビジネスチームによるフィードバック
  • Deploy:横断的な探索的テスト、非機能テストの実施
  • Release / Observe:Feature Flagを活用した段階的リリース、パフォーマンス監視
  • Learn:仮説検証やOSTを取り入れた振り返り
品質はテストだけの話ではない

という実感を、より多くのメンバーが持てるようになってきたと思います。


🔭 これからの展望と「元気玉」的品質づくり

今後もホリスティックテストの実践を深めていきますが、
次のチャレンジとして以下のような領域に取り組んでいます:

  • チームごとの最適な品質プラクティスの探索
  • Agile Testing(4象限)との再接続
  • BDD / 実例マッピングの実践

有名な漫画に「元気玉」という技があります。

多くの人から少しずつエネルギーを分けてもらい、それを集めて大きな力に変える必殺技です。 私が考えるホリスティックテストも、まさにこの「元気玉」に近いものだと思っています。

一人ひとりが少しずつ品質を意識し、行動に移すことで、結果として大きな品質が生まれる。

ただ、それだけでは足りません。 ホリスティックテストで本当に大切なのは、集めた“エネルギー”が人や工程をまたいでも減衰しないこと。 つまり、

品質に対する熱量が組織全体にスムーズに伝わる“伝導率の高さ”

が肝だと考えます。

QAはその“エネルギー”をうまく集めるだけでなく、 それがライフサイクル全体に途切れなく伝わり、自然に循環する仕組みをつくる存在でありたい。

そんな「元気玉の伝導路」のような品質の仕組みを、 これからも医療という難易度の高い現場で、粘り強く磨いていきたいと思います。


2025/07/30にヘンリー主催のQAイベント開催します。 その中でホリスティックテストの話をLTで話す予定なので、ご都合あえばぜひご参加ください

henry.connpass.com

📮 最後まで読んでいただきありがとうございました!
「医療×QA」「ホリスティックテスト」などに興味がある方、ヘンリーに興味を持っていただいた方、一度お話ししましょう。 jobs.henry.jp

製品組織全体のHoneycomb活用事例

株式会社ヘンリーでオブザーバビリティをやっているsumirenです。

弊社ではトレースバックエンドにHoneycombを活用しています。いまや多くの方が日々の業務で使うようになり、プロダクトの運用になくてはならないものになりました。

この記事では、ヘンリーのメンバがHoneycombをどのように使っているか、Honeycombのアクティビティログから事例紹介します。

ヘンリーにとってのHoneycombの価値

トレースバックエンドというと、おそらく多くの方が1リクエストのツリーを可視化する用途を想像するかと思います。しかし、ヘンリーでHoneycombが定着しているのは、スパンの集計が非常に強力だからだと筆者は考えています。

この記事を通じて、ログの集計やメトリクスではなく、スパンを探索的に集計できることの価値の大きさや面白さが伝われば幸いです。

事例1. ある機能を各テナントのユーザーが何名くらいリクエストしているか集計する

id:horsewinの利用ログを参考にしています。

マイクロサービスAについて、目玉となる新機能をリリースしたようです。特定のアクター向けの機能であり、テナント別にも機能のオンオフをしています。各テナントのどれくらいのユーザーがこのエンドポイントを利用しているか確認することで、リリースがうまくいって使ってもらえているかを確かめることができます。

Honeycombではこうした集計は簡単です。WHEREで特定マイクロサービスの特定エンドポイントに絞ったうえで、テナント別にGROUP BYし、ユーザーIDでCOUNT_DISTINCTします(下図)。

同じことをアプリケーションメトリクス で実現するには、エンドポイントやユーザー ID、テナント ID などの属性ごとにメトリクスを分割・生成できるようあらかじめ定義しておく必要があります。ここで言う属性を一般にディメンションと呼びます。しかし、こうしたディメンションを含むメトリクスが都合よく収集されていることはそう多くありません。

Honeycombではインデックスなどの設定なしで、スパンに含まれる全ての項目を集計対象にできます。そのため、テナントを絞ってGraphQLオペレーションごとのレイテンシと実行数をカウントするなど、アイデアとニーズ次第で様々な軸でリクエストを集計することができます。

事例2. 問題のあるDBクエリがどのエンドポイントから呼ばれたときに遅いか集計する

id:nabeopの利用ログを参考にしています。

あるクエリでスロークエリが発生していたようです。一方で、いつも遅いわけでないため、どうやらクエリの使われ方に依存していそうです。Query InsightsなどマネージドDB側でわかるのは普通ここまでです。

トレースバックエンドでエンドポイントごとにクエリを集計することで、問題のあるエンドポイントを絞り込むことができます。

Honeycombでは、こうしたユースケースのために、「トレースとのJOIN」のような集計ができます。この例は、「db.statementを持つスパンを、そのルートにあたるスパンのgraphql.operation.nameでGROUP BYする」といった内容になっています(下図)。

集計結果を見ると、ListPatientSummariesというエンドポイントから使われたときだけこのクエリが遅いことがわかります(下図)。

あとは当該エンドポイントのコードを読んでもいいですし、さらに分母を絞って、次の事例のように異常検知を利用してもよいでしょう。

事例3. 異常検知で遅いリクエストの属性を絞り込む

id:giiita22の利用ログを参考にしています。

集計から洞察を得るには、異常値を示すメトリクスと相関のあるディメンションを特定することが重要です。例えば、前の例では「DB クエリの遅さ」という異常メトリクスと、「エンドポイント名」というディメンションの相関を検証し、その結果、特定エンドポイントだけが遅いと判明しましす。ここで、どのディメンションに着目するかという仮説の立案自体を自動化できれば、さらに効率的です。

さて、ある機能について、顧客から応答が遅いという問い合わせを受けたようです。過去のメンバーの記事にもあるとおり、マイクロサービスのモジュラモノリス移行といったアーキテクチャ的な変更や、大規模なリニューアルもあり、モジュラモノリスが怪しいのはわかっていました。

ヒートマップを見てみると、時間帯によらずリクエストの応答時間にばらつきがあることがわかります(下図)。

ここで、ディメンションをあれこれと入れてみるかわりに、異常検知を実行して、2秒以下のものと2秒以上のものでどのような属性の違いがあるかを調べていました。「EncounterTemplatesQuery」のエンドポイント(GraphQLオペレーション)が、異常な集合において76%を占めていることがわかります(下図)。これで、インフラ起因ではなくアプリ起因のようだということがわかりました。

さらに、ここから分母を絞り込んでもう一度集計と異常検知をしています。問題のあるエンドポイントだけの集計結果を見ると、必ずしもそのエンドポイントが常に遅いとは言えなさそうで、データに依存していそうです(下図)。ここでまた異常検知の出番です。

こうして分母を絞り込んで異常検知をすることで、無関係な分母と属性値が減るため、より異常なものと正常なものの差がクリアに見えるようになります。この例では、特定のparentFolderIdが指定されている場合は明らかに異常側に入る割合が多くなっています(下図)。ただし、それでも8%と少ないため、おそらくnullの場合も遅いのでしょう。この情報と、遅いトレースの確認により、DataLoader周りの実装に問題があることがわかったようです。

まとめ

ヘンリーでは製品組織全体でHoneycombを日々使いこなしています。カスタマーサクセスの方がアクティブユーザーを計測するのに利用したり、正式リリース前から使用していることもログからわかっています。

また、用途としては、単にトレース1つ1つを見るだけでなく、集計や全体の分析が大きく役立っています。障害や問題に対する仮説の検証も即座にデータで行うことができ、異常検知で経験や勘といったアートに頼らない仮説立案まで進めています。

GraphQLオペレーション名やリクエストボディ(マスキング含む)などの手動計装は、主に筆者が実装しました。他にもフィーチャーフラグやレスポンスのサイズなど豊富な計装があります。事例を踏まえると、手動計装がいかに強力か理解できるのではないでしょうか。スパンに1つ属性を足すことは、問題分析の手札を1つ増やすことと全く同義だからです。ヘンリーでは、機能個別のドメインロジックに対する手動計装も進めています。

Honeycombを導入した直後は、本当に組織に浸透するか不安もありましたが、この記事を書きながらヘンリーのメンバが日々大量のクエリを投げているログを見て、杞憂だったと安心しています。ご興味がある方はぜひカジュアルに弊社メンバとお話しましょう。

jobs.henry-app.jp

gRPCで分断されたモノリスを段階的にモジュラーモノリスに移行する

こんにちは。株式会社ヘンリーでエンジニアをしているagatanです。

私たちが開発する電子カルテ・医事会計システム「Henry」は、非常に巨大な単一のプロダクトです。そして、その性質上、明確なドメイン境界を見出すことが難しいという特性を持っています。この「巨大で複雑なプロダクトを、いかにして組織的に開発し続けるか」という問いに開発チームは長年向き合ってきました。

最近、この大きな問いに対する新たな一手として、かつて2つのgRPCサービスとして分割されていたバックエンドを、段階的に「1プロセスのモジュラーモノリス」へと移行させるプロジェクトが進捗しています。

今回は、その移行の過程についてお話しします。

第一歩: モノレポ化

移行への大きな第一歩目は、2年前に遡ります。当時、第一歩として踏み切った「モノレポ化」については、過去のブログでも紹介されています。

dev.henry.jp

この記事にもあるように、もともとHenryのバックエンドは general-api と receipt-api という2つのサービス、2つのリポジトリに分かれていました。general-api がユーザーからのリクエストの矢面に立ち、その裏で receipt-api が複雑な医療制度の計算を担う、という設計思想です。

しかし、事業が成長し、ドメインやプロダクトへの解像度が上がるにつれて、この当初の分割境界がもはや理想的ではないことが明らかになってきました。誤った分割境界が歪みを生み、密結合でありながら物理的に分断されているという、矛盾した状態に陥ったコードが散見される状態になっていました。

この歪みを正し、より適切な境界線を見出すための第一歩が、モノレポ化でした。 まず誤った境界を取り払い境界の見直しの試行錯誤をやりやすくする、という方針を掲げ、中期的には receipt-api を解体し general-api に一旦寄せることを目指しました。

その理想を目指すトランジションの初手として、バックエンドのコードを一旦モノレポにまとめ、サービス境界の見直しを含めた変更をやりやすくするという戦略を取りました。

これにより、関連する変更をatomicに行えるようになり、開発効率は確かに向上しました。 しかしながらこの時点では general-api と receipt-api という2つのサービスは依然として存在し、根本的な課題は手つかずのままでした。

第二歩:Gradleモジュールによる「内側からの分割」

Henryのバックエンドは、主にKotlinとGradleで構築されています。モノレポ化以前から、general-api と receipt-api はそれぞれGradleモジュールとして定義されていました。

モノレポになったことで、両サービスから依存されるような基盤的実装などを含む :shared モジュールが生まれるなど、Gradle モジュールの活用が進んできました。

dev.henry.jp

Gradle モジュールの活用が進むにつれて、徐々に :receipt-api をさらに分割する動きが出てきました。 医療事務(医事)の業務にまつわる処理なので :iji という prefix をつけ、より細かい業務単位で :iji:foo, :iji:bar のようなモジュールが徐々に切り出されていきました。

flowchart LR
  general[[:general-api]] --> :shared
  receipt[[:receipt-api]] --> :shared
  receipt --> :iji:foo
  receipt --> :iji:bar

巨大だった :receipt-api が、内側から徐々に分割されはじめました。 分割するだけでも、ビルドの並列性を高めたり、責務と依存の方向を明確に強制できるようになったりと、この時点でも大きな恩恵がありました。

そしてモジュラーモノリスへ

このように Gradle モジュールが分割されていったことで、これまで :receipt-api 配下にあった処理も :general-api から直接参照できるケースが増えてきました。

flowchart LR
  general[[:general-api]] --> :shared
  receipt[[:receipt-api]] --> :shared
  receipt --> :iji:foo
  general --> :iji:foo
  receipt --> :iji:bar

従来であれば general-api が receipt-api に gRPC 経由で処理を委譲していたものについても、general-api のプロセス内で完結することができるようになりました。 gRPC を経由しないことで、パフォーマンスの向上や安定性の向上のみならず、処理同士のインターフェースをより柔軟に Kotlin の表現力をフル活用して定義できるようになり、開発生産性も向上しています。

receipt-api は単体で見ても相当なサイズがあったため、移行は段階的に行う必要がありました。 直近まったく触っていない機能などもあり、よく変更する部分から徐々に移行を行っています。

これを段階的に行うための実践的工夫として 1. あえて protobuf 依存を切らない選択肢を持つ 2. 逃げ道を作る といった工夫をしてきました。

あえて protobuf 依存を切らない選択肢を持つ

general-api と receipt-api はもともと gRPC で分断されており、そのインターフェースは protobuf で定義されていました。

理想的な進め方としては、そもそも gRPC を廃止しようとしているわけなので、protobuf 依存も外すことが望ましいと考えています。 :iji:foo のようなモジュールには純粋なドメインロジックのみを定義し、そこに protobuf 依存が入り込むということは許容しないのが理想です。

基本的にはそのような考え方に基づいて移行を進めていったのですが、どうしても巨大すぎるドメインや技術的負債が積み重なった部分については protobuf 依存を剥がすことが非常に大変でした。 開発の極初期のころ、protobuf をドメインロジックから引き剥がす設計判断をしていなかったのもあり、古くからあるコードベースでは protobuf がドメインロジックの中核まで染み込んでいるケースがあったためです。 そういった領域については、むしろ protobuf 依存を剥がすためにも、まず無理矢理でもいいから 1 プロセスで収まるようにしてしまって、そこからリファクタリングするほうが、安全性を担保したまま移行を推進できる、と判断しました。

protobuf そのものはある意味では Plain Old なデータなので、剥がさないことの弊害も小さく、実装面としても

  • before
    • Object (general-api) → protobuf → (gRPC) → protobuf → Object (receipt-api)
  • after
    • Object (general-api) → protobuf → Object (receipt-api)

のようにネットワーク越しに呼び出すという処理が消し飛ぶだけで、コード上の字面はほとんど変えずに済みました。 (このあたりは元来 RPC フレームワークがネットワーク越しであることを隠蔽するような設計になっているので、ある意味当たり前なのですが)

この戦略を取った領域については、 :iji:foo モジュールへの切り出しも非常に軽量でした。要するにコントローラー層から先をすべてバコッとモジュールに押し込んでしまえばよいためです。 とはいえもちろんただ押し込むだけでは価値が生まれないので、その後の改善戦略とセットです。 実際にあった例としては、protobuf にプロパティを生やさないと対応できないような機能実装を行う際に段階的に protobuf を剥がす、といった活動をしています。

逃げ道を作る

ここまで、 :iji:foo のようなモジュールは、まるで単独で駆動する純粋なモジュールのような顔をしている説明をしてきました。 実際には、ほとんどの業務領域が共通的なモデル(Henry でいえば「患者」や「保険制度」など)に依存しており、ここまで単純なものではありません。

そういった「本来であればそれ単独でモジュールをなして依存グラフの根に近い位置にいるべき」であろうクラスたちについても、既存実装がモジュール分割を想定しないで作ってきたため、クラス同士の依存関係が巨大な団子をなしており、一部を切り取ることが難しくなっていました。 そういったクラスたちはだいたいどの領域の実装からも依存されており、彼らの存在が邪魔をしてしまってモジュール分割が阻害されるということが頻発していました。

そこで、 :iji:shared のような駆け込み寺モジュールをあえて用意し、大きな団子を団子のままバコッと押し込むことにしました。 もともと :receipt-api にあった団子を移動しただけなので、複雑さは微増で済み、代わりに団子の外側でキレイなモジュールを作る選択肢が圧倒的に増えました。

将来的にはこれが問題となる未来はもちろん見えているのですが、厳密にやることを重視しすぎて前に進めなくなるよりもこの方向性で前に進めてしまう方が、短期・長期どちらにとっても有益だろうと判断しました。

flowchart LR
  general[[:general-api]] --> :shared
  receipt[[:receipt-api]] --> :shared
  receipt --> :iji:foo
  general --> :iji:foo
  receipt --> :iji:bar
  :iji:foo --> :iji:shared
  :iji:bar --> :iji:shared

現時点の進捗

現在、医事領域が先行してモジュール分割を進めており、:iji:* を冠するモジュールは14個にまで増えました。 既存の2つのサービスから実装を削り取って融合させたモジュールや、全く新しい機能として生まれたモジュールが混在し、:general-api 単体で完結する処理は着実に増え続けています。

その効果は、日々の開発サイクルにも現れています。 Gradle はモジュールごとに依存関係を見て自動でタスクを並列実行してくれますが、 :iji:* モジュールにまつわるタスクについてうまく並列化できていることがわかります。(ヘンリーでは develocity を導入しています)

Gradleタスクの実行時間

dev.henry.jp

臨床領域でもモジュール分割の機運が高まってきており、このまま少しずつ receipt-api が痩せていって最後には大統一される未来を期待しています。

おわりに

本記事では、ヘンリーのバックエンドが2つのgRPCサービスから単一プロセスのモジュラーモノリスへと移行している様子を、その背景と実践的な工夫と共にご紹介しました。

巨大でドメイン境界が曖昧なプロダクトをいかに組織的に開発していくか、という問いに対して、私たちはまずモノレポ化に踏み切り、次にGradleのモジュール分割を進め、そして今、モジュラーモノリスへの道を歩んでいます。この道のりは、当初のサービス分割がもたらした歪みを解消し、より柔軟で生産性の高い開発体制を築くための挑戦です。

移行の過程では、理想論だけでは乗り越えられない現実的な壁に対する工夫も行ってきました。完璧な設計を追い求めるあまり歩みを止めるのではなく、将来の改善を見据えながらも、まずはチームが前に進むことを優先する。こうした現実的な判断の積み重ねが、大きな変革を少しずつ、しかし着実に推し進める力になっていると感じています。

この取り組みはまだ道半ばです。まだまだ2つのサービスが本当に統合され切るには遠いですし、ここまでやってきたモジュール分割がいつかまた別の問題として火を吹く日も来るかもしれません。 が、これまでモジュール分割を行ってきて、「複雑・巨大すぎて手のつけようがなさそうに思えること」も、まず少しやってみることが始めると、意外となんとかなると思えるようになるなと感じています。 ソフトウェア開発は継続的な改善の積み上げなので、今後も振り返りつつも挑戦し続けていきたいと思っています。

ヘンリーでは、このようにプロダクトの成長に合わせてアーキテクチャを柔軟に見直し、開発者体験とプロダクト価値の向上にチーム一丸となって取り組んでいます。複雑な課題に対して、泥臭く、しかし戦略的に向き合っていくことに面白さを感じる方がいらっしゃいましたら、ぜひ一度お話しさせてください。

dev.henry.jp

jobs.henry-app.jp

最後までお読みいただき、ありがとうございました。

実装手順書よりもコンパイルエラー

株式会社ヘンリーでエンジニアをしている okbee です。
直近は製品のフルリニューアルを行なっており、詳細は省きますが、私もこの開発に参加しています。社内では「コスト連携」と呼ばれる機能の開発を主に担当していました。 「コスト連携」をざっくりと説明するならば、患者に対して医師が作成した指示(オーダー)を元にして、実際の金額を算出するためのワークフローです。詳細はコンテキストで紹介します。

さて、今回は「コスト連携」の実装を通して感じた反省を元に、より良い設計のヒントとして「コンパイルエラーを活用した手順不足の検知」を考えていきます。

コンテキスト

コスト連携には臨床・会計の2つの大きなコンテキストが背景にあります。
患者に対して医師が作成する指示(以降はオーダーと表記)は臨床と呼ばれるコンテキストで作成されます。ちょうど、皆さんがクリニックなどで診察を受ける際に医師が薬を処方したり、注射の指示を行ったりします。これが臨床側のコンテキストです。

その後、医師の指示を元に患者が実際に支払う金額を算出します。
ここでは初診料などが追加されますが、元となるのは臨床側で作成されたオーダーです。例えば処方箋が出ていれば、薬価(薬の値段)や調剤料などを会計時に支払う必要があります。 これが会計側のコンテキストです。(普段は見えない部分)

言ってみれば、コスト連携は臨床・会計のコンテキストを連携する機能です。
ヘンリーでも同じように臨床・会計と大きく2つの開発チームが存在しています。

コスト連携の大変さ

コスト連携では臨床側で作られたオーダーを中間データに変換して、最終的にコストと呼ばれるデータに変換します。 コンテキスト境界ゆえの難しさはありますが、特別に複雑な実装が必要なわけではありません。

しかし、最終的なコストに変換するためには、マスタや電子点数表といった基金などが公開している多種多様なデータが必要です。オーダーごとに必要なデータは異なり共通化が難しく、どうしてもオーダーごとの対応が必要になります。

中間データの作成時にも同じ問題が起こります。オーダーごとに参照するプロパティが全く異なるため、共通化が難しいです。 オーダーの種類が2・3つであれば苦労しませんが、現時点では6つのオーダーがコスト連携の対象となっています。そして、今後も対象となるオーダーは増えていきます。

graph LR
    subgraph 臨床コンテキスト
        処方オーダー
        注射オーダー
    end
    subgraph 会計コンテキスト
        処方マスタ[(処方用マスタ)] --> 処方コスト
        処方オーダー --> 処方中間データ
        処方中間データ --> 処方コスト

        注射オーダー --> 注射中間データ
        注射マスタ[(注射用マスタ)] --> 注射コスト
        注射中間データ --> 注射コスト

        注射コスト --> 総額
        処方コスト --> 総額
    end

コスト連携の大変さは「手数 x 対象のオーダー数」になります。

実装手順書を作ったが...

私が別機能の開発を行うことになり、今後のオーダー拡張に備えて、実装手順書を作成しました。 実際の中身はお見せできませんが、結構なボリュームがあります。また、最新のコードは常に変化しているので、実装手順書を常にコードへ同期させる必要があります。 しかし、残念ながらこういったドキュメントの維持管理は後手に周り、忘れ去られがちです。

実際に、私が行ったコード変更を実装手順書に反映し忘れた結果、他の方に対応してもらった際に対応漏れが発生しました。PRレビュー時には、私自身、対応漏れに気づきませんでした。 人間はミスを必ずします。実装手順書では、対応漏れが発生しても気づけない可能性が十分に考えられます。

理想: 対応漏れはコンパイルエラーとなる

理想を考える上でビジネスロジックを「型」で表現するOOPのための関数型DDDがヒントになりました。 また近い時期に販売された「関数型ドメインモデリング」も参考にしています。どちらも素晴らしい内容なので、ぜひご覧ください。

特に秀逸なのは「代数型データを使ってデータ不整合を取り除く & 網羅性を担保する」箇所です。 網羅性が不十分なためコンパイルエラーとなるシンプルな例を見てみます。エラー内容からも網羅性が不十分なのは自明です。

// sealed interfaceを用いて直和型を定義
sealed interface Animal {
  object Dog : Animal
  object Cat : Animal
  object Bird : Animal
}

fun sound(animal: Animal): String {
  return when (animal) {
    Animal.Dog -> "Woof!"
    Animal.Cat -> "Meow!"
    // ↓ Animal.Bird を忘れていると、コンパイルエラーになる
    // 'when' expression must be exhaustive, add necessary 'is Bird' branch or 'else' branch instead
  }
}

言ってみれば「Birdの条件分岐を追加する」という必要な手順が足りていないため、コンパイルエラーとなります。つまり、定められた手順を満たせていないということです。
同じことが、コスト連携の対象オーダー拡張でも出来ないかな?と考えました。

実装例

まず、設計に関して大きく2つの制約があります。

  1. コスト連携はワークフローとして実装しており、全てのオーダーは同じステップをもつ
  2. 臨床側で作成される全てのオーダーが、コスト連携の対象となるわけではない

また、臨床側ではオーダーの定義が以下のようにされているとします。

sealed interface Order {
  data class 処方(...) : Order
  data class 注射(...) : Order
  data class 検体検査(...) : Order
  :
}

ワークフローのステップを定義

再掲ですが、コスト連携は臨床側で作られたオーダーを中間データに変換して、最終的にコストと呼ばれるデータに変換する一連のワークフローです。 制約の通り、全てのオーダーで同じステップをもつため、素直にステップをインターフェースとして定義します。

どのオーダーでも実装は共通だったため、デフォルトの実装まで書いています。

/**
 * コスト連携のワークフローのステップを定義したIF。
 * オーバーライド先で実装の詳細を書くとファットなクラスになってしまうため、
 * Dependency Injectionを利用して、実装の詳細は別で行うことを期待している。
 */
interface CostSyncWorkflow<CostSyncTargetOrder : Order> {
  /**
   * マスタ取得が必要な項目を抽出する
   */
  fun getMasterRequirements(
    orders: List<CostSyncTargetOrder>,
    extractor: (List<CostSyncTargetOrder>) -> MasterRequirements
  ): MasterRequirements = extractor(orders)

  /**
   * オーダーを中間データに変換する
   */
  fun to中間データ(
    orders: List<CostSyncTargetOrder>,
    converter: (List<CostSyncTargetOrder>) -> List<中間データ>
  ): List<中間データ> = converter(orders)
}

コスト連携の対象となるオーダーの定義

2つ目の制約から、臨床側で定義されるオーダーとコスト連携の対象となるオーダーの定義は別にしておくのが良さそうです。 また、臨床と会計のコンテキスト境界でもあるため、同じ定義を使わずに分けておいた方が良いでしょう。合わせて、先ほど定義したインターフェースを実装するようにします。

試しに検体検査はコスト連携の対象外として、サブクラスとして定義しません。

/**
 * コスト連携の対象となるオーダーの一覧。
 * ここに連携対象としたいオーダーをサブクラスとして追加していく事を期待している。
 * パフォーマンスの都合上(N+1を回避するため)、オーダーの一覧をプロパティに持たせている。
 */
sealed interface CostSyncSourceType {
  val orders: List<Order>

  data class 処方(override val orders: List<Order.処方>) : Order, CostSyncWorkflow<Order.処方>
  data class 注射(override val orders: List<Order.注射>) : Order, CostSyncWorkflow<Order.注射>
}

ワークフローの実装例

これまでの定義を用いて、ワークフローを実装してみます。
CostSyncSourceType.注射に関する実装をあえてしないと、網羅性が不十分なため、期待通りにコンパイルエラーとなります。 また、検体検査に関してはコスト連携の対象ではないため、網羅性チェックには含まれません。
(実際に動くコードではありませんが、イメージの共有が目的)

/**
 * コスト連携を実行するワークフロー。
 * 対象となるオーダーを受け取り、中間データを経て、最終的にコストへ変換をする。
 */
class CostSyncWorkflowV2(
  private val masterFetcher: MasterFetcher,
  private val 処方MasterExtractor: 処方MasterExtractor,
  private val 処方To中間データConverter: 処方To中間データConverter,
) {
  fun syncCost(sources: List<CostSyncSourceType>) {
    val masters = fetchMasters(sources)
    val costInvocations = to中間データ(sources, masters)
    :
  }

  /**
   * 対処となるオーダーのプロパティをそれぞれ参照して、マスタ取得に必要な情報を集める。
   */
  private fun fetchMasters(sources: List<CostSyncSourceType>): Masters {
    val masterRequirements = sources
      .map { source ->
        when (source) {
          is CostSyncSourceType.処方 -> {
            source.getMasterRequirements(source.orders) { orders ->
              処方MasterExtractor.extract(orders)
            }
          }
          // 網羅性が不十分なためコンパイルエラーとなる
          // 'when' expression must be exhaustive, add necessary 'is 注射' or 'else' branch
        }
      }
      .reduce { acc, master -> acc + master }
    return masterFetcher.fetchFromRequirements(masterRequirements)
  }

  /**
   * コスト連携の対象となるオーダーを中間データに変換する。
   */
  private fun to中間データ(
    sources: List<CostSyncSourceType>,
    masters: Masters
  ): List<中間データ> {
    return sources.map { source ->
      when (sources) {
        is CostSyncSourceType.処方 -> {
          source.toCostInvocations(source.orders) { orders ->
            処方To中間データConverter.convert(orders, masters)
          }
        }
        // 網羅性が不十分なためコンパイルエラーとなる
        // 'when' expression must be exhaustive, add necessary 'is 注射' or 'else' branch
      }
    }
  }
}

このように不足している手順(実装)を、コンパイルエラーによって安全に把握することが可能となりました。 コンパイルエラーがあるので、もちろんコンパイルに失敗し、ビルドもできません。つまり、対応漏れのコードがリリースされることはありません。

トレードオフ

実装例を示したところで、最後に設計に関するトレードオフについて考えてみます。

型・操作(関数)のどちらを追加しやすくするか

Expression problem で議論されているように、型と操作(関数)の追加はトレードオフの関係になります。 V2の実装では、ワークフローに大きな変更(操作の追加)が発生する可能性は低いが、対象となるオーダー追加(型の追加)は頻繁にされる前提で行なっています。

そのため、ワークフローの頻繁な更新、ステップの大改修が起きた場合、V2実装では対応のコストが非常に高くなってしまいます。

操作の追加時

// interface に操作を定義 & ワークフローの変更 が必要 -> コストが高い
interface CostSyncWorkflow<CostSyncTargetOrder : Order> {
  :
  fun doSomething()
}

型の追加時

// サブクラスを追加 & whenへの条件追加が必要 -> コストが低い
sealed interface CostSyncSourceType {
  :
  class 新たなオーダー(
    override val orders: List<HogeOrder>
  ) : CostSyncSourceType, CostSyncWorkflow<HogeOrder>
}

総合的なコスト

今後も新たなオーダーが追加されますが、何百と追加されることはありません。
オーダー拡張に対するコストを設計で下げたとしても、愚直にそれぞれ実装した場合のコストと比較すると、総合的なコストはあまり変わらないかもしれません。
設計時点で対象となるオーダーがどれぐらいになるのかを把握しているかどうかで、最終的にどのような設計にすべきかの選択肢は変わってくるはずです。

早すぎる最適化

今回のアイディアが思い浮かんだのは、現在の実装を通して共通性が見えてきたからです。 早い段階でワークフローを整理していれば、設計はまた違ったかもしれませんが、私は実装の初期段階で強い確証がなければ、変更・拡張に対して強いコードを素直に書くべきだと考えています。

例えば、一本の木をじっと見つめていても、それが森かどうかは分かりません。高いところから俯瞰して初めて、広大な森だと気づくことがあります。 実装に関しても同じアプローチである方が自然ではないでしょうか。

早すぎる最適化は、不要な複雑さと制約を生み出す可能性があります。

まとめ

  • 実装手順書の管理は難しい上、人間はミスをする
  • 手順に不備があることをコンパイルエラーとして表現したい
  • Kotlinでは網羅性チェックを用いることで、コンパイルエラーとすることが可能
    • 網羅性チェックを用いて手順の不備を検知することができるかも?
  • 今回の実装例では大きく2つの定義を用いた
    • コスト連携のステップを定義したインターフェース
    • コスト連携の対象となるオーダーの定義
  • V2の実装ではコンパイルエラーにより手順に不備があることを検知できた


ヘンリーでは医療業界の課題に向き合い、電子カルテ・レセコンの開発に取り組むソフトウェアエンジニアを募集しています。 今回の記事の感想など、お気軽にカジュアル面談でお話ししましょう👍

jobs.henry-app.jp


おまけ: コスト連携 対象オーダーの取得

コスト連携の対象となるオーダを全種類、取得する実装について考えてみます。
以前の実装では、手順書に従う他なく、対象オーダーの拡張時に対応漏れがあっても、コンパイルエラーとなることはありませんでした。ここでも同じようにコンパイルエラーが発生するようにしてみます。

本当はCostSyncSourceTypeから動的に判断したいところですが、シンプルな仕組みで実装するのが難しかったため、愚直に手でenumを定義しました。
CostSyncSourceTypeCostSyncSourceTypesをオーダーの拡張時に更新することを期待しています。

もっと良い方法をご存知の方がいれば、教えて頂きたいです。

enum class CostSyncSourceTypes(val klass: KClass<out CostSyncSourceType>) {
  処方(CostSyncSourceType.処方::class),
  注射(CostSyncSourceType.注射::class),
}

fun fetchOrders() {
    CostSyncSourceTypes.entries.map { sourceType ->
        when (sourceType) {
            CostSyncSourceTypes.処方 -> println("処方オーダーの取得")
            CostSyncSourceTypes.注射 -> println("注射オーダーの取得")
        }
    }
}