【入門編】Dartの「base」「interface」「final」クラス修飾子による、変数宣言時の継承・実装制限 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!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開発にこの知見を活かしてくださいね。それでは、次回の記事もお楽しみに!

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