こんにちは!Dartの世界へようこそ。
Flutterを使ったアプリ開発や、バックエンドでのDartプログラミング、楽しんでいますか?
他のプログラミング言語(JavaやC++、TypeScriptなど)を触ってきた方なら、「クラスの継承やインターフェースの実装」はお馴染みの概念ですよね。でも、APIを設計する側からすると、こんな悩みを持ったことはありませんか?
- 「このクラスは単体で使ってほしいのに、勝手に継承(extends)されて内部構造を書き換えられてしまった……」
- 「このインターフェースは、自ライブラリの中だけで実装を強制したいのに、外部から勝手にimplementsされて仕様変更ができなくなった……」
こうした「クラスの使われ方を設計者の意図通りにガッチリ制限したい」という現場の強い要望に応えるために導入されたのが、Dartのクラス修飾子(`base`、`interface`、`final`)です。
ここをクリアすれば、あなたもDartの型システムを手の内に入れることができますよ。さあ、一緒に本質をマスターしていきましょう!
—
1. そもそもDartのクラス制限はどう進化してきたのか?
かつてのDart(Null安全導入前夜)では、すべてのクラスがデフォルトで「どこからでも継承でき、どこからでも実装できる」というオープンな状態でした。これはライブラリを作る側からすると、ちょっと危険な仕様です。なぜなら、親クラスのプライベートではない内部メソッドをサブクラスが勝手にオーバーライドしてしまい、ライブラリのバージョンアップ時にアプリがクラッシュする原因(いわゆる「脆弱な基底クラス問題」)になり得るからです。
そこでDart 3では、クラスの振る舞いを明確にコントロールするために、以下の新しい修飾子が登場しました。
- `base`: 継承(`extends`)はできるが、インターフェースとしての実装(`implements`)はできない。
- `interface`: 実装(`implements`)はできるが、継承(`extends`)はできない。
- `final`: 継承も実装も一切できない(完全にそのクラスで完結する)。
これらを使いこなすことで、コンパイル時に「意図しない使われ方」を完全にブロックできるようになります。
—
2. それぞれの修飾子の挙動をコードで体感しよう!
百聞は一見に如かず。実際にコードを書いて、それぞれの修飾子がどう働くのかを見ていきましょう。
① `base` クラス:実装を禁止し、継承の安全性(内部実装の保護)を守る
`base`修飾子は、「このクラスを継承して機能拡張するのはOKだけど、インターフェースとして型だけを借りて勝手に中身をゼロから実装し直すのはNG」という場合に使います。なぜなら、サブクラスが `extends` することで、親クラスのメソッドの実装(ロジック)を安全に引き継げるからです。
// baseクラスの定義
base class Vehicle {
void move() {
print(‘乗り物が移動します’);
}
}
// 【OK】extends(継承)はできる!
base class Car extends Vehicle {
@override
void move() {
print(‘車が走ります’);
}
}
// 【コンパイルエラー】implements(実装)はできない!
// エラーメッセージ: The type ‘Vehicle’ can’t be implemented outside of its library.
class Bicycle implements Vehicle {
@override
void move() {
print(‘自転車を漕ぎます’);
}
}
【ここがポイント!】
`base` クラスを継承するクラス側にも、ルールとして `base`(または `final`)を付ける必要があります。これにより、「継承のチェーン」が安全に管理されます。
—
② `interface` クラス:継承を禁止し、型の契約(振る舞い)だけを強制する
Javaの `interface` や TypeScript の `interface` に近い挙動を、Dartのクラスで実現するのがこれです。中身のロジック(具象メソッド)を持たせず、型定義(契約)だけを配りたいときに使います。
// interfaceクラスの定義
interface class Logger {
void log(String message) {
// デフォルトの実装を持たせることも可能ですが…
print(‘LOG: $message’);
}
}
// 【OK】implements(実装)して、独自のロジックを強制できる!
class ConsoleLogger implements Logger {
@override
void log(String message) {
print(‘[CONSOLE]: $message’);
}
}
// 【コンパイルエラー】extends(継承)はできない!
// エラーメッセージ: The class ‘Logger’ can’t be extended outside of its library.
class AdvancedLogger extends Logger {
// 親の内部実装に依存したくないため、継承は禁止される
}
「中身のコードを流用されたくない(継承させたくない)、でもこういうメソッドを持っているという『ルール(型)』だけを強制したい!」という場面で絶大な効果を発揮します。
—
③ `final` クラス:これ以上の拡張を完全にシャットアウトする
「このクラスは、この形で完成しているんだ!」という最終形態(Leaf)を表すのが `final` です。
// finalクラスの定義
final class DatabaseConfig {
final String url = ‘https://api.example.com’;
void connect() {
print(‘データベースに接続します: $url’);
}
}
// 【コンパイルエラー】extends(継承)は絶対にできない!
class MyDatabaseConfig extends DatabaseConfig {}
// 【コンパイルエラー】implements(実装)も絶対にできない!
class FakeDatabaseConfig implements DatabaseConfig {
@override
String get url => ‘http://localhost’;
@override
void connect() {}
}
`final` クラスは、セキュリティ面や、設計上これ以上サブクラスを作らせる必要がないユーティリティクラス、値オブジェクト(Value Object)などに最適です。
—
3. 初学者が陥りやすい「よくある文法エラー」と解決策
これらの修飾子を使い始めると、コンパイラから次のような怒られが発生しがちです。
> 「The subtype ‘X’ of a base class must be base, final, or sealed」
> (baseクラスのサブタイプである ‘X’ は、base、final、または sealed である必要があります)
なぜこのエラーが出るの?
もし親クラスが `base` で「内部実装の安全性を守りたい」と宣言しているのに、それを継承する子クラスが普通の `class`(制限なし)で作られてしまうと、その孫クラスの段階で別のファイルから `implements` さてしまい、結果として安全性が破られてしまいます。
そのため、「`base` クラスを継承するクラスは、自分自身も `base` か `final`(または `sealed`)でなければならない」という伝播のルールがあります。
解決策のコード例:
base class Animal {}
// ❌ NG: 普通のクラスで継承するとコンパイルエラーになる
// class Dog extends Animal {}
// ⭕ OK: 子クラスにも base や final を付与して安全性を担保する
base class Dog extends Animal {}
// または
final class Cat extends Animal {}
このルールを知っていれば、エラーメッセージを見た瞬間に「あ、修飾子のチェーンが途切れているな」と秒速で原因が特定できるようになりますよ。
—
まとめ:API設計の意図をコンパイラに伝えよう
今回は、Dartのクラス修飾子(`base`、`interface`、`final`)について解説しました。
- `base`: 継承はOK、実装はNG(内部実装を守る)
- `interface`: 実装はOK、継承はNG(型の契約だけを強制する)
- `final`: 継承も実装もNG(完全に拡張を塞ぐ)
これらの修飾子は、単なる「おカタい制限」ではありません。「自分やチームのメンバー、そして将来の自分が、意図しないバグを生まないようにするための最強のガードレール」です。
ここをクリアしたあなたは、もうただ動くコードを書くだけのプログラマーではなく、保守性や拡張性を見据えた「美しい設計ができるエンジニア」の扉を開いています。
自信を持って、日々のDart/Flutter開発にこの知見を活かしてくださいね。それでは、次回の記事もお楽しみに!