こんにちは!FlutterやDartを使った開発を楽しんでいますか?
他のプログラミング言語(JavaやC#、TypeScriptなど)からDartの世界にやってくると、「クラスの設計をもっと厳密にコントロールしたいな」と感じる瞬間があるのではないでしょうか。「このクラスは継承してほしくない」「このクラスは特定のパッケージ内だけで実装を強制したい」といった設計者の意図を、コード上でガッチリと表現できたら安心ですよね。
Dart 3.0で導入された `base`、`interface`、`final` というクラス修飾子(Modifiers)は、まさにその願いを叶えるための強力な武器です。
ここをクリアすれば、あなたもDartの型システムを手の内に入れ、大規模開発でも破綻しない堅牢なAPI設計ができるようになりますよ。さあ、一緒にマスターしていきましょう!
—
なぜクラスの継承・実装をコントロールする必要があるのか?
これまでのDart(Dart 2.x時代)では、すべてのクラスがデフォルトで「継承(extends)」も「実装(implements)」もできてしまいました。
しかし、これはライブラリの作者にとっては悩みの種でした。
「内部のロジックが依存しているから、勝手にサブクラスを作られては困る!」あるいは「このクラスはインターフェースとしてだけ使ってほしいのに、中身まで継承されてしまった……」といった、設計者の意図しない使われ方(脆弱な基底クラスの問題)を防げなかったからです。
Dart 3.0では、クラスの宣言時に修飾子をつけることで、「他のクラスからどう扱われるべきか」をコンパイラに厳格に教え込めるようになりました。
—
4つの修飾子(`base`, `interface`, `final`, `abstract`)の全体像
まずは、クラスの振る舞いを決めるキーワードたちの関係性を整理してみましょう。頭の中で次のようなイメージを持ってみてください。
[ 継承 (extends) できるか? ]
├── できる: base, final (同一ライブラリ内), プレーンなクラス
└── できない: interface, ライブラリ外の final
[ 実装 (implements) できるか? ]
├── できる: base (同一ライブラリ内), interface, プレーンなクラス
└── できない: final
これだけだと少し抽象的なので、それぞれの修飾子が持つ「性格」を一つずつ優しく紐解いていきますね。
—
1. `base` クラス:実装は許すが、メソッドの破壊を防ぐ
`base` 修飾子は、「継承(extends)は許可するが、インターフェースとしての実装(implements)は禁止する」という性質を持ちます。
なぜこんな制限が必要なのでしょうか?
それは、スーパースタークラスが持つ内部のプライベートな状態や、メソッド間の複雑な連携(内部構造)を守るためです。`implements` を使ってしまうと、クラスの中身を完全に無視して「ガワ(見た目の型)だけ」を真似て作ることになるため、スーパースター側の想定外のバグを生みやすくなります。
// lib/engine.dart
base class Vehicle {
// エンジン始動の基本ロジック
void startEngine() {
print(‘Vroom! エンジンが始動しました。’);
}
}
// 【OK】baseクラスは extends による継承ができる
base class Car extends Vehicle {
@override
void startEngine() {
super.startEngine();
print(‘四輪駆動で走り出します!’);
}
}
// 【NGコンパイルエラー】implements は許可されない!
// class FakeVehicle implements Vehicle {
// @override
// void startEngine() {}
// }
> 先輩からのワンポイントアドバイス
> `base` クラスを継承する子クラスも、必ず `base` か `final` をつけなければならないというルール(伝播性)があります。これにより、階層のどこかで意図せぬ `implements` が混ざるのを完全に防いでくれます。
—
2. `interface` クラス:ガワ(型)だけを強制する
`interface` 修飾子は、その名の通り「インターフェース」として振る舞います。
つまり、「実装(implements)は自由に許可するが、継承(extends)によるコードの流用は一切禁止する」という性質を持ちます。
スーパースタークラスの持つ具象メソッド(中身のある処理)を勝手に受け継がせたくない、共通の「型」だけを他のクラスに強制したいときに使います。
// lib/logger.dart
interface class Logger {
// 中身のあるメソッドを持つこともできるが…
void log(String message) {
print(‘LOG: $message’);
}
}
// 【OK】implements して「型」として利用する
class ConsoleLogger implements Logger {
@override
void log(String message) {
print(‘[Console] $message’);
}
}
// 【NGコンパイルエラー】extends によるコードの継承はできない!
// class AdvancedLogger extends Logger {
// }
「中身のコードは引き継がせたくないけれど、同じメソッドを持っていることを保証(強制)させたい!」という場面で絶大な効果を発揮します。
—
3. `final` クラス:継承も実装も一切させない(終端クラス)
`final` 修飾子は、文字通り「これでおしまい!」を表します。
同一ライブラリ(同じファイル、または `part` ファイル)の外では、継承(extends)も実装(implements)も完全に禁止します。
APIの作者が「このクラスは私たちが完璧に作った完成品なので、勝手に改造しないでください」と宣言するためのものです。
// lib/config.dart
final class AppConfig {
final String appName = ‘SuperApp’;
int get timeoutSeconds => 30;
}
// 【NGコンパイルエラー】外部ライブラリ(または別ファイル)からは一切継承できない!
// class MyConfig extends AppConfig {}
// 【NGコンパイルエラー】implements もできない!
// class FakeConfig implements AppConfig {}
セキュリティや、将来のバージョンアップ時の安全性(バイナリ互換性など)を担保したいコアなクラスには、基本的に `final` を付与するのがモダンDartのベストプラクティスになりつつあります。
—
比較表でスッキリ整理!
ここまでの内容を、開発現場でパッと見返せるように表にまとめておきますね。
| 修飾子 | 外部からの `extends` (継承) | 外部からの `implements` (実装) | 主なユースケース |
| :— | :—: | :—: | :— |
| プレーン (修飾子なし) | ⭕️ 可能 | ⭕️ 可能 | 従来のクラス(デフォルト) |
| `base` | ⭕️ 可能 (子もbase等が必要) | ❌ 不可能 | 内部構造を守りつつ機能拡張させたい時 |
| `interface` | ❌ 不可能 | ⭕️ 可能 | 型のルール(振る舞い)だけを強制したい時 |
| `final` | ❌ 不可能 | ❌ 不可能 | 完全に変更をせき止めたい完成されたクラス |
—
陥りやすい文法エラーと注意点
初心者の開発者の方からよくいただく質問が、「コンパイルエラーが消えない!」というものです。代表的な2つの罠を頭に入れておきましょう。
トラップ1: サブクラスの修飾子忘れ
スーパースタークラスに `base` を使った場合、それを継承する子クラスにも `base`(または `final`)をつけ忘れると、コンパイラに怒られてしまいます。
base class A {}
// エラー: baseクラスを継承するクラスも、base, final, またはsealedである必要があります
class B extends A {}
解決策: `base class B extends A {}` と書き換えましょう。
トラップ2: ライブラリの境界線
`final` や `base` の制限は、「同じライブラリ(同じファイル内)」であれば一時的に緩まります。同じファイル内であれば、`final` クラスを継承することが言語仕様上は可能です(プライベートなテストや、同一ファイル内でのモジュール分割のため)。
しかし、別のファイルに切り分けた瞬間に厳格なチェックが走るため、「昨日まで動いていたのに、ファイルを分けたらコンパイルエラーになった!」というときは、このライブラリの境界を意識してみてください。
—
まとめ
いかがでしたか?
Dart 3.0の `base`、`interface`、`final` 修飾子を使えば、「このクラスはこういう意図で使ってほしいんだ」という設計者のメッセージを、コードの構造レベルで強制できるようになります。
- 中身を守りつつ拡張させたい = `base`
- 型(ルール)だけを強制したい = `interface`
- 変更を一切許さず守り抜きたい = `final`
これらを適切に使い分けることで、あなたの書くコードは一気にプロフェッショナルな品質へと洗練されます。ぜひ、次のFlutterアプリやDartプロジェクトの設計で試してみてくださいね。
ここをクリアできれば、Dartのオブジェクト指向はもうバッチリマスターです!快適なDartライフを送りましょう!