【入門編】Dart 3.0の「base」「interface」「final」修飾子によるクラス階層の制御 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!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ライフを送りましょう!

タイトルとURLをコピーしました