自己申告に頼らない飲酒測定――ウェアラブルでとらえる飲酒とその影響

1.飲酒習慣を測ることの難しさ

こんにちは。この記事は、テックドクターでデータ解析を担当する坂本と藤本が共同で執筆しています。

みなさんが健康診断を受けると、飲酒の習慣を尋ねられると思います。私(坂本)はよく、「飲み過ぎ注意」と言われていました。飲酒は、健康のいろいろな面に影響を与えるとされています(※1)

飲酒の量や習慣は、主に質問紙(アンケート)などで測定されます。しかし、例えば、過去7日間を振り返って飲酒の量を思い出して回答した場合、実際より少ない量で回答してしまうなどのリスクがあると言われています(※2)
もちろん、質問紙を用いた飲酒の測定も十分に有効活用できますが、毎日の記録/長期的な記録が必要な場面では、回答の負担が大きく、継続も難しいです。

そんなとき、スマートウォッチで自動的に飲酒の記録ができたら便利ですよね。
こういった背景から、私たちは「スマートウォッチを代表とするウェアラブル端末を装着しているだけで、飲酒を自動検出し、飲酒の影響を評価する」という技術開発に挑戦することにしました。

そして先日、その成果について、第32回日本行動医学会学術総会にてポスター発表を行いました(※3)。この記事では、その発表をもとに、「ウェアラブル端末で飲酒の検出と影響評価を行う技術」、そして「ウェアラブル端末のデータからわかった、飲酒が睡眠や翌日の活動に与える影響」について紹介したいと思います。

※1
例えば、厚生労働省「健康に配慮した飲酒に関するガイドラインについて」, URL: https://www.mhlw.go.jp/stf/newpage_38541.html, Last Accsess: 2025-12-10)。

※2
例えば、
Gmel G, Daeppen JB. Recall bias for seven-day recall measurement of alcohol consumption among emergency department patients: implications for case-crossover designs. J Stud Alcohol Drugs. 2007 Mar;68(2):303-10. doi: 10.15288/jsad.2007.68.303. PMID: 17286350.

※3
◎坂本・◯藤本・深見(2025). ウェアラブル端末を用いた20歳以上の一般成人を対象とする飲酒の多変量予測モデルの開発とアルコール飲料摂取が日常生活に与える影響の評価. 第32回日本行動医学会学術総会, 神奈川県相模原市 相模原市立産業会館, 2025年12月6日. (◎は責任著者・発表者、◯は発表者)

イメージ画像

2.飲酒日を検出する指標の開発

ウェアラブルデバイスデータを用いた「飲酒日」の検出

ウェアラブル端末では、主に心拍数、歩数、睡眠などの情報が取得できます。
しかし、例えば「心拍数が高い」というだけでは、運動や緊張、発熱などと区別がつけられず、飲酒を特定することができません。飲酒を見つけ出すためには相応の工夫が必要です。

そこで、「お酒を飲むと心拍が早くなる」という経験に着想を得て、「歩いていない時間に持続的に心拍数が上昇する」という特徴を定量化する計算手法を開発しました。
この指標を、本記事では「飲酒指標(Alcohol Index)」と呼ぶことにしましょう。この指標が、飲酒と高い関連性を示しました(※4)

※4
2026年1月現時点: 特許出願中

グラフ画像
図. 飲酒があった日の飲酒指標の例

飲酒指標を代表的な情報として、睡眠中の心拍数などを加えた特徴量を用いて機械学習モデルによる解析(XGBoost)を行ったところ、飲酒日の検出精度は90%程度となりました。

グラフ画像
図. 飲酒日検知モデルの性能

予測モデルの妥当性評価と研究報告

飲酒の検出精度が90%と聞くと、「すぐにでも実用化できる」ように感じられるかもしれません。しかし、安全な技術を確立するためには、多面的な検証を積み重ねる必要があります。
例えば、「どのような人でも同じ性能が得られるのか」や「誤判定が生じやすい条件は何か」といったような、公平性やリスク、安全性等に関する検証や対策が必要となります。

研究報告において大切なのは、何が未検証/未対策であるかを誠実に報告し、次の検証に繋げることです。「TRIPOD+AI声明」という研究報告で求められる事項が整理された国際的な指針(※5)があるので、それに沿って多面的な評価と今後の課題をまとめました。

ウェアラブル端末を装着しているだけで飲酒の自動記録が可能になり、健康管理など様々な場面で活用できるようになるという未来に向けて、検証を積み重ねたいと考えています。

※5
Collins G S, Moons K G M, Dhiman P, Riley R D, Beam A L, Van Calster B et al. TRIPOD+AI statement: updated guidance for reporting clinical prediction models that use regression or machine learning methods BMJ 2024; 385 :e078378 doi:10.1136/bmj-2023-078378


3.ウェアラブルデバイスを用いた飲酒の翌日の影響評価

次に、実際に飲酒が睡眠や翌日の活動にどのような影響を及ぼすかについても解析しました。

社内での取り組みを通して可視化できたデータをご紹介していきます。

心拍変動指標について

今回解析に利用した、心拍変動に関する指標を紹介しておきます。

  • RMSSD……副交感神経活動を反映する指標。これが低下した場合、十分にリラックスできていないと考えられる。
  • SDNN……交感・副交感神経の両方の変動を反映する指標であり、値が高いほど活発な自律神経活動が発生していたと考えられる。

データと参加者

下記の対象者/対象データを解析しました。

  • 21名、4932日分の飲酒報告と対応する日のウェアラブル端末装着情報を利用(男性57%)
  • 前日の飲酒報告(非飲酒・少量・中量・大量)
  • アンケート回答日と翌日のウェアラブル装着がある者
  • 装着時間が70%以上の日のみ採用
  • 平均値±3SD以内の睡眠時間の日

※今回は飲酒の有無はアンケートを利用して判定しました。

解析方法

解析では、一元配置分散分析、線形混合モデルと呼ばれる手法を使用しました。

  • 一元配置分散分析(ANOVA)……飲酒量4段階の群間差を比較
  • 線形混合モデル(LMM)……個人差を調整し、飲酒有無の影響を精緻に検討

解析結果

まず、一元配置分散分析により見えてきた結果をご紹介していきます。

睡眠 大量の飲酒をした場合、少量・中量飲酒に比べてさらに睡眠中の最低心拍が高くなっていました。(図1)
翌日の心拍 大量の飲酒をすると、翌日の安静時心拍数が高くなっていました。(図2)
翌日の活動 少量・中量の飲酒をすると、飲酒しなかった場合に比べて翌日の歩数が多くなっていました。(図3)
大量の飲酒をすると、少量・中量の飲酒に比べて翌日の歩行ペースが遅くなっていました。(図4)
少量の飲酒をすると、飲酒しなかった場合に比べて翌日の高強度運動時間割合が上がっていました。(図5)

次に、線形混合モデルにより見えてきた結果をご紹介します。

睡眠 飲酒をすると睡眠中の最低心拍が高くなっていました。(図6)
飲酒をすると睡眠中のSDNNが高くなっていました。(図7)
飲酒をすると睡眠中のRMSSDが低くなっていました。(図8)
翌日の心拍 飲酒をすると翌日の安静時心拍数が高くなっていました。(図9)
グラフ画像
図1: 飲酒レベルごとの睡眠中の最低心拍数比較
グラフ画像
図2: 飲酒レベルごとの安静時心拍数比較
グラフ画像
図3: 飲酒レベルごとの翌日の合計歩数比較
グラフ画像
図4: 飲酒レベルごとの翌日の歩行ペース比較
グラフ画像
図5: 飲酒レベルごとの翌日の高強度運動時間比較
グラフ画像
図6: 飲酒日と非飲酒日の睡眠中の最低心拍数比較
グラフ画像
図7: 飲酒日と非飲酒日の睡眠中のSDNN比較
グラフ画像
図8: 飲酒日と非飲酒日の睡眠中のRMSSD比較
グラフ画像
図9: 飲酒日と非飲酒日の安静時心拍数比較

考察

飲酒をすると、しなかった日に比べて睡眠中の最低心拍およびSDNNが高くなり、RMSSDが低くなっています。このことから、飲酒した日は自律神経が交感神経優位な状態になりやすく、睡眠の質が低下している可能性があります。

また、大量の飲酒をすると中量以下の飲酒時に比べて睡眠中の最低心拍が高くなり、翌日の歩行ペースが遅くなります。ここからは、大量の飲酒は特に睡眠および翌日の活動に大きな影響を与える可能性が読み取れます。
一方で、少量の飲酒では飲酒していない場合との差が少ないことや、むしろ活動量が増える人もおり、個人差が大きい可能性が考えられます。

4.まとめ

ウェアラブルデバイスのデータを使用することで、飲酒日を検出する指標の開発と、飲酒の影響の評価を行うことができました。

さらなる研究によって、より安全で確かな技術を確立してゆくことが必要とされています。技術開発が進めば、アンケートの併用がなくともより手軽に飲酒の影響についての解析ができるようになるかもしれません。
ウェアラブルデバイスを活用することで日々の行動を振り返る負担を減らしつつ、健康状態への理解を深めることができます。
こうした技術が、一人ひとりの生活を少し便利に、そして健やかにする助けになることを期待しています。

5.今後の展望

飲酒を検知する技術については、まだ様々な方々を対象とした多面的な検証と安全性の評価を必要としています。

本テーマに関する共同開発にご関心のある方がいらっしゃいましたら、お気軽に、お問い合わせフォームよりテックドクターまでご連絡ください。

似顔絵似顔絵
書いた人:坂本、藤本

FlutterのFlavor設定完全ガイド:iOS/Android対応のマルチ環境を構築する

モバイルアプリ開発では、開発・ステージング・本番など複数の環境を切り替えて運用することがあります。
Flutterでのアプリ開発において、その運用に役立つのがFlavorです。

Flavorは、アプリを複数の環境ごとに設定を切り替えるための仕組みです。
例えば「開発環境ではテスト用API、本番環境では本番APIを使いたい」といった場合に、Flavorを使うことでビルド時に環境を選択し、自動的に異なる設定を適用することができます。

この記事では、そんな便利なFlavorの設定方法をご紹介します。iOS/Androidの両方に対応しているので、マルチプラットフォーム向けアプリの開発に役立ててください。

この記事はこんな人向けです
  • Flutterで複数環境のアプリビルドを構築したい方
  • iOS/Android両方のFlavor設定を網羅的に知りたい方
  • Firebase等の外部サービスを環境ごとに切り替えたい方

 

1. 概要:Flavorとは

Flutterでは「Flavor」という仕組みを使って、同一コードベースから異なる設定のアプリをビルドできます。

Flavorを使うことで、例えばこういった設定を環境ごとに切り替えられます:

  • アプリ名・アイコン:開発版と本番版を異なる見た目にすることで視覚的に区別できます
  • Bundle ID / Application ID:同一端末に複数環境のアプリを共存させることができます
  • APIエンドポイント:環境ごとに接続先の自動切り替えが可能です
  • Firebase設定:環境ごとに異なるFirebaseプロジェクトを使用できます
プラットフォームごとの実現方法

AndroidとiOSそれぞれにおいて、Flavorを定義する方法は下記のとおりです。

  • Android:Gradleの productFlavors 機能を使用
  • iOS:XcodeのSchemeとBuild Configurationを使用

それぞれ、詳しいやり方は記事中で説明していきます。

本記事で構築する環境(Flavor)

本記事では、Flutter公式ドキュメントを参考に、4環境(local、development、sandbox、production)のFlavor設定手順を解説します。

Flavor 用途 アプリ名 Bundle ID /
Application ID
local ローカル開発 [LOC] アプリ名 *.local
development 開発サーバー接続 [DEV] アプリ名 *.development
sandbox ステージング環境 [SAN] アプリ名 *.sandbox
production 本番リリース アプリ名 (サフィックスなし)

では、具体的な手順を紹介していきましょう。

2. Android側のFlavor設定

まずはAndroidでの構成方法です。
Gradleの productFlavors 機能を使用し、シンプルに定義できます。

build.gradleの設定

android/app/build.gradleflavorDimensionsproductFlavors を追加します。

android {
   // ... 既存の設定

   flavorDimensions += "default"

   productFlavors {
       create("local") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "[LOC] アプリ名",
           ]
           applicationIdSuffix = ".local"
       }
       create("development") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "[DEV] アプリ名",
           ]
           applicationIdSuffix = ".development"
       }
       create("sandbox") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "[SAN] アプリ名",
           ]
           applicationIdSuffix = ".sandbox"
       }
       create("production") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "アプリ名",
           ]
           applicationIdSuffix = ""
       }
   }
}


ポイント解説

  • flavorDimensions:Flavorのグループを定義します。複数の軸でFlavorを管理する場合に使用します(例:有料版/無料版 × 環境)
  • applicationIdSuffix:ベースのApplicationIDに追加されるサフィックスです。これにより同一端末に複数環境のアプリをインストール可能にできます。
  • manifestPlaceholders:次に説明するAndroidManifest.xmlで参照するための変数を定義します。
AndroidManifest.xmlでの変数参照

くわえて、android/app/src/main/AndroidManifest.xml でplaceholderを参照します。

<application
   android:label="${appName}"
   android:name="${applicationName}"
   android:icon="@mipmap/ic_launcher">
   <!-- ... -->
</application>

Android側の設定手順は以上です!

3. iOS側のFlavor設定

iOSのFlavor設定はAndroidより少し複雑です。
XcodeのSchemeとBuild Configurationを使用します。まずBuild ConfigurationとSchemeを作成し、それぞれを紐づけたあと、具体的な設定を定義していく流れです。

Build Configurationの作成

Xcodeで`Runner.xcodeproj`を開き、以下の手順でBuild Configurationを作成します。

  1. プロジェクトナビゲータで Runner プロジェクトを選択
  2. Info タブ → Configurations セクションを開く
  3. 「+」ボタンで以下のConfigurationを追加:
Configuration名 複製元
Debug-local Debug
Debug-development Debug
Debug-sandbox Debug
Debug-production Debug
Release-local Release
Release-development Release
Release-sandbox Release
Release-production Release
Profile-local Release
Profile-development Release
Profile-sandbox Release
Profile-production Release

重要:Configuration名は必ず {BuildType}-{flavor名} の形式にしてください。Flutterがこの命名規則でFlavorを識別します。

Schemeの作成

次に、各FlavorごとにSchemeを作成します。

  1. ProductSchemeNew Scheme… を選択
  2. Scheme名を入力(例:localdevelopmentsandboxproduction

重要:Scheme名は小文字で、Flavor名と完全に一致させるようにしてください。

SchemeとConfigurationの紐付け

作成したSchemeを、Configurationに割り当てていきます。

  1. ProductSchemeManage Schemes… を開く
  2. 各Schemeを選択し「Edit...」をクリック
  3. 以下のように各アクションにConfigurationを割り当て:


localスキームの例:

アクション Build Configuration
Run Debug-local
Test Debug-local
Profile Profile-local
Analyze Debug-local
Archive Release-local


重要:全てのSchemeで「Shared」にチェックを入れてください。これにより.xcscheme ファイルがリポジトリにコミットされ、チームで共有できます。


作成されたSchemeファイル( ios/Runner.xcodeproj/xcshareddata/xcschemes/ 配下)の例:

<?xml version="1.0" encoding="UTF-8"?>
<Scheme version = "1.7">
  <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES">
     <!-- 省略 -->
  </BuildAction>
  <TestAction buildConfiguration = "Debug-local">
     <!-- 省略 -->
  </TestAction>
  <LaunchAction buildConfiguration = "Debug-local">
     <!-- 省略 -->
  </LaunchAction>
  <ProfileAction buildConfiguration = "Profile-local">
     <!-- 省略 -->
  </ProfileAction>
  <AnalyzeAction buildConfiguration = "Debug-local">
  </AnalyzeAction>
  <ArchiveAction buildConfiguration = "Release-local" revealArchiveInOrganizer = "YES">
  </ArchiveAction>
</Scheme>

 

Build Settingsの設定

各ConfigurationでのBundle IDやアプリ表示名を設定します。

  1. Runner ターゲット → Build Settingsタブを開く
  2. 「+」→「Add User-Defined Setting」で以下のカスタム設定を追加:

DISPLAY_PRODUCT_NAME_PREFIX(アプリ名のプレフィックス):

Configuration
Debug-local [LOC]
Debug-development [DEV]
Debug-sandbox [SAN]
Debug-production (空)
Release-* 同上
Profile-* 同上


PRODUCT_BUNDLE_IDENTIFIER:

Configuration
*-local com.example.app.local
*-development com.example.app.development
*-sandbox com.example.app.sandbox
*-production com.example.app

 

Info.plistの設定

ios/Runner/Info.plist でBuild Settings変数を参照します。
これにより、アプリのビルド時にはConfigurationごとに設定した値が使用されるようになります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <!-- アプリ表示名:プレフィックス + アプリ名 -->
   <key>CFBundleDisplayName</key>
   <string>$(DISPLAY_PRODUCT_NAME_PREFIX)アプリ名</string>

   <!-- Bundle IDはBuild Settingsから自動取得 -->
   <key>CFBundleIdentifier</key>
   <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

   <!-- その他の設定... -->
</dict>
</plist>

iOS側の設定は以上で完了です!

4. Firebase設定(FlutterFire CLI)

複数の設定ファイルを生成することで、Flavorごとに異なるFirebaseプロジェクトを使用することができます。ここではその設定方法を紹介します。

事前に準備しておくこと

  • 各Flavorに対応したFirebaseプロジェクトが作成済み
  • Firebase CLIとFlutterFire CLIがインストール済み
# Firebase CLIのインストール
npm install -g firebase-tools
firebase login

# FlutterFire CLIのインストール
dart pub global activate flutterfire_cli

 

Flavor別の設定ファイルを生成

各Flavorに対して flutterfire config コマンドを実行し、設定ファイルを生成します。
こうすることで、FlutterFire CLIはDart設定ファイルの生成に加えて、iOS/Android両方の設定ファイルを適切な場所に配置し、必要なBuild Phase設定も自動で行います。

# development環境の設定
flutterfire config \
 --project=your-project-development \
 --out=lib/config/firebase/firebase_options_development.dart \
 --ios-bundle-id=com.example.app.development \
 --ios-out=ios/flavors/development/GoogleService-Info.plist \
 --android-package-name=com.example.app.development \
 --android-out=android/app/src/development/google-services.json

# sandbox環境の設定
flutterfire config \
 --project=your-project-sandbox \
 --out=lib/config/firebase/firebase_options_sandbox.dart \
 --ios-bundle-id=com.example.app.sandbox \
 --ios-out=ios/flavors/sandbox/GoogleService-Info.plist \
 --android-package-name=com.example.app.sandbox \
 --android-out=android/app/src/sandbox/google-services.json

# production環境の設定
flutterfire config \
 --project=your-project-production \
 --out=lib/config/firebase/firebase_options_production.dart \
 --ios-bundle-id=com.example.app \
 --ios-out=ios/flavors/production/GoogleService-Info.plist \
 --android-package-name=com.example.app \
 --android-out=android/app/src/production/google-services.json

 
ここで使用する主要なオプション

オプション 説明
--project FirebaseプロジェクトID
--out Dart設定ファイルの出力先
--ios-bundle-id iOSのBundle ID
--ios-out GoogleService-Info.plistの出力先
--android-package-name AndroidのパッケージID
--android-out google-services.jsonの出力先

 

生成されるファイル構成

コマンド実行後、以下のファイルが生成されます:

lib/config/firebase/
├── firebase_options_development.dart
├── firebase_options_sandbox.dart
└── firebase_options_production.dart

ios/flavors/
├── development/
│   └── GoogleService-Info.plist
├── sandbox/
│   └── GoogleService-Info.plist
└── production/
   └── GoogleService-Info.plist

android/app/src/
├── development/
│   └── google-services.json
├── sandbox/
│   └── google-services.json
└── production/
   └── google-services.json

つづいて、各FirebaseプロジェクトをDartのコード内から扱うための設定をします。

Flavor enumの定義

lib/config/env/flavor.dart でFlavorを定義します。Dart 3以降ではenumに直接メソッドやgetterを定義できます。

import 'package:firebase_core/firebase_core.dart';
import 'package:your_app/config/firebase/firebase_options_development.dart'
   as development;
import 'package:your_app/config/firebase/firebase_options_production.dart'
   as production;
import 'package:your_app/config/firebase/firebase_options_sandbox.dart'
   as sandbox;

enum Flavor {
 local,
 development,
 sandbox,
 production;

 /// Dart define から Flavor を取得
 static const _flavorStr = String.fromEnvironment('FLAVOR');
 static Flavor get fromDartDefine => Flavor.values.byName(_flavorStr);

 /// Firebase設定
 FirebaseOptions get firebaseOptions {
   switch (this) {
     case Flavor.local:
     case Flavor.development:
       return development.DefaultFirebaseOptions.currentPlatform;
     case Flavor.sandbox:
       return sandbox.DefaultFirebaseOptions.currentPlatform;
     case Flavor.production:
       return production.DefaultFirebaseOptions.currentPlatform;
   }
 }
}

設定自体はこれで完了です。

main.dartでの使用例

実際にDartのコード内でFirebaseを扱うにはこのようにします。

void main() async {
 WidgetsFlutterBinding.ensureInitialized();

 final flavor = Flavor.fromDartDefine;

 // Flavorに応じたFirebase初期化
 await Firebase.initializeApp(
   options: flavor.firebaseOptions,
 );

 runApp(MyApp(flavor: flavor));
}

以上で、Firebaseに関する設定は完了です。

5. アプリアイコンの切り替え

flutter_launcher_icons パッケージを使用して、Flavor別のアイコンを生成することもできます。
各環境用のアプリを視覚的に区別できるようになり、便利です。

アイコン画像の配置

各Flavor用のアイコン画像を作成し、assets/app_icons/ ディレクトリに配置します。

assets/
└── app_icons/
   ├── app_icon_local.png
   ├── app_icon_development.png
   ├── app_icon_sandbox.png
   └── app_icon_production.png

アイコン画像は1024x1024px以上の正方形PNGファイルを推奨します。

設定ファイルの作成

各Flavor用の設定ファイルを、プロジェクトルートに作成します。

flutter_launcher_icons-local.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_local.png"
 android: true
 ios: true
 remove_alpha_ios: true

 
flutter_launcher_icons-development.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_development.png"
 android: true
 ios: true
 remove_alpha_ios: true

 
flutter_launcher_icons-sandbox.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_sandbox.png"
 android: true
 ios: true
 remove_alpha_ios: true

 
flutter_launcher_icons-production.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_production.png"
 android: true
 ios: true
 remove_alpha_ios: true

 

アイコン生成

最後に、下記のコマンドを実行するとアイコンの生成が行われます。

# 全Flavorのアイコンを生成
dart run flutter_launcher_icons

# 特定Flavorのみ生成
dart run flutter_launcher_icons --flavor development

以上で、全ての設定が完了しました。

7. 実行方法

最後に、ここまでで設定したFlavorを使い、実際にアプリを実行/ビルドする方法を紹介します。

コマンドラインから実行
# local環境で実行
flutter run --flavor local --dart-define=FLAVOR=local

# development環境で実行
flutter run --flavor development --dart-define=FLAVOR=development

# sandbox環境でリリースビルド
flutter run --flavor sandbox --dart-define=FLAVOR=sandbox --release

# production環境でアーカイブ
flutter build ios --flavor production --dart-define=FLAVOR=production
flutter build appbundle --flavor production --dart-define=FLAVOR=production

 

Makefileでの管理

毎回コマンドを打つのは面倒なので、Makefileにまとめると便利です。

run: ## Run app. Pass F={local,development,sandbox,production}, M={profile,release} (optional)
ifeq (${M},)
 flutter run --flavor ${F} --dart-define=FLAVOR=${F}
else
 flutter run --flavor ${F} --dart-define=FLAVOR=${F} --${M}
endif

 
使用例:

make run F=development        # デバッグモード
make run F=production M=release  # リリースモード

 

IDE(VS Code / Android Studio)での実行

各IDEで実行する場合は、それぞれ設定が必要です。

VS Code(設定)
.vscode/launch.json に設定を追加:

{
 "version": "0.2.0",
 "configurations": [
   {
     "name": "Local",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "local", "--dart-define=FLAVOR=local"]
   },
   {
     "name": "Development",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "development", "--dart-define=FLAVOR=development"]
   },
   {
     "name": "Sandbox",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "sandbox", "--dart-define=FLAVOR=sandbox"]
   },
   {
     "name": "Production",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "production", "--dart-define=FLAVOR=production"]
   }
 ]
}


上記の設定により、Run and Debugから構成名を選択して実行できるようになります。

Android Studio(設定)

  1. 上部メニューから RunEdit Configurations... を選択
  2. 左上の「+」ボタンをクリックし、「Flutter」を選択
  3. 各Flavor用の設定を作成:
項目 設定値(developmentの例)
Name Development
Dart entrypoint lib/main.dart
Additional run args --dart-define=FLAVOR=development
Build flavor development

4. 同様に local 、sandbox、production 用の設定も作成

これにより、ツールバーのドロップダウンから実行したいFlavorを選択して実行できるようになります。

8. まとめ


本記事では、FlutterのFlavor設定をiOS/Android両対応で解説しました。

ポイントのおさらい
  1. Android:build.gradleproductFlavors で設定する
  2. iOS:Xcode Scheme + Build Configurationで設定する。命名規則が重要
  3. Firebase:FlutterFire CLIで各Flavorの設定ファイルを生成する
  4. Dart:--dart-define でFlavorを渡し、enumで管理する

Flavorを適切に設定することで、開発効率とリリース品質の両方を向上させることができます。

行動を分解すると、課題や解決策が見えてくる~「プロダクトデザインの第一歩」体験ワークショップ

こんにちは、プロダクトデザイナーの庄司です。
今回は、社内で「プロダクト開発を皆に身近に思ってもらう」を目的として開催したワークショップについて紹介します。

弊社では毎月末にその月の成果等を発表する締め会(*1)を行っています。

その中に毎月テーマを変えた全社ワークショップの時間があり(*2)、先日は私がファシリテーターとなりプロダクトデザインに関するワークショップを開催しました。

*1 参考:文化は育てるもの。また“締め会”で会いましょう
*2 参考:問いを囲んで、チームを耕す月イチワークショップ

デザインとは設計である

もともとは、毎月のワークショップを主催している組織開発のメンバーから打診を受けたのがきっかけです。

「新規プロダクトのリリース予定もあることだし、プロダクト開発を皆に身近に思ってもらえるように、デザインのワークショップを行いませんか?」というオファーでした。

説明スライドのキャプチャ

  1. 新規アプリの開発が進行中だが、社内のデザインへの理解がまだ浅い。
  2. プロダクト開発全体をデザインの観点で巻き込むことが必要。

このような課題意識を受け、情報設計の体験をしてもらうワークショップを考えました。
プロダクトデザインは、デザインという言葉のイメージから、どうしても「見た目を整える仕事でしょ?」と思われがち。確かにそういう側面もあるのですが、実際にはプロダクトの体験やそのプロダクトがもたらす課題解決まで含めた情報設計を行う仕事です。デザインとは設計であると理解してもらうことが第一歩。

そんなわけで、学生時代に「インフォメーション・アーキテクト」の授業で行ったワークを大いに思い出しつつ、多少の応用を加えたワークショップを考えました。

このワークショップは、2つのパートで構成しました。
①行動を洗い出す
②課題を見つけ、ソリューションを考える
順に紹介していきます。

①行動を洗い出してみる

例として「カップやきそばを作って食べる」場合。
その中にあるたくさんの行動を、ひとつひとつ書き出してみます。
「ビニール包装を取る」「蓋を開ける」「あと入れの袋を取り出す」……。

実際にやってみると、全てを書き出すのは果てしない作業に思えてくるかもしれません。
一つの行動として扱っているものでも、考えてみると実は無数の行動の連なりであることに気づくからです。 また、同じ「ビニール包装を取る」にしても爪を立てるのかハサミを使うのかなど多くの派生が生まれうるでしょう。

しかし今回はざっくりこれだけの行動の連なりで体験ができているということが分かればいいです。例えばこのようなサンプルを作成しました。

カップ焼きそばを作って食べる工程の図。11工程ある
良い洗い出しの例
カップ焼きそばを作って食べる工程の図。2工程
大雑把すぎるのは×

個人ワーク「歓迎会ランチの予約をするときの行動」

ワークショップでは、このパートは個人ワークとして行いました。時間は10分間。

テーマは「歓迎会ランチの予約をする」に統一しました。テーマを自由設定にせず統一することによって、あとで比較しやすく、人によって行動を分解する粒度の違いも見えやすいからです。
またより具体的にイメージするため、「ランチ参加人数10名」「12:00〜」「オフィス周辺」「予算2,000円以下」といった条件も付け加えています。

ワークはmiroにスペースを作って行いました(FigJamなどの他ツールや、紙の付箋でも実施できます)。
自分の名前のあるエリアに、一つの行動につき一つの付箋を貼っていきます。

ワークの結果

ワークの結果

まず最初の行動を見てみても、「何を食べるか考える」「アレルギーについて聞く」、はたまた「社内のグルメの●●さんにまかせる」のようなユニークなものまで個性が出ます。

終わらせ方についても、「予約」で終わる人もいれば、「予約完了のslackを送る」「カレンダーにお店のURLを貼る」など、どこまでを一連の行動とみなしているかが可視化されて非常に興味深かったです。

②課題を見つけ、ソリューションを考える

さて、行動を書き出していくにつれ、いろいろなことが見えてきます。
「この行動の不便を解消するためにこの機能が生まれたのでは?」という気づきを得たり、「こうすることで解決できるのでは?」と課題の解決法を思いついたり。

たとえばカップ焼きそばの例であれば、「お湯を捨てる」時にうっかり麺も一緒に流してしまう……という不便が思いつきますし、そのためのソリューションとして湯切りの穴が開発されたのだろう、ということも想像できます。

説明用スライドのキャプチャ

別の例として、情報デザインの大切さとして私がよく引き合いに出す事例があります。1996年に発売された日立の冷蔵庫、「野菜中心蔵」です。

それ以前の冷蔵庫は野菜室が一番下なのが普通でした。しかしこの製品は野菜室が真ん中にあります。使用シーンの行動を洗い出していくにつれ、「野菜を取り出すためにしゃがまなければいけない」という課題(ペイン)に行きつき、その解決案として野菜室を中心におくという手段を取ったであろうことが想像できます。

説明用スライドのキャプチャ

こういった気付きは、日常の中にも隠れています。

「帰宅」の行動を洗い出してみる
→「手を洗う」際に毎回「泡立てる」という行動が発生する
→「泡立てる」手間を解消するための、プッシュすると泡で出てくるディスペンサー

「webで新規会員登録」の行動を洗い出してみる
→住所を入力するフォームで、郵便番号にくわえ全ての住所を手入力する行動が発生する
→郵便番号を入力すれば、地名まで入力された状態になる自動入力フォーム

などなど。

ちなみに、ここに行動に付随する感情変化を付け加えていけば、いわゆるカスタマージャーニー、ペイシェントジャーニー等を作成することができます。(が、長くなるのでそれに関してはまた別の機会に...)

グループワーク「歓迎会ランチ予約のソリューションを考える」

ワークショップでは、このパートは40分間のグループワークとして行いました。
ランダムに指定した4~5人ずつのグループに分かれ、「行動に対するソリューションを考えてみる」をテーマにディスカッションしてもらいます。

まずは個人ワークで自分が書き出したものをグループ内で共有しあいます。
その後、みんなに共通していた行動や、書き出してみてストレスがあると感じた行動、この行動に対してこんな機能や仕組みがあったら問題が解決されるのではないか……などを話しあってもらいました。
実現可能かどうかは置いておいて、あくまで自由に話し合ってもらうのがポイントです。

最後にそれぞれのグループの代表に、どんな案がでたのかを軽く紹介してもらいました。
「多数決が決まったら勝手に予約されるシステム」「AIエージェントで条件の店をリスト化」のような開発者らしい意見が出たり、「歓迎会はいつも同じお店にする」といったそっちで解決するんかい!と笑ってしまいたくなるようなアイディアも出てきて、大変興味深かったです。

ワークショップを終えて

今回のワークショップは、情報設計というデザインの基本の部分を身近な行動に落とし込んで体験してもらおうというものでした。

どんな行動でも、洗い出し細分化していくと、無意識に行っていた無数の行動が存在することに気づきます。そしてそこには必ずユーザーの体験を改善するヒントが隠れています。

デザインとは何かを体験する目的のみならず、サービスやプロダクトを新しく考える時、はたまたそれらのUXを改善したい時など、行き詰まったらまずは「行動を洗い出してみる」ワークをぜひやってみてください。


似顔絵
書いた人:庄司

ホルモン治療薬が身体のリズムに与える影響:女性たちの半年間のデータから見えたこと

こんにちは、データサイエンスチームの瀬川です。

テックドクターでは、女性社員のみで構成された「Ladynamic」プロジェクトを通して、女性の視点に立った課題提起とデータ解析を目指しています。同プロジェクトでは女性の健康に関する様々なデータ分析を行っており、これまでのブログ記事でもいくつかの事例をご紹介しました。

techblog.technology-doctor.com

techblog.technology-doctor.com

そんなLadynamicプロジェクトから、今回は基礎体温についてのお話です。

基礎体温は排卵の前には低く、排卵の後には高くなるというように、2つの段階に分かれます(このような性質を「二相性」と呼びます)。また、この変化には月経周期と連動した周期性があります。この連動を利用して月経周期の把握ができるため、基礎体温を毎日記録されている方もいらっしゃるかと思います。

基礎体温のニ相性。引用元:病気が見えるvol.9 婦人科・乳腺外科(第4版)(メディックメディア)

スマートウォッチ等の一般的なウェアラブルデバイスで基礎体温を記録することは、現時点ではできません(※)。代わりにウェアラブルデバイスから得られる別のデータを使用して、この月経周期と連動した周期性と同様の傾向は確認できないでしょうか?

※皮膚温であればFitbit等で記録が可能ですが、その違いについては後ほど触れます。また一般的なスマートウォッチ等ではない、基礎体温測定用の専用のウェアラブル機器は存在します。

本稿では、この疑問を解明するため、月経周期とウェアラブルデバイスデータで得られた心拍数・脈拍数との関連を探っていきます。また、ホルモン製剤を服用している人と服用していない人とのデータから、服薬がその関連にどういった影響を与えるかも調べていきたいと思います。

イメージイラスト

ウェアラブルデータが月経周期と関連するかどうか

まずはウェアラブルデバイスのデータが月経周期と連動した周期性(二相性)を示すかどうかを検証しました。

【対象データ】

女性ホルモン製剤を服用していない女性社員(Aさん)の約半年間のデータを可視化しました。

使用ウェアラブルデバイス:Fitbit
指標:安静時心拍数、起床直前30分(基礎体温の計測タイミングに合わせて)の脈拍数、皮膚温

【結果】

グラフはそれぞれの指標の1日ごとのデータを点で表し、7日移動平均線を表示しています。月経開始日は縦の赤線で示しました。

グラフ

分析の結果、安静時心拍数と起床直前30分の脈拍数については、月経開始日に向けて上昇し、その後低下するという傾向が見られました。この二つの値については、月経周期と連動した周期性があると言えそうです。

一方、皮膚温では心拍数・脈拍数データほどの明確な周期性は確認できませんでした。皮膚温も基礎体温と同様に周期性を示すかと思われましたが、Fitbitを装着した手の位置(布団の中か外か)や室温といった外部環境の影響を受けやすく、月経周期の影響が反映されにくい結果になったと考えられます。

ホルモン剤を服用することによって周期性が変化するのかどうか

次に、ウェアラブルデバイスの心拍数データと月経周期との関係が、女性ホルモン製剤を服用している方々でどのように異なるかを検証しました。

【対象データ】

2名の女性社員から得られた約半年間のデータが対象です。
先ほどの検証で月経周期と特に明確な関連性が見られた、安静時心拍数に注目して分析を進めました。

  • Aさん: 何も服薬していない方
  • Bさん: ジエノゲストを服用している方

【ジエノゲストの作用】

可視化したデータを見る前に、ジエノゲストが体にどのような作用をもたらすのかを簡単に説明します。

  • ジエノゲスト:

子宮内膜症や子宮腺筋症に伴う痛みの治療に使用され、毎日服用します。
女性ホルモンの一種であるプロゲステロン受容体に対して似た働きをし、卵巣機能抑制および子宮内膜細胞の増殖抑制によりプロスタグランジン産生を抑制することから、月経困難症に対する有効性を示すと考えられます。また、LHサージを抑制し、排卵抑制作用を示すと考えられます(1)。排卵が抑えられ月経が来なくなります。

ジエノゲストの服用は、女性の身体の周期性にどのように影響を与えるのでしょうか。

【結果】

2人の安静時心拍数の経過を可視化しました。

グラフ

グラフからは、以下の特徴が見られました。

服用なし(Aさん):

先ほど見たとおり、安静時心拍数が月経開始日に向けて上昇し、その後低下するという周期性が見られます。女性ホルモンの変動が心拍数にも影響を与えている可能性があります。

ジエノゲスト服用者(Bさん):

Aさんよりグラフの変動幅が少なく、明確な周期性は見られませんでした。
ジエノゲストは毎日服用することから服用による女性ホルモン量の変動が少なくなります。これにより、安静時心拍数の周期性が、服用なしのAさんよりもみられないのではないかと考えています。

まとめ

今回の調査から、以下のようなことがわかりました。

  • 安静時心拍数と起床直前30分の脈拍数については、月経開始日に向けて上昇し、その後低下するという傾向が見られ、月経周期と連動した周期性があると言えそうです。
  • ジエノゲストを服用している方の心拍数に周期性が見られなかったことは、薬が女性ホルモンを通じて身体の周期性にも影響を与えているという可能性を示唆しているかもしれません。

ジエノゲストなど女性ホルモン剤の服用者に関する基礎体温や体調のデータはまだ多くありません。そんな中、ウェアラブルデバイスのデータは、毎朝手動で計測しなければいけない基礎体温と違って継続的に女性の体調を記録できるという点から非常に意義があると考えられます。今回の可視化から得られた「女性ホルモンが生体へ影響を与える可能性がある」という結果も、今後の女性の体調管理に役立つかもしれません。

今回の調査は少数のデータに基づいたものであり、今後医学的観点からの考察をより深める必要があります。今後も、より多くのデータを集め、女性の健康に関する課題を解き明かし、よりパーソナライズされた健康管理や治療法の開発に貢献していきたいと考えています。

DDDにCQRSをどう組み込むか~バックエンドアーキテクチャ設計時の考え方

はじめに

こんにちは。テックドクターでバックエンドエンジニアをしている筧と申します。

新規プロダクトのバックエンドで、DDD (Domain-Driven Design) と CQRS (Command Query Responsibility Segregation) を組み合わせたアーキテクチャを採用しました。

DDDの本や記事は、Eric Evans著『Domain-Driven Design』や『実践ドメイン駆動設計』など様々あります。CQRSについてもMartin Fowler氏のCQRSの解説記事などがあります。しかし、DDDにCQRSをどう組み込んでいったかという話はあまり見かけません。

この点について以前より情報収集や試行錯誤を重ねていましたが、今回のプロダクトでようやく納得のいく形で実装ができました。この記事ではそのポイントをご紹介します。特にCQRSを具体的に実装していくApplication層を中心に、他の層とのデータのやりとりや責務分担について詳しく説明したいと思います。

この記事の想定読者とゴール

この記事は、以下のような方を想定しています:

  • DDDの基本(Entity、Value Object、Repository等の概念)は理解している
  • 実際のコードでCQRSをどう導入すればよいかわからない

記事を読み終わったときに、Application層のCommand/QueryHandlerの実装や、他の層とのデータのやりとりを具体的にイメージできるようになることがゴールです。

CQRSとは何か

まず、この記事でのCQRSの定義を明確にしておきます。

CQRSは、データを変更する操作(Command)と、データを読み取る操作(Query)を分離するパターンです。通常のCRUDではデータモデルが読み書き共通ですが、CQRSでは以下のように分けます:

  • Command側(書き込み): ビジネスルールの検証を重視。Entityを経由してデータを更新
  • Query側(読み込み): パフォーマンスと利便性を重視。最適化されたクエリで直接DTOにマッピング

この分離により、それぞれの操作に最適な実装を選択できるようになります。

全体像:レイヤー構成の概略と責務の割り当て

今回のプロダクトは一種のダッシュボードシステムで、データの集計・可視化を行うほか、組織やユーザー情報等の登録も行います。
レイヤー構成は以下の5層としました。

層名 説明
Presentation層 ← API エンドポイント(薄い層)
UseCase層 ← ビジネスフロー + 認可制御
Application層 ← Command/Query Handler(純粋なCRUD)
Domain層 ← Entity、Value Object、Repository Interface
Infrastructure層 ← Repository実装、Query Service

各層の責務を簡単に整理します。

Presentation層はHTTPリクエスト/レスポンス変換のみを担当します。ビジネスロジックは持たせません。

UseCase層は認可チェックや、複数のCommand/Queryを組み合わせたビジネスフローを扱います。

Application層は純粋なCRUD操作のみで、認可処理は持ちません。

Domain層はビジネスルールの中核を担い、他の層に依存しません。

Infrastructure層はデータベースや外部サービスとの実際のやりとりを担当します。

Core層もありますが、DIコンテナや共通設定を担う補助的な層なので本稿では詳しく扱いません。

特に重要なのはUseCase層とApplication層の境界です。Application層のCommand/QueryHandlerは認可処理を持たず、純粋にドメインロジックに集中します。一方、認可や監査ログといった横断的な関心事はUseCase層で扱います。

この設計にした理由は、別プロダクトでUseCase層とその下のService層(今回のApplication層に相当)を分けて成功した経験があったからです。UseCase層にCQRSのCommand/Queryを直接配置すると、認可処理とデータ操作のロジックが混在してしまうため、Application層として分離しました。

Application層におけるCQRS実装

Application層では、Command(書き込み)とQuery(読み込み)を明確に分離しています。

Commandの実装例:組織を作成する

例として、組織を作成するCommandHandlerを見てみましょう。以下は実際のプロダクトのコードを簡略化したサンプルです。

class CreateOrganizationCommand(BaseModel):
    """組織作成コマンド"""
    organization_id: OrganizationId
    name: OrganizationName
    # ... その他のフィールド

class CreateOrganizationCommandResult(BaseModel):
    """組織作成結果"""
    organization: Organization
    created: bool

class CreateOrganizationCommandHandler(
    ICommandHandler[CreateOrganizationCommand, CreateOrganizationCommandResult]
):
    """組織作成コマンドハンドラー"""

    def __init__(self, organization_repository: IOrganizationRepository) -> None:
        self._organization_repository = organization_repository

    async def handle(
        self, command: CreateOrganizationCommand
    ) -> CreateOrganizationCommandResult:
        # 1. 既存チェック
        if await self._organization_repository.exists(command.organization_id):
            raise EntityAlreadyExistsException(...)

        # 2. ドメインモデルでビジネスルール検証
        organization = Organization.create(
            organization_id=command.organization_id,
            name=command.name,
            # ...
        )

        # 3. 永続化
        await self._organization_repository.save(organization)

        return CreateOrganizationCommandResult(
            organization=organization,
            created=True,
        )

このCommandHandlerで最も重要なのは、認可処理を一切持たない点です。「この組織を作成できる権限があるか?」といったチェックはUseCase層の仕事で、Application層は純粋に「組織の作成」というビジネスロジックに集中しています。

具体的な処理の流れは、入力としてCommand(必要なデータのみ)を受け取り、出力としてCommandResult(処理結果)を返します。内部では、ドメインモデルのOrganization.create()を使ってビジネスルール検証を行い(例えば、OrganizationNameというValue Objectで組織名の長さや形式をチェックしています)、最後にRepositoryで永続化します。

こうすることでHandlerがシンプルになり、テストも書きやすくなります。認可を気にする必要がなく、Infrastructure層のDBセッションにも依存しないため、モックのRepositoryを渡すだけで単体テストができました。

Queryの実装例:組織を取得する

次に、組織を取得するQueryHandlerです。

class GetOrganizationQuery(BaseModel):
    """組織取得クエリ"""
    organization_id: OrganizationId

class GetOrganizationQueryResult(BaseModel):
    """組織取得結果"""
    organization: OrganizationDTO | None

class GetOrganizationQueryHandler(
    IQueryHandler[GetOrganizationQuery, GetOrganizationQueryResult]
):
    """組織取得クエリハンドラー"""

    def __init__(self, organization_repository: IOrganizationRepository) -> None:
        self._organization_repository = organization_repository

    async def handle(self, query: GetOrganizationQuery) -> GetOrganizationQueryResult:
        organization = await self._organization_repository.find_by_id(
            query.organization_id
        )

        if organization is None:
            return GetOrganizationQueryResult(organization=None)

        # Entity → DTOへの変換
        organization_dto = OrganizationDTO(
            organization_id=str(organization.organization_id.value),
            name=str(organization.name.value),
            created_at=organization.created_at,
            # ...
        )

        return GetOrganizationQueryResult(organization=organization_dto)

QueryHandlerで重要なのは、Entity→DTOの変換です。入力としてQuery(検索条件)を受け取り、出力としてQueryResult(DTO形式のデータ)を返します。責務はデータ取得とDTO変換のみで、Command側のようなビジネスルール検証は行いません。

DTOに変換する理由は、Presentation層で使いやすい形にするためです。Entityはビジネスロジックを持つ重いオブジェクトですが、DTOは単なるデータ転送用の軽いオブジェクトです。この変換をApplication層で行うことで、Presentation層はシンプルに保てます。

データモデルの使い分け

実装していて最も悩んだのが、「どのデータモデルをどこで使うか」でした。

当初、各データモデルの役割は ”なんとなく” 決まっていたものの、具体的なルールがありませんでした。例えば、DTOの使い回しや、概念の分離ができていなかったりしました。

最終的に、各層の境界を明確にするため、以下のようにデータモデルを整理しました:

データモデル 役割 配置場所 命名の由来
Command/Query リクエストデータ(入力) Application層 CQRSの概念からそのまま
CommandResult
/QueryResult
レスポンスデータ(出力) Application層 Commandの結果、Queryの結果という明確な名前
Projection Infrastructure
→Application層のデータ
Infrastructure層 CQRSの文献で使われている用語(後述)
DTO Application
→Presentation層のデータ
Application層 特に他の名前が思いつかなかった

Projectionという名前について補足します。当初はXXResultという名前も検討しましたが、CommandResult/QueryResultと名前が被ってしまうこと、そしてRepositoryの結果とQuery Serviceの結果の両方があり「どちらの名前を取るか」という論争が起きそうだったため不採用にしました。

CQRSの文献を調べたところ、読み取り側のデータモデルとして「Projection」という用語が使われていること(参考1参考2)がわかり、この名前を採用しました。

これらを明確に分けることで、各層の関心事が混ざらないようにできています。特に名前付けには苦労しましたが、役割が明確になってからはコードの見通しが格段に良くなりました。

UseCase+Infrastructure:認可とデータアクセスをどう接続したか

Application層のCommand/QueryHandlerは純粋なCRUD操作だけを扱うので、認可処理はUseCase層で行います。

UseCaseでの認可統合:Command実行前に権限チェック

UseCase層では、Application層のHandlerをラップして認可処理を追加します。実装例を見てみましょう。

class CreateOrganizationUseCase:
    """組織作成UseCase(認可付き)"""

    def __init__(
        self,
        permission_checker: PermissionChecker,
        create_organization_handler: CreateOrganizationCommandHandler,
    ):
        self._permission_checker = permission_checker
        self._create_organization_handler = create_organization_handler

    async def execute(
        self,
        request: CreateOrganizationRequest,
        user_claims: JWTClaims
    ) -> Organization:
        # 1. 認可チェック(UseCase層の責務)
        await self._permission_checker.verify_role(user_claims, required_roles=["org-admin"])

        # 2. RequestからCommandへの変換
        command = CreateOrganizationCommand(
            organization_id=OrganizationId.generate(),
            name=OrganizationName(request.name),
            # ...
        )

        # 3. Application層のHandlerを実行
        result = await self._create_organization_handler.handle(command)

        return result.organization

このように、認可チェックとCommand実行を分離することで、いくつかのメリットがあります。

まず、Application層はビジネスロジックに集中できます。「組織を作成する」というドメインロジックに認可処理が混ざらないので、コードが読みやすくなります。

次に、認可ロジックを一箇所に集約できます。権限チェックの方法を変更したいとき、UseCase層だけ修正すれば済みます。

そして何より、テストが書きやすくなります。Application層はビジネスロジックの単体テストに集中でき、UseCase層は認可のテストとして分離できました。

認可処理の設計

認可処理の実装について、今回のプロダクトでは細かいロール設定をバックエンド側で担当することにしました。これにより、より柔軟な権限管理を実現しています。

Query Serviceでのデータ取得最適化

読み取り側において、複雑なクエリはInfrastructure層のQuery Serviceで最適化します。

Query Serviceとは、最適化されたクエリを実行するための読み取り専用のサービスです。Repositoryパターンとは異なり、複数のテーブルをJOINして1回のクエリで必要なデータを取得することに特化しています。

今回のプロダクトは、ダッシュボードシステムであり、複雑なデータの集計・可視化を行います。Repositoryパターンだけだと大量のクエリが発生し、パフォーマンスが低下する可能性があるため、CQRSを採用し、読み取り側ではQuery Serviceを使って1回のクエリで効率的にデータを取得しています。

具体例を見てみましょう(実際のプロダクトのコードを簡略化したものです):

class OrganizationQueryService:
    """組織クエリサービス(Infrastructure層)"""

    async def get_organization_with_stats(
        self, organization_id: str
    ) -> OrganizationProjection | None:
        # 最適化されたJOINクエリで一度に取得
        query = select(
            OrganizationModel.id,
            OrganizationModel.name,
            func.count(MemberModel.id).label('member_count'),
            func.count(ItemModel.id).label('item_count'),
        ).select_from(
            OrganizationModel
        ).outerjoin(
            MemberModel
        ).outerjoin(
            ItemModel
        ).where(
            OrganizationModel.id == organization_id
        ).group_by(OrganizationModel.id)

        result = await self._session.execute(query)
        row = result.first()

        if not row:
            return None

        # Projectionとして返す(DTO変換はQueryHandlerで)
        return OrganizationProjection(
            id=row.id,
            name=row.name,
            member_count=row.member_count or 0,
            item_count=row.item_count or 0,
        )

ここでのポイントは、Query ServiceがProjectionを返す点です。データフローは次のようになります:

QueryService(Projection) → QueryHandler(DTO) → UseCase → API(Response)

ProjectionはInfrastructure層からApplication層へのデータモデル、DTOはApplication層からPresentation層へのデータモデルと、役割が分かれています。

この設計にしたのは、Infrastructure層にドメイン知識を漏らさないためです。もしQuery ServiceがDTOを直接返すと、Infrastructure層がPresentation層の都合(どんな形式でデータを返すか)を知る必要があり、依存方向が逆転してしまいます。Projectionという中間データモデルを挟むことで、各層の責務を明確に保てています。

Query ServiceとRepositoryの使い分け

具体的な判断基準として、複数回JOINが必要かどうかを見ています。

単純な取得であればRepositoryを使いますが、組織に紐づく複数の関連データを同時に取得する場合など、複数のテーブルをJOINする必要がある場合はQuery Serviceを使います。これにより、クエリ実行回数を抑え、パフォーマンスを向上させています。

Presentation層とCore層の扱い

Presentation層:UseCaseに委譲するだけの薄い層

APIエンドポイントは本当に薄く、UseCaseに処理を委譲するだけです。

@router.post("", response_model=CreateOrganizationResponse, status_code=201)
@require_roles(["org-admin"])
async def create_organization(
    request: CreateOrganizationRequest,
    create_organization_use_case: Annotated[
        CreateOrganizationUseCase, Depends(get_create_organization_use_case)
    ],
    user_claims: Annotated[JWTClaims, Depends(get_current_user)],
) -> CreateOrganizationResponse:
    """組織作成エンドポイント"""
    try:
        # UseCaseに処理を委譲
        organization = await create_organization_use_case.execute(request, user_claims)

        # レスポンスに変換して返すだけ
        return CreateOrganizationResponse(
            organization_id=str(organization.organization_id.value),
            name=str(organization.name.value),
            # ...
        )
    except ValidationException:
        # エラーハンドリング(詳細は省略)
        raise

エンドポイントではビジネスロジックを持たず、HTTPの世界とアプリケーションの世界を繋ぐ責務だけに徹しています。実際のコードを見ても、UseCaseの実行結果をResponseに変換し、例外をHTTPステータスコードに変換しているだけです。

この設計にしてよかったのは、エンドポイントのテストがシンプルになったことです。HTTPリクエストの形式が正しいかだけをテストすればよく、ビジネスロジックのテストはUseCase層で完結します。

Core層

Core層はDI ContainerやJWT検証といった共通機能を提供していますが、アーキテクチャの中心ではないので本稿では詳しく触れません。重要なのは、各層の責務を明確に分けることです。

まとめ:実際に導入してみて感じたメリット

DDD+CQRSを実際のプロダクトに導入してみて、以下のメリットを感じました。

まず、認可ロジックの明確化です。UseCase層に認可処理を集約することで、「誰が何にアクセスできるか」が一目でわかるようになりました。

次に、テストの容易性です。Application層のHandlerがシンプルなので、単体テストが書きやすくなりました。認可処理と分離されているため、ビジネスロジックのテストに集中できます。また、Infrastructure層に依存しないため、DBセッションを用意する必要もありません。

そして、保守性の向上です。各層の責務が明確なので、変更の影響範囲が予測しやすく、コードの見通しが良くなりました。新しいメンバーがジョインしたときも、「この機能はどこを見ればいい?」という質問に明確に答えられるようになっています。

特にApplication層とUseCase層を分けたことで、ビジネスロジックと認可処理が混ざらず、それぞれに集中できるようになりました。当初は「層が多すぎて複雑では?」と心配したのですが、実装を進めるうちに明確なルールが整備され、責務が明確な分、むしろシンプルに感じています。

DDDもCQRSも学習コストは高いですが、実際に手を動かして設計してみると、その価値が実感できます。この記事が、これからDDD+CQRSを導入しようとしている方の参考になれば幸いです。

似顔絵
書いた人:筧

UI生成ツールv0でゼロイチ開発が変わった話~AI製プロトタイプの「捨てやすさ」が仮説検証を加速する

こんにちは。テックドクターでプロダクトマネージャーをしている田向です。

テックドクターでは各種AIツールを積極的に導入し、プロダクト開発のプロセス改善に取り組んでいます。

中でもUIデザイン生成ツールv0の導入は、単に個々のプロトタイプの実装を効率化するだけでなく、プロトタイプ開発全体を大きく加速させてくれました。

本記事では、v0の概要から具体的な導入効果までをご紹介します。

v0とは

v0は、Vercelが開発したAIツールです。自然言語のプロンプトや画像、Figmaファイルなどをもとに、WebやモバイルアプリのUIを簡単に作成できます。

v0.app

v0を選んだ理由

他にもUIを生成できるAIツールはありますが、今回は下記の理由からv0を選びました。

非エンジニアでも使える手軽さ

v0の大きな魅力は、自然言語のプロンプトでUIを生成できる点です。

大まかな指示でも一定の品質のものが作成できますが、プロンプトで要件を細かく指定したり参考画像を提示することで、さらに質が高く、意図に近いプロトタイプを仕上げることができます。

簡易的な指示で作成したサンプル。所要時間は1分程度!プロンプトは「プロジェクト管理のSaaSのダッシュボード画面を作成してください」

shadcn/ui との親和性

現在開発中のプロダクトでは、UIコンポーネントライブラリにshadcn/uiを採用しています。
v0はshadcn/uiを使ってUIを生成するため、プロトタイプとして作成したコードを本番の実装にも活かすことができ、開発のリードタイム短縮に繋がります。

※shadcn/uiとは

shadcn/uiは、Radix UIとTailwind CSSを組み合わせて構築された、カスタマイズ性に優れたコンポーネントライブラリです。
従来のライブラリと異なり、必要なコンポーネントのコードを直接自分のプロジェクトにインストールする方式のため、デザインの微調整や機能追加を自由に行えるのが特徴です。

v0の導入効果

v0を導入することで、様々なメリットがありました。

プロトタイプ開発の高速化とコスト削減

v0の導入前、プロトタイプを開発するには、①エンジニアやデザイナーに要件を伝え、②仕様書を作成し、③開発する……という工程が必要でした。関わる人数も多くなりますし、開発期間もアイデアから検証までに数週間かかることが当たり前でした。

v0導入後は、プロダクトマネージャーが自然言語の指示により自分でプロトタイプを作成できるようになりました。プロトタイプ作成にくわえフィードバックを受け改善するサイクルまでが1人で完結するようになったことで、プロトタイプ開発のコストが大幅に削減されました。

認識のズレと開発の手戻り防止

従来、仕様の共有はドキュメントや静的なワイヤーフレームで行っていましたが、静的なドキュメントだけでは関係者がそれぞれ頭の中で挙動を想像しながら議論することになり、認識のズレが生まれがちです。
その結果、実装後に「イメージと違う…」という手戻りが発生することも少なくありませんでした。

v0導入後はv0で作成したプロトタイプを中心に議論することで、認識のズレに起因する開発の手戻りを未然に防げるようになりました。

「捨てやすさ」が仮説検証を加速する

プロトタイプの目的は、あくまでアイデアの仮説検証です。

仮説検証の結果、もしユーザーにとってそのアイデアに価値がないと分かれば、すぐに捨てて次のアイデアを考えるのが正しいアプローチです。

しかし、エンジニアが時間をかけて作ったプロトタイプは、「せっかく作ってもらったのだから…」というサンクコストバイアスに陥りやすく、客観的な判断を鈍らせる原因になり得ます。

その点、v0で作成したプロトタイプはひとりで短時間で作成したものです。検証結果が思わしくないときは、何の心理的な負い目も感じることなく、躊躇なく捨てることができます。

このことが、プロトタイプ開発における仮説検証のサイクル全体を大きく加速させてくれました。

今後の展望

現在はshadcn/uiのレジストリからコンポーネントを配布し、v0のプロジェクトに適用する検証を進めています。
本番環境と同等のコンポーネントを使うことで、質の高いプロトタイプをより短時間で作成できるようになることを目指しています。

ui.shadcn.com

今回はAIツール導入の一例として、v0の導入効果についてご紹介しました。参考になれば幸いです。

似顔絵
書いた人:田向

SaaS型翻訳サービスWeglotでReact SPAを多言語化した

はじめに

こんにちは。エンジニアリングマネージャの星野です。今回はReactを用いたSPAの他言語化についての事例を紹介します。

テックドクターで開発している臨床研究支援システム「SelfBase」において、海外案件への対応をきっかけに管理画面の多言語化が必要となりました。

フロントエンドはReactを用いたSPA(Single Page Application)で構築されており、自前で実装する場合は react-i18next などを利用したi18n(国際化)対応を行うことになります。そのためには数ヶ月単位の少なくない開発工数が見込まれました。

そこで、「自前での実装」という選択肢だけでなく、「SaaS型の翻訳サービス」の導入を本格的に検討することにしました。


フロントエンド技術スタックと検討ポイント

多言語化を検討するにあたり、技術スタックとSaaS選定のポイントについて説明します。

フロントエンド技術スタック

フロントエンドの主要な技術スタックは以下の通りです。

  • UI: React
  • 言語: TypeScript
  • ルーティング: React Router
  • スタイリング: MUI + Emotion

SaaS選定における検討のポイント

SaaSを選定するにあたっては、開発工数をかけずに多言語化を実現するため、いくつかの点を重視しました。

  1. SPAに対応していること。
  2. 翻訳管理が容易であること。たとえば、翻訳された文言の一元管理機能ができる、ユーザーデータのような翻訳不要な要素を柔軟に除外できる、など。
  3. 予算内で導入できるコストであるか。
  4. SOC2やGDPRなどのセキュリティ基準を満たしているか。
  5. 国内外での十分な導入実績があるか。

SaaSによる多言語化の比較検討

上記のポイントに基づき、いくつかのSaaSを比較検討しました。

サービス名 SPA対応 価格帯 特徴
shutto翻訳 実績不明 安価 国内サービス。SPAでの利用実績が確認できず、今回の要件には合致しませんでした。
WOVN.io 対応可能 高額(要問合せ) 日本国内での大手企業による採用実績が豊富。ただし、コストが比較的高額でした。アプリの翻訳機能もあり、予算があるなら利用したかった。
ConveyThis 対応可能 安価 比較的安価で導入しやすそう。ただ、後述するWeglotと比較すると実績やシェアの面で見劣りしていました。
Weglot 対応可能 手頃 SPA対応が可能。料金も手頃で、セキュリティ基準も満たしていました。調べた範囲ではPV数ベースの集計において高いシェアを持っているようでした。

検討結果

比較検討の結果、Weglotを導入することにしました。理由は下記です。

  • ReactなどのSPAプロジェクトへの導入がヘルプに記載されており、実績があると判断できました。
  • 実際にテストで動作させてみても、問題なく対応していることが確認できました。
  • 翻訳結果を管理するダッシュボードがあり、非エンジニアでも直感的に翻訳の修正や管理が可能でした。
  • AIを利用した翻訳が利用でき、翻訳の修正工数を減らせると期待しました。
  • SOC2およびGDPRに準拠しており、エンタープライズレベルのセキュリティ要件を満たしています。
  • Google検索やWappalyzerなどの情報から、ある程度シェアがあると判断。国外では大手での導入実績もあるようでした。
  • 必要な機能を備えつつ、料金が手頃で、予算感に最もマッチしていました。

実装と運用の工夫

Weglotの導入はスムーズでしたが、実際のプロダクトで運用するためにはいくつかの工夫が必要でした。

導入方法

  • Weglotでのプロジェクト作成

Weglotでアカウントを作成し、翻訳元(日本語)と翻訳先の言語(英語など)を設定します。

  • インテグレーションの方式の選択

サブドメイン方式とサブディレクトリ方式があります
SEOなどの観点からサブディレクトリ方式が推奨されていますが、今回はサブドメイン方式を選択しました。
理由としては、既存サイト全体がWeglot経由で配信されようになるため、影響範囲を抑える目的でサブドメイン方式を選択しました。

  • DNSレコードの設定

WeglotからCNAMEに登録すべきURLが提供されるので、利用しているDNSプロバイダー(Amazon Route 53、Google Cloud DNSなど)から、指定したサブドメインをCNAMEレコードとして、追加します。
この設定により、例えば、en.example.comへのアクセスがWeglotのサーバーに向けられ、翻訳されたコンテンツが配信されるようになります。

  • JavaScriptスニペットの埋め込み

発行されたAPIキーを含むJavaScriptスニペットをWebサイトに埋め込みます。
これは主に、翻訳単語の抽出や言語切り替えボタン(言語スイッチャー)の表示、言語によるサイトの自動切り替えなどに利用します。

  • SPA特有の設定

デフォルトでは動的に描画されるコンテンツは翻訳されません。
翻訳対象に含めるため、Weglotのプロジェクト設定にある Dynamic Element にて、 body セレクタを指定します。

キャプチャ
動的要素の翻訳設定画面

実装上の注意点

Weglotは手軽に導入できる反面、自由度は低いと感じました。
そのため運用にあたっては、以下のような工夫が必要でした。

翻訳対象の除外設定:

Weglotはデフォルトでページ上の全テキストを翻訳しようとします。翻訳が不要な箇所については、翻訳対象から除外する設定を行う必要がありました。

  • ユーザー名などのユーザが入力した内容

本来翻訳の対象としたくない部分ですし、翻訳の管理上もユーザが新しい単語を入力するために訳語の設定が必要となってしまうのは好ましくありません。
また、翻訳語数ベースの課金モデルにおいてコストを抑える意図もあります。

対策としては、翻訳したくない要素をラップするコンポーネントをReactで作成、特定のCSSセレクタを持つ要素を翻訳対象から除外する機能と組み合わせ、自動翻訳を抑止しました。

export const NoTranslate = forwardRef<HTMLElement, BoxProps>(
  ({ component = 'span', ...boxProps }, ref) => (
    <Box
      ref={ref}
      className="no-translate"
      translate="no"
      component={component}
      {...boxProps}
    >
      {boxProps.children}
    </Box>
  ),
)

NoTranslate.displayName = 'NoTranslate'

NoTranslateコンポーネント実装イメージ
 

キャプチャ
Weglotの自動翻訳抑止機能
  • 日付や数値のローカライズ

日付や数値は翻訳ではなく、各言語の文化に合わせたフォーマットが必要です。これらは翻訳対象から除外し、Intl.DateTimeFormatIntl.NumberFormatといったブラウザ標準APIで対応しました。

動的テキストの制約(語順と単数/複数形):

例えば {count}日間以上デバイスのデータがアップロードされてない場合 のような動的に数値を埋め込むテキストを翻訳する場合、{count} 部分は翻訳から除外するため、数字を除いた「日間以上デバイスのデータがアップロードされてない場合」というテキストをもとに翻訳が行われます。

これにより、2つの大きな問題が生じます。

1つ目は語順の問題です。
翻訳後のテキストは、翻訳されなかった {count} の後ろに単純に連結されます。しかし、多くの言語では日本語と語順が異なります。例えば、英語で「For more than {count} days...」のような {count} の前に前置詞が来るケースがありますし、他にも文法上、数字が文中に入ってしまう場合もあります。こういった文章は自然に作ることができません。

2つ目は単数/複数形の切り替えです。`{count}`の値に応じて `1 day` / `2 days` のように単語の形を変化させることができません。

今回はこれらの制約を許容し、妥協案として {count} day(s) without device data uploaded のような、英語表記で対応しました。

1つ目の問題は翻訳元の文に変数プレースホルダーを挿入する「variables」機能を利用することで解決はできそうでしたが、今回はそこまで対応することができませんでした。

文脈に応じた翻訳の難しさ

Weglotでは、基本的に一つの単語は一つの訳語に対応します。例えば「日」という単語を day(s) と翻訳した場合、別のページで「日曜日」の文脈で使われていても day(s) と訳されてしまう可能性があります。これを防ぐには、元の日本語の単語を文脈に応じてより具体的に(例:「日付」「日曜日」など)使い分けるといった、原文側での工夫が必要になります。

Weglotよりもさらに厳密な表現や文脈に合わせた翻訳が求められる場合は、やはり react-i18next のようなi18nライブラリによる本格的な対応が必要と感じました。

キャプチャ
該当部分の翻訳前
キャプチャ
該当部分の翻訳後

翻訳の質と翻訳管理について

自動翻訳の品質は、まずまずといった程度でしたが、AI機能を使うとかなり不自然な内容は減りました。

長文では、手動での言い換えが必要になる場合も多少ありましたが、短いフレーズや単語レベルの翻訳は正確でした。全体としては十分に実用的な品質といえます。

また、専門用語や製品名などについては、翻訳するかどうかも含めて辞書登録できる点が便利でした。

キャプチャ
会社名や、製品名を登録した例

さらに、翻訳結果にレビュー済みかどうかが分かる仕組みが用意されており、今後画面修正などが発生した際にも、追加翻訳をスムーズに行えるようになっています。

キャプチャ
フィルタ機能で、自動翻訳対象のテキストを検索している様子。(すべて翻訳が登録されているので結果は0件)

i18nライブラリのように翻訳者が翻訳ファイルを直接編集するわけではないため、翻訳者と開発者の役割分担が明確になり、運用がスムーズになる点もメリットと感じています。

キャプチャ
コラボレーションするための画面。日本語と英語訳が表示される

導入後の感想

Weglotの導入を決定したことで、当初3ヶ月以上かかると想定していた多言語化対応が、1ヶ月程度で完了する見込みとなりました!
これにより、SaaS導入は期待通りにエンジニアの工数を大幅に削減でき、非常に助かりました。

課題点としては、初回アクセス時は一瞬だけ日本語が表示されてから英語に切り替わるという動作になっています。
この点は、当初から理解していて実用上は問題ないと判断していましたが、不自然さは感じてしまいます。

今後について

今回は、まずはいったん多言語サイトをサービス提供することが重要だと考えて、SaaSという選択肢を選びました。
ですが、一瞬日本語で表示されてしまう点や翻訳の制限などから、どこかでi18nライブラリを用いた本格的な多言語化基盤の構築も必要だと感じています。

その際には、今回見送った react-i18next によるi18n基盤の構築を再度検討し、翻訳ファイルの管理に特化したSaaS(Lokalise, Transifexなど)へ移行することも検討したいと考えています。

まずは、Weglotで翻訳の知見を溜めつつ、翻訳精度の向上や用語の統一を行うことでグローバルに通用するサービスの第一歩目としていきます。


似顔絵
書いた人:星野