こんにちは、Sally 株式会社 CTO の @aitaro です。普段はマーダーミステリーアプリ「ウズ」とマダミス情報サイト「マダミス.jp」を開発しています。
今回は、Flutter アプリケーションにおいて値オブジェクトの実装に Dart 3.3 から導入された extension types を(今更ながら)採用したところ、パフォーマンスが改善したという話をご紹介します。特に、ゲーム状態構築処理において 31%の高速化を実現できました。
値オブジェクト(Value Object)は、ドメイン駆動設計(DDD)における重要な概念の一つです。単純なプリミティブ型(String、int など)をラップして、ビジネスロジックに意味を持たせる設計パターンです。
例えば、ユーザー ID を表現する場合、単なる String
ではなく UserId
という専用の型を定義することで、型安全性が向上します。異なる種類の ID を間違えて渡すことをコンパイル時に防げるようになり、バグの早期発見につながります。また、バリデーションや変換処理を値オブジェクト内に集約できるため、ビジネスロジックが整理され、コードの意図も明確になります。
void updateUser(String userId, String userName) {
}
void updateUser(UserId userId, UserName userName) {
}
弊社のマーダーミステリーアプリ「ウズ」では、ゲーム内の様々な ID や値を型安全に扱うため、値オブジェクトパターンを採用していました。これまでの実装は以下のような抽象クラスベースのアプローチでした。
抽象クラスベースの実装
import 'package:meta/meta.dart';
abstract class IDT extends IDT, V>, V> {
const ID(this.value);
final V value;
bool operator ==(Object other) => other is T && value == other.value;
int get hashCode => value.hashCode;
String toString() {
return value.toString();
}
}
class RoomID extends IDRoomID, String> {
const RoomID(super.value);
factory RoomID.fromJson(dynamic json) => RoomID(json as String);
String toJson() => toString();
}
class PhaseID extends IDPhaseID, String> {
const PhaseID(super.value);
factory PhaseID.fromJson(dynamic json) => PhaseID(json as String);
String toJson() => toString();
}
class CharacterID extends IDCharacterID, String> {
const CharacterID(super.value);
factory CharacterID.fromJson(dynamic json) => CharacterID(json as String);
String toJson() => toString();
}
この方法で型安全に ID を実装することができた一方、実行時のパフォーマンスオーバーヘッドの大きさが課題となっていました。
パフォーマンス問題の認識
弊社が開発する「ウズ」は、オンラインでマーダーミステリーを楽しめるアプリです。マーダーミステリーでは、プレイヤーがそれぞれ異なるキャラクターを演じ、様々な情報を集めながら事件の真相を推理します。ゲームシステムでは「特定のキャラクターが特定のアイテムを持っている時」「あるフェーズで特定の選択肢を選んだ時」「複数の条件が揃った時」など、無数の条件分岐とトリガーが設定されています。
特定のシナリオでパフォーマンスが悪化するという問題がありました。プロファイリングツールで調査したところ、CharacterID、ItemID、PhaseID、ActionID など、様々な ID の比較処理が全体の実行時間の大きな割合を占めていることが判明しました。
1 回のゲームセッションで数万回から数十万回の ID 比較が発生しており、抽象クラスベースの実装では各 ID 比較でequals
メソッドの呼び出しとオブジェクトの型チェックが発生していました。これが積み重なることで、無視できないパフォーマンス影響が生じていたのです。
この問題をどう解決すべきか悩んでいたところ、Claude Code との技術的な壁打ちの中で「extension types」という機能の存在を知りました。Dart 3.3 で導入されたこの機能は、私たちが直面していた「型安全性とパフォーマンスの両立」という課題を解決するために設計されたものだったのです。
Dart 3.3 で導入された extension types は、上記の問題を解決する言語機能です。コンパイル時の型安全性を提供しながら、実行時のオーバーヘッドがゼロという特徴があります。
extension types とは
extension types は、既存の型を「ラップ」して新しい型を作成する機能です。重要なのは、これがコンパイル時のみの抽象化であり、実行時には元の型と同じように扱われる点です。
lib/domain/value_objects/user_id.dart
extension type const UserId(String value) implements String {
factory UserId.fromString(String value) {
if (value.isEmpty) {
throw ArgumentError('UserIdは空にできません');
}
return UserId(value);
}
bool get isValid => value.isNotEmpty && value.length 50;
}
実際の導入例
ゲーム中に使われる 20 個以上の ID クラスを一括で extension types に移行しました。
Before: 抽象クラスを継承した実装
abstract class IDT extends IDT, V>, V> {
const ID(this.value);
final V value;
bool operator ==(Object other) => other is T && value == other.value;
int get hashCode => value.hashCode;
String toString() {
return value.toString();
}
}
class CharacterID extends IDCharacterID, String> {
const CharacterID(super.value);
factory CharacterID.fromJson(dynamic json) => CharacterID(json as String);
String toJson() => toString();
}
class ClueID extends IDClueID, String> {
const ClueID(super.value);
factory ClueID.fromJson(dynamic json) => ClueID(json as String);
String toJson() => toString();
}
After: extension types の実装
extension type const CharacterID(String value) {
factory CharacterID.fromJson(dynamic json) => CharacterID(json as String);
String toJson() => value;
}
extension type const ClueID(String value) {
factory ClueID.fromJson(dynamic json) => ClueID(json as String);
String toJson() => value;
}
extension types への移行により、以下のようなパフォーマンス改善を実現しました。
パフォーマンス測定結果
弊社では、ウズ上の各シナリオの状態をキャッシュなしで再現するのにかかる時間をベンチマークしています。
全てのシナリオでパフォーマンスが向上し、平均 31%の改善を達成しました。
(シナリオ名はボカシをかけています。)
なぜこれほど改善したのか
extension types によるパフォーマンス改善は、主に以下の 3 つの要因によるものだと考えています。
1. オブジェクト生成コストの削減
クラスベースの実装では、各値オブジェクトがヒープ上に独立したオブジェクトとして生成されていました。例えばUserId('user123')
を作成すると、内部の String 値に加えて UserId オブジェクト自体もヒープ上に確保されるため、実質的に 2 つのオブジェクトが生成されていたのです。
final userId = UserId('user123');
final userId = UserId('user123');
extension types は実行時には元の型(String)として扱われるため、追加のオブジェクト生成が発生しません。マーダーミステリーのように多くの ID を扱うアプリケーションでは、この差がパフォーマンス改善につながりました。
2. メモリアクセスの効率化
extension types は元の型と同じメモリレイアウトを持つため、メモリアクセスが効率的になりました。クラスベースの実装では、値を取得するために「オブジェクト → value フィールド」という間接参照が必要でしたが、extension types では直接値にアクセスできます。
この結果、CPU キャッシュの効率が向上し、特に多くの ID を連続して処理する場面でキャッシュミスが減少しました。
3. ID 比較の高速化
マーダーミステリーでは膨大な数の ID 比較が発生しますが、これが最も重要な改善点でした。
クラスベースの実装では、equals
メソッドの呼び出し時に以下のオーバーヘッドが発生していました:
bool operator ==(Object other) =>
other is CharacterID &&
value == other.value;
各ステップで仮想関数テーブルの参照も必要になるため、単純な文字列比較に比べて大きなオーバーヘッドが発生していました。
一方、extension types では実行時に String の直接比較となるため、これらのオーバーヘッドが排除されました。結果として、ネイティブな String 比較と同等の速度で実行されるようになり、ゲーム中の条件判定処理が高速化されたのです。
Flutter/Dart アプリケーションにおいて、値オブジェクトの実装に extension types を採用することで、実行時間を 31%短縮することができました。このパフォーマンス改善を実現しながら、コンパイル時の型チェックはそのまま維持できているため、型安全性を犠牲にすることなくパフォーマンスを向上させることができました。
Views: 0