Dart 3.0では、ライブラリの作者がクラスの継承・実装・インスタンス化の権限をきめ細かく制御できるように、クラス修飾子(Class Modifiers) が導入されました。
特に sealed クラス は、RustのEnumやKotlinのSealed Class、TypeScriptのTagged Union(判別可能なUnion型)に相当する 代数的データ型(ADT) をDartで美しく実現し、switch 式と組み合わせることで網羅性チェックの恩恵を最大限に引き出します。
この章では、sealed, base, interface, final 修飾子の役割と、堅牢なドメインモデル設計を学びます。
sealed クラスによる代数的データ型(ADT)とSwitchの組み合わせsealed クラス は、そのクラスを直接インスタンス化できず(暗黙的に abstract)、サブクラスの定義を同一ライブラリ(同一ファイル)内のみに限定する 修飾子です。
コンパイラは「そのクラスのサブクラスの全パターン」を完全に把握できるため、switch 式で網羅性チェックが働きます。
// 1. sealed クラスでUI状態の基底クラスを定義
sealed class UiState {}
class InitialState extends UiState {}
class LoadingState extends UiState {}
class SuccessState extends UiState {
final List<String> data;
SuccessState(this.data);
}
class ErrorState extends UiState {
final String message;
ErrorState(this.message);
}
// 2. switch式で状態に応じたレンダリング文字列を生成
String render(UiState state) {
// 全サブクラスが網羅されているため、default (_) 節が不要!
return switch (state) {
InitialState() => '待機中...',
LoadingState() => '読み込み中...',
SuccessState(:var data) => 'データ取得成功: ${data.join(', ')}',
ErrorState(:var message) => 'エラー発生: $message',
};
}
void main() {
UiState state = LoadingState();
print(render(state));
state = SuccessState(['Dart', 'Flutter']);
print(render(state));
state = ErrorState('ネットワーク接続に失敗しました');
print(render(state));
}dart run sealed_demo.dart読み込み中... データ取得成功: Dart, Flutter エラー発生: ネットワーク接続に失敗しました
base、interface、final 修飾子の使い分けDart 3では、ライブラリ境界外(外部パッケージや別ファイル)からのクラス利用方法を制限するために、各種クラス修飾子が提供されています。
| 修飾子 | 外部でのインスタンス化 | 外部での extends (継承) | 外部での implements (実装) | 外部での with (Mixin) |
|---|---|---|---|---|
class (無印) | ○ | ○ | ○ | × |
base | ○ | ○ (base 必須) | × | × |
interface | ○ | × | ○ | × |
final | ○ | × | × | × |
sealed | × (abstract) | × (同ファイル内のみ) | × (同ファイル内のみ) | × |
base 修飾子(継承のみを許可し implements を禁止)クラスに新しいメソッドを追加しても外部のサブクラスが壊れないように設計したい場合に利用します。外部ライブラリからの暗黙的インターフェース実装(implements)を禁止します。
// 外部ライブラリ側
base class Vehicle {
void move() => print('移動中');
}
// 利用側
// class Car implements Vehicle {} // エラー: implements 不可
base class Car extends Vehicle {} // OK: extends のみ許可interface 修飾子(実装のみを許可し継承を禁止)APIの型シグネチャのみを提供し、内部実装の継承による暗黙の依存を防ぎたい場合に利用します。外部ライブラリからの継承(extends)を禁止し、実装(implements)のみを許可します。
// 外部ライブラリ側
interface class StorageService {
void save(String key, String value) {}
}
// 利用側
// class LocalStorage extends StorageService {} // エラー: extends 不可
class LocalStorage implements StorageService { // OK: implements のみ許可
@override
void save(String key, String value) => print('Saved $key');
}final 修飾子(外部からの継承・実装を完全に禁止)クラスの動作を完全に固定し、外部ライブラリからのサブクラス化(継承 extends および実装 implements の両方)を一切許さない場合に使用します。
// 外部ライブラリ側
final class ImmutableConfig {
final String env;
const ImmutableConfig(this.env);
}
// 利用側
// class MyConfig extends ImmutableConfig {} // エラー: extends 不可
// class MyConfig implements ImmutableConfig {} // エラー: implements 不可クラス修飾子(特に sealed)とDart 3のパターンマッチングを組み合わせることで、「不正な状態を型レベルで表現不可能にする」 堅牢なドメインモデルが構築できます。
sealed class AuthState {
const AuthState();
}
class AuthUnauthenticated extends AuthState {
const AuthUnauthenticated();
}
class AuthAuthenticating extends AuthState {
const AuthAuthenticating();
}
class AuthAuthenticated extends AuthState {
final String userId;
final String token;
const AuthAuthenticated({required this.userId, required this.token});
}
class AuthError extends AuthState {
final String errorMessage;
const AuthError(this.errorMessage);
}
void printAuthAction(AuthState state) {
final action = switch (state) {
AuthUnauthenticated() => 'ログインボタンを表示します',
AuthAuthenticating() => 'スピナーを表示して待機します',
AuthAuthenticated(:var userId) => 'ユーザー $userId のマイページを表示します',
AuthError(:var errorMessage) => 'エラーダイアログを表示: $errorMessage',
};
print(action);
}
void main() {
AuthState state = const AuthAuthenticating();
printAuthAction(state);
state = const AuthAuthenticated(userId: 'u_777', token: 'jwt_abc123');
printAuthAction(state);
}dart run auth_state_modeling.dartスピナーを表示して待機します ユーザー u_777 のマイページを表示します
この章では、Dart 3のクラス修飾子によるモデリングの厳密化について学びました。
sealed クラス: 同一ファイル内のみでサブクラス定義を制限し、代数的データ型(ADT)と switch 式の完全な網羅性チェックを実現。base 修飾子: 継承のみを許可し、暗黙的インターフェースとしての implements を防ぐ。interface 修飾子: インターフェース実装(implements)のみを許可し、実装の継承を防ぐ。final 修飾子: 外部からの継承と実装を両方禁止する。次の 第9章 では、Dartのイベント駆動アーキテクチャの中核である 非同期処理の基礎(Futureとasync/await) を学びます。
天気予報アプリの画面状態を表す WeatherState を sealed クラスで実装してください。
sealed class WeatherState を定義する。class WeatherInitial extends WeatherStateclass WeatherLoading extends WeatherStateclass WeatherSuccess extends WeatherState: フィールド final String city, final double temperature を持つ。class WeatherFailure extends WeatherState: フィールド final String error を持つ。String getWeatherMessage(WeatherState state) 関数を switch 式で実装し、全状態に応じた適切なメッセージ文字列を返す。main() で各状態を作成し、メッセージを出力する。// ここにクラスと関数を定義してください
void main() {
// ここで動作確認を行ってください
}dart run practice8_1.dartキャッシュストレージのインターフェースと、基底となるローカルストレージ実装を設計してください。
abstract interface class CacheRepository<T> を定義し、メソッド T? get(String key); と void set(String key, T value); を宣言する。base class MemoryCacheRepository<T> implements CacheRepository<T> を実装し、内部の Map<String, T> にデータを保存・取得する処理を実装する。final class ExpiringMemoryCache<T> extends MemoryCacheRepository<T> を定義する。main() で ExpiringMemoryCache<String> を生成し、データの追加と取得をテストする。// ここにクラスを定義してください
void main() {
// ここで動作確認を行ってください
}dart run practice8_2.dart