こんにちは!FlutterやDartを使った開発を楽しんでいますか?
今回は、Dartのオブジェクト指向を語る上で絶対に避けて通れない、そして中級者へのステップアップとして最高の壁となる「クラス修飾子(`base`・`interface`・`final`)」についてお話ししていきますね。
他の言語(JavaやC#など)からDartに入ってきた開発者の中には、「あれ、クラスの継承や実装をライブラリの境界で完全にコントロールしたいんだけど、どうやるんだっけ?」と手が止まった方も多いのではないでしょうか。
ここをクリアすれば、Dartの型システムとAPI設計の裏側が手に取るように分かり、大規模開発でも破綻しない堅牢なコードが書けるようになりますよ。さあ、一緒に深掘りしていきましょう!
—
1. なぜ「継承・実装の制限」が必要なのか?(設計の背景)
Dart 2.xまでの世界では、すべてのクラスがデフォルトで「どこからでも継承でき、どこからでも実装(implements)できる」オープンな状態でした。
しかし、これには大きな問題がありました。ライブラリの作者が「内部のロジックを変更したい」と思っても、ユーザー(利用者)が勝手にそのクラスを継承していたり、インターフェースとして実装してしまっているため、ちょっとしたメソッドの追加や内部構造の変更が、利用者側のコードを破壊する(いわゆる「Fragile Base Class(脆弱な基底クラス)問題」)原因になっていたのです。
Dart 3以降、私たちはクラスに「どう使ってほしいか(あるいは使われたくないか)」という意図を、コンパイラに対して明確に指示できるようになりました。それが今回紹介するクラス修飾子です。
—
2. 3つの修飾子(base, interface, final)の基本と使い分け
まずは、それぞれの修飾子が「何を許可し、何を禁止するのか」を整理しましょう。
イメージしやすいように、まずは全体像を把握するための早見表を見てください。
| 修飾子 | 同一ライブラリ内での継承 | 外部ライブラリでの継承 | 同一ライブラリ内での実装 | 外部ライブラリでの実装 |
| :— | :—: | :—: | :—: | :—: |
| `base` | ◯ 許可 | ◯ 許可 | ❌ 禁止 | ❌ 禁止 |
| `interface` | ❌ 禁止 | ❌ 禁止 | ◯ 許可 | ◯ 許可 |
| `final` | ❌ 禁止 | ❌ 禁止 | ❌ 禁止 | ❌ 禁止 |
※ ここでいう「ライブラリ」とは、基本的に1つのDartファイル(または`part`で結合されたファイル群)を指します。
それぞれの性質を、優しく紐解いていきましょう。
—
① `base` クラス:メソッドの「実装の再利用」を強制する
`base`修飾子は、「継承は許可するが、インターフェースとしての実装(implements)は絶対にさせない」という制約をかけます。
なぜこれが必要かというと、基底クラス側で定義されている内部状態やプライベート変数、メソッドの実装ロジックを、サブクラスに確実に受け継がせたいからです。`implements`されてしまうと、内部の実装をごっそり無視してメソッドのシグネチャ(形)だけを強制実装されてしまい、基底クラス側の前提が崩れてしまいます。
// baseクラスの定義(同じライブラリ内、または別ライブラリ)
base class Vehicle {
void move() {
print(‘移動します’);
}
}
// 【OK】継承(extends)は問題なし!
base class Car extends Vehicle {
@override
void move() {
print(‘車で走ります’);
}
}
// 【NGコンパイルエラー】implementsすることはできない!
// class ElectricCar implements Vehicle {
// @override
// void move() {}
// }
> 💡 コンパイラの裏側:
> `base`がついたクラスを継承したクラスも、必ず `base` か `final` を付ける必要があります。これにより、継承のチェイン全体で「実装が勝手にインターフェースにすり替えられること」を防ぎ、Dart VMが最適化(Devirtualizationなど)を行いやすい環境を作っています。
—
② `interface` クラス:振る舞いの「型(契約)」だけを強制する
`interface`修飾子は、「実装(implements)は許可するが、継承(extends)は絶対にさせない」という、`base`とは真逆の制約をかけます。
基底クラス側の具象メソッド(中身のあるメソッド)のロジックを勝手に引き継がせたくなく、「こういうメソッドを持っているというルール(インターフェース)だけを強制したい」場合に用います。
// interfaceクラスの定義
interface class Logger {
void log(String message) {
print(‘LOG: $message’);
}
}
// 【OK】インターフェースとしての実装(implements)はOK!
class ConsoleLogger implements Logger {
@override
void log(String message) {
// 基底クラスの実装を引き継がないため、完全に自分で書き直す必要がある
print(‘[Console] $message’);
}
}
// 【NGコンパイルエラー】継承(extends)することはできない!
// class AdvancedLogger extends Logger {
// }
APIの設計者が「うちの内部ロジックは一切引き継がせない。ただし、この画面や機能を作るなら、最低限このメソッドの形は守ってね」という契約を結びたい時に非常に強力な武器になります。
—
③ `final` クラス:継承も実装も一切許さない「要塞」
`final`修飾子は、「継承も実装も完全に禁止する」という最も強力な制限です。
「このクラスの挙動は完成しており、これ以上派生クラスを作られると困る」という場合に使います。Javaの `final` や、他の言語の構造に近いイメージですね。
// finalクラスの定義
final class SecureToken {
final String value;
SecureToken(this.value);
}
// 【NGコンパイルエラー】継承(extends)は不可!
// class JwtToken extends SecureToken {
// JwtToken(super.value);
// }
// 【NGコンパイルエラー】実装(implements)も不可!
// class FakeToken implements SecureToken {
// @override
// String value = ‘fake’;
// }
セキュリティ上の理由から勝手に拡張されたくないデータモデルや、状態管理のステート(Stateクラスなど)において、予期せぬサブクラスの乱立を防ぐために多用されます。
—
3. 開発現場でよくある文法エラーと対策
初学者のうちや、これらの修飾子を導入し始めた頃によくやってしまうミスをいくつか見ておきましょう。
エラーケース 1: `base` クラスを継承したのに、子クラスに修飾子を忘れる
base class Machine {}
// コンパイルエラー:
// The type ‘Computer’ must be marked ‘base’, ‘final’, or ‘sealed’ because it extends a ‘base’ class.
class Computer extends Machine {}
【解説と対策】
Dartでは、`base` クラスを継承するサブクラスは、自身も `base`、`final`、または `sealed` のいずれかの修飾子を持つことが強制されます。これは、継承ツリーの末端に至るまで「継承のルール」を安全に伝播させるための言語仕様です。
修正版:
base class Machine {}
base class Computer extends Machine {} // 解決!
—
まとめ:Dartの型安全な未来へ向けて
今回は、Dartの `base`、`interface`、`final` 修飾子による継承・実装制限について解説しました。
- `base`: 継承 🟢 / 実装 ❌ (実装の再利用を強制)
- `interface`: 継承 ❌ / 実装 🟢 (振る舞いの契約を強制)
- `final`: 継承 ❌ / 実装 ❌ (拡張を完全にブロック)
これらの修飾子を適切に使いこなせるようになると、あなたが作ったパッケージやクラス群を他のエンジニア(あるいは未来の自分)が使うとき、「意図しない使われ方をしてバグる」という事故をコンパイルレベルで完全に防ぐことができるようになります。
ここをクリアできれば、もうDartのオブジェクト指向の基本はバッチリマスターできていますよ!自信を持って、より堅牢なアーキテクチャ設計に挑戦していってくださいね。