こんにちは!Dartの世界へようこそ。
Flutterを使ったアプリ開発や、Dart単体でのバックエンド開発を楽しんでいますか?
他のプログラミング言語(JavaやC#、TypeScriptなど)からDartに入った開発者から、よくこんな質問を受けることがあります。
- 「クラスのプロパティを外部から書き換えられたくない場合、どう書くのがDartらしいの?」
- 「`final` と `getter` って、なんとなく似ているけれどどう使い分けるの?」
ここをクリアすれば、あなたの書くDartコードは一気に堅牢になり、オブジェクト指向の本質を捉えた美しい設計になりますよ。
今回は、Dartの `final` 変数と `getter` を組み合わせた「外部から変更不可能なプロパティの設計パターン」について、言語の裏側の仕組みまで交えながら優しく紐解いていきましょう。
—
1. なぜ「隠蔽(カプセル化)」と「イミュータブル(不変)」が重要なのか?
オブジェクト指向プログラミングにおいて、クラスの内部状態(プロパティ)を野放しにしておくのは、いわば「鍵をかけずに玄関を開けっ放しにしておく」ようなものです。
外部のコードから勝手に値書き換えられてしまうと、「予期せぬタイミングで状態が変わり、バグの温床になる」「どこでバグが起きたのか追跡できなくなる」という悲劇が起きます。
そこで登場するのが、「内部では自由に変更できるけれど、外からは読めるだけで、勝手に書き換えられない(イミュータブルな)状態を公開する」という設計パターンです。Dartはこの設計を非常にエレガントに実現できます。
—
2. 基本のキ:`final` と `getter` の役割
まずは、言葉の定義を整理しておきましょう。
- `final`: 変数に一度だけ値を代入するための修飾子です。コンパイル時、あるいは実行時に一度値が決まると、二度と再代入(書き換え)できません。
- `getter` (`get`): プロパティの値を外部へ「取り出す」ための特別なメソッドです。見た目はプロパティアクセス(`object.name`)のように書けますが、実態はメソッドとして安全に値を返します。
この2つを組み合わせることで、「外からはプロパティに見えるけれど、中身はガチガチに保護された読み取り専用の砦」を作ることができます。
—
3. 実践!安全なプロパティ設計パターン
百聞は一見に如かず。実際にコードを見てみましょう。
銀行の口座残高を管理する `BankAccount` クラスを例にとります。
class BankAccount {
// 1. 内部でのみ使用するプライベートな変数(アンダースコアから始める)
// この値はクラスの外部から直接触ることができません。
double _balance;
// コンストラクタで初期残高を受け取る
BankAccount(this._balance);
// 2. 外部へ公開する「読み取り専用」のgetter
// 外からはあたかも `double` 型の final 変数があるように見えます。
double get balance => _balance;
// 3. お金を入金するメソッド(状態の変更は安全なメソッド経由で行う)
void deposit(double amount) {
if (amount <= 0) {
print('エラー: 0円以下の入金はできません。');
return;
}
_balance += amount;
print('入金成功: +${amount}円(残高: ${_balance}円)');
}
}
void main() {
// 口座を開設(初期残高 10,000円)
final account = BankAccount(10000);
// getterを通じて残高を「読む」ことはできる
print('現在の残高: ${account.balance}円'); // 出力: 現在の残高: 10000.0円
// 🔴 ここでコンパイルエラーになる!
// account.balance = 50000;
// エラーメッセージ: 'balance' can't be used as a setter because it's not a variable.
// (解説: balanceは単なるgetterであり、代入先(セッター)を持たないためコンパイルエラーになります)
// 状態の変更は、用意された安全なメソッド(deposit)経由でのみ行える
account.deposit(5000);
}
このコードが優れている理由
1. プライベート変数 (`_balance`):
Dartでは、識別子の先頭にアンダースコア `_` をつけることで、そのライブラリ(ファイル)内でのみアクセス可能なプライベートメンバーになります。外部のファイルから `account._balance` と直接アクセスすることは絶対にできません。
2. getter (`double get balance => _balance;`):
外部のユーザーは、`account.balance` と書くことで現在の残高を知ることができます。しかし、これは「値のコピー(あるいは参照の読み取り)」を返しているだけであり、変数そのものを渡しているわけではありません。
3. カプセル化の維持:
「残高を増やす」というルール(0円以下は弾くなど)を `deposit` メソッドの中に閉じ込めることができました。外部から勝手に書き換えられる心配がありません。
—
4. さらに進んだパターン:完全なイミュータブルオブジェクト
もし、クラスそのものが持つすべてのプロパティを「一度作ったら二度と変更させたくない」のであれば、クラス内の変数をすべて `final` にしてしまい、そもそも `getter` すら省略して公開してしまうアプローチもよく使われます。
class UserProfile {
// クラスのプロパティ自体を final にする
final String id;
final String username;
// コンストラクタ(イニシャライザ)
const UserProfile({
required this.id,
required this.username,
});
}
void main() {
const user = UserProfile(id: ‘123’, username: ‘DartMaster’);
// user.username = ‘FlutterFan’; // ❌ コンパイルエラー! finalなので再代入不可
}
この「すべて `final` なイミュータブル(不変)パターン」は、FlutterのWidget設計でも至るところで使われています。状態が変化しないため、UIの再描画最適化(パフォーマンス向上)において絶大な効果を発揮するのです。
—
5. 初学者が陥りがちな罠とエラー
ここで、よくある勘違いやエラーを見ておきましょう。
罠1: 「getterってことは、計算結果を返すこともできるの?」
答えはYESです。
getterは単にプライベート変数を返すだけでなく、動的に計算した結果を返すこともできます。これがgetterの非常に強力な点です。
class Rectangle {
final double width;
final double height;
Rectangle(this.width, this.height);
// 面積を計算して返すgetter
// 外からはプロパティ(.area)のように見えるが、アクセスされるたびに計算される
double get area => width height;
}
外からは `rect.area` と書くだけで面積が取れます。中身がどう計算されているかを呼び出し元が気にする必要はありません。
罠2: `final` と `const` の混同
- `final`: 実行時に一度だけ値を代入できる(例:ユーザーからの入力や、APIからのレスポンスを格納する時)
- `const`: コンパイル時(プログラムをビルドする瞬間)に値が完全に決まっている必要がある(例:定数)
プロパティを公開する際は、実行時に決まる値であれば `final` + `getter`(あるいは `final` フィールド)を使いましょう。
—
まとめ
今回は、Dartにおける `final` と `getter` を組み合わせた安全なプロパティ設計について解説しました。
- 内部のデータは `_`(アンダースコア)で隠す(プライベート化する)
- 外部へ見せたい値は `getter` や `final` を使って「読み取り専用」として安全に公開する
- 状態の変更は、ルールを担保した専用のメソッド経由で行う
このパターンをマスターするだけで、あなたの書くコードの安全性・保守性は劇的に跳ね上がります。他の言語からのステップアップとしても、Dartの美しいオブジェクト指向の恩恵を存分に受けてみてくださいね。
ここをクリアすれば、Dartの基本はバッチリマスターできますよ!
次の記事でも、さらに一歩進んだDartのコアな世界へご案内します。お楽しみに!