Dart クラスとオブジェクト — オブジェクト指向の中核
クラスはオブジェクトの蓝图である — 良い蓝图は良いオブジェクトを作り、良いオブジェクトは良いシステムを構築する。
1. 学べること
- クラス定義、プロパティとメソッドの宣言
- コンストラクタ:デフォルト / 名前付き / ファクトリ / リダイレクト / 定数
- 継承とメソッドオーバーライド(
@override) - 抽象クラスとインターフェース(暗黙的インターフェース)
- Bob のシナリオ:DataPipeline の Order / Product / Customer モデルクラス
2. 開発者のリアルな物語
(1) 課題:データモデルの混沌によるロジックの分散
Bob の DataPipeline の初期には、すべてのデータが Map で扱われ、型安全性がなかった。order['amout'](タイポ)はランタイムではエラーをスローする代わりに null を返し、10 万件の注文の金額統計が完全にゼロになった。さらに悪いことに、注文を作成するロジックが 5 つのファイルに散在しており、統一された検証がなかった。
(2) クラスによる解決
クラスを使ってデータモデルを定義し、コンストラクタで検証ロジックを一元化し、型システムを使ってコンパイル時にタイポや型の不一致を検出する。
class Order {
final String id;
final double amount;
final DateTime date;
Order({required this.id, required this.amount, required this.date})
: assert(amount > 0, 'Amount must be positive');
double calcTax(double rate) => amount * rate;
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- 型エラーがランタイムからコンパイル時へと移行し、バグが 80% 削減
- コンストラクタが統一されたデータ検証を提供し、検証漏れを撲滅
- 継承とインターフェースによりコードの再利用率が 60% 向上
3. クラス定義の基礎
(1) クラスの構造
▶ サンプル:基本的なクラス定義
class Product {
// フィールド
final String name;
final double price;
String category;
// コンストラクタ
Product({required this.name, required this.price, this.category = 'General'});
// メソッド
String formatPrice() => '\$${price.toStringAsFixed(2)} USD';
// ゲッター
bool get isExpensive => price >= 1000;
// toString をオーバーライド
@override
String toString() => 'Product($name, ${formatPrice()}, $category)';
}
void main() {
final product = Product(name: 'Laptop', price: 1299.99, category: 'Electronics');
print(product); // Product(Laptop, $1299.99 USD, Electronics)
print(product.isExpensive); // true
print(product.formatPrice()); // $1299.99 USD
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| クラスメンバー | 宣言構文 | 説明 |
|---|---|---|
| フィールド | Type name |
インスタンス変数 |
| コンストラクタ | ClassName() |
オブジェクトを作成 |
| メソッド | returnType name() {} |
インスタンスメソッド |
| ゲッター | Type get name => expr |
計算プロパティ |
| セッター | set name(Type value) |
代入のインターセプト |
4. 5 種類のコンストラクタ
(1) DataPipeline ドメインモデル
classDiagram
class Order {
+String id
+double amount
+DateTime date
+calcTax(double rate) double
}
class Product {
+String name
+double price
+formatUSD() String
}
class Customer {
+String name
+List~Order~ orders
}
Customer "1" --> "*" Order : has
Order "1" --> "*" Product : contains
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:デフォルトコンストラクタと名前付きコンストラクタ
class Order {
final String id;
final double amount;
final DateTime date;
String status;
// デフォルトコンストラクタ
Order({required this.id, required this.amount, required this.date, this.status = 'pending'});
// 名前付きコンストラクタ - CSV 行から作成
Order.fromCsv(String csvLine)
: id = csvLine.split(',')[0],
amount = double.parse(csvLine.split(',')[1]),
date = DateTime.parse(csvLine.split(',')[2]),
status = 'pending';
// 名前付きコンストラクタ - デフォルトの日付で作成
Order.now({required this.id, required this.amount})
: date = DateTime.now(),
status = 'pending';
double calcTax(double rate) => amount * rate;
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)}, $status)';
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:ファクトリコンストラクタ
class DataSource {
final String type;
final String connection;
// プライベートコンストラクタ
DataSource._internal(this.type, this.connection);
// ファクトリコンストラクタ - キャッシュまたはカスタムインスタンスを返す
factory DataSource.create({required String type, required String connection}) {
return DataSource._internal(type, connection);
}
// デフォルト設定を持つファクトリ
factory DataSource.api(String endpoint) =>
DataSource._internal('api', endpoint);
factory DataSource.file(String path) =>
DataSource._internal('file', path);
factory DataSource.database(String connectionString) =>
DataSource._internal('database', connectionString);
}
void main() {
final api = DataSource.api('https://api.example.com/orders');
final file = DataSource.file('/data/orders.csv');
print('${api.type}: ${api.connection}');
print('${file.type}: ${file.connection}');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:リダイレクトコンストラクタ
class Report {
final String title;
final String format;
final int recordCount;
// メインコンストラクタ
Report({required this.title, this.format = 'json', this.recordCount = 0});
// リダイレクトコンストラクタ
Report.json(String title, int count) : this(title: title, format: 'json', recordCount: count);
Report.csv(String title, int count) : this(title: title, format: 'csv', recordCount: count);
Report.html(String title, int count) : this(title: title, format: 'html', recordCount: count);
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:定数コンストラクタ
class Config {
final String appName;
final int maxRecords;
// const コンストラクタ - 全フィールドが final 必須
const Config({this.appName = 'DataPipeline', this.maxRecords = 1000000});
}
void main() {
const config1 = Config(); // コンパイル時定数
const config2 = Config(maxRecords: 500000);
print(identical(config1, config2)); // false (異なる値)
print(config1.maxRecords); // 1000000
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| コンストラクタタイプ | 構文 | 特徴 | シナリオ |
|---|---|---|---|
| デフォルト | ClassName() |
自動生成またはカスタマイズ | 標準作成 |
| 名前付き | ClassName.name() |
複数の作成方法 | fromCsv、now |
| ファクトリ | factory ClassName() |
サブクラスやキャッシュインスタンスを返せる | シングルトン、キャッシュ |
| リダイレクト | : this() |
メインコンストラクタに委譲 | 便利な作成 |
| 定数 | const ClassName() |
コンパイル時に作成、イミュータブル | 設定定数 |
5. 継承とメソッドオーバーライド
▶ サンプル:継承階層
// ベースクラス
class DataProcessor {
final String name;
DataProcessor(this.name);
// オーバーライド対象のメソッド
String process(String input) => 'Processing $input with $name';
// オーバーライド不可のメソッド
String getStatus() => 'Ready';
@override
String toString() => '$name Processor';
}
// サブクラス
class OrderProcessor extends DataProcessor {
final double taxRate;
OrderProcessor(this.taxRate) : super('Order');
@override
String process(String input) {
final amount = double.tryParse(input) ?? 0;
final taxed = amount * (1 + taxRate);
return 'Order processed: \$${taxed.toStringAsFixed(2)} USD';
}
}
// 別のサブクラス
class ProductProcessor extends DataProcessor {
ProductProcessor() : super('Product');
@override
String process(String input) => 'Product cataloged: $input';
}
void main() {
final orderProc = OrderProcessor(0.08);
print(orderProc.process('1500')); // Order processed: $1620.00 USD
print(orderProc.getStatus()); // Ready
final productProc = ProductProcessor();
print(productProc.process('Laptop')); // Product cataloged: Laptop
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
6. 抽象クラスとインターフェース
(1) 抽象クラス
▶ サンプル:抽象クラスと実装
// 抽象クラス - インスタンス化できない
abstract class ReportGenerator {
final String format;
ReportGenerator(this.format);
// 抽象メソッド - 実装必須
String generate(List<Map<String, dynamic>> data);
// 具象メソッド - 共有実装
String header(String title) => '=== $title ($format) ===';
}
class JsonReportGenerator extends ReportGenerator {
JsonReportGenerator() : super('json');
@override
String generate(List<Map<String, dynamic>> data) {
final buffer = StringBuffer();
buffer.writeln(header('DataPipeline Report'));
for (final entry in data) {
buffer.writeln(' ${entry['id']}: ${entry['amount']}');
}
return buffer.toString();
}
}
class CsvReportGenerator extends ReportGenerator {
CsvReportGenerator() : super('csv');
@override
String generate(List<Map<String, dynamic>> data) {
final lines = <String>['id,amount'];
for (final entry in data) {
lines.add('${entry['id']},${entry['amount']}');
}
return lines.join('\n');
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(2) 暗黙的インターフェース
Dart のすべてのクラスは暗黙的にインターフェースを定義し、すべてのインスタンスメソッドとフィールドを含む。他のクラスはこのインターフェースを implements できる。
▶ サンプル:暗黙的インターフェースの実装
class FileDataSource {
final String path;
FileDataSource(this.path);
List<String> readLines() => ['line1', 'line2'];
bool get exists => true;
}
// FileDataSource の暗黙的インターフェースを実装
class MockDataSource implements FileDataSource {
@override
String path = '/mock/data.csv';
@override
List<String> readLines() => ['mock1', 'mock2', 'mock3'];
@override
bool get exists => true;
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 観点 | 抽象クラス | インターフェース(implements) | ミックスイン |
|---|---|---|---|
| インスタンス化 | 不可 | — | 不可 |
| メソッド実装 | 任意 | すべて再実装必須 | 任意 |
| 多重継承 | 単一継承 | 複数実装可能 | 複数ミックスイン可能 |
| コンストラクタ | あり | なし | なし |
7. 完全なサンプル:DataPipeline ドメインモデル
// ============================================
// DataPipeline ドメインモデル
// 完全な OOP による Order、Product、Customer
// ============================================
class Product {
final String id;
final String name;
final double price;
final String category;
const Product({
required this.id,
required this.name,
required this.price,
this.category = 'General',
});
String formatUSD() => '\$${price.toStringAsFixed(2)} USD';
@override
String toString() => 'Product($name, ${formatUSD()})';
}
class Order {
final String id;
final double amount;
final DateTime date;
String status;
final List<Product> products;
Order({
required this.id,
required this.amount,
required this.date,
this.status = 'pending',
this.products = const [],
}) : assert(amount > 0, 'Amount must be positive');
Order.now({required this.id, required this.amount, this.products = const []})
: date = DateTime.now(),
status = 'pending';
double calcTax(double rate) => amount * rate;
double calcTotal(double taxRate) => amount + calcTax(taxRate);
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)}, $status, ${products.length} items)';
}
class Customer {
final String name;
final String email;
final List<Order> orders;
Customer({required this.name, required this.email, List<Order>? orders})
: orders = orders ?? [];
double get totalSpent => orders.fold(0.0, (sum, o) => sum + o.amount);
int get orderCount => orders.length;
double get averageOrderValue => orderCount > 0 ? totalSpent / orderCount : 0;
void addOrder(Order order) => orders.add(order);
String tier() => switch (totalSpent) {
>= 50000 => 'Enterprise',
>= 10000 => 'Premium',
>= 1000 => 'Standard',
_ => 'Free',
};
@override
String toString() => 'Customer($name, ${tier()}, ${orderCount} orders, \$${totalSpent.toStringAsFixed(2)} total)';
}
void main() {
final laptop = Product(id: 'P001', name: 'Laptop', price: 1299.99, category: 'Electronics');
final mouse = Product(id: 'P002', name: 'Mouse', price: 29.99, category: 'Electronics');
final alice = Customer(name: 'Alice', email: 'alice@example.com');
final bob = Customer(name: 'Bob', email: 'bob@example.com');
alice.addOrder(Order(id: 'ORD-001', amount: 1500.0, date: DateTime(2024, 1, 15), products: [laptop]));
alice.addOrder(Order(id: 'ORD-002', amount: 3200.0, date: DateTime(2024, 2, 20)));
bob.addOrder(Order(id: 'ORD-003', amount: 890.0, date: DateTime(2024, 3, 10), products: [mouse]));
print('=== DataPipeline Customers ===');
print(alice);
print(bob);
print('\n--- Alice Orders ---');
for (final order in alice.orders) {
print(' $order (tax: \$${order.calcTax(0.08).toStringAsFixed(2)} USD)');
}
print('\nTier Summary:');
print(' Alice: ${alice.tier()} (\$${alice.totalSpent.toStringAsFixed(2)} total)');
print(' Bob: ${bob.tier()} (\$${bob.totalSpent.toStringAsFixed(2)} total)');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
=== DataPipeline Customers ===
Customer(Alice, Premium, 2 orders, $4700.00 total)
Customer(Bob, Standard, 1 orders, $890.00 total)
--- Alice Orders ---
Order(ORD-001, $1500.00, pending, 1 items) (tax: $120.00 USD)
Order(ORD-002, $3200.00, pending, 0 items) (tax: $256.00 USD)
Tier Summary:
Alice: Premium ($4700.00 total)
Bob: Standard ($890.00 total)
❓ よくある質問
Q: Dart は多重継承に対応していますか? A: いいえ。Dart は単一継承(
extends)のみサポートしますが、implementsで複数のインターフェースを実装したり、withで複数の ミックスイン をミックスインしたりできます。
Q: ファクトリコンストラクタと通常のコンストラクタの違いは何ですか? A: 通常のコンストラクタは常に新しいインスタンスを作成しますが、ファクトリコンストラクタはキャッシュされたインスタンス、サブクラスのインスタンスを返したり、何を返すかを決定する前に複雑なロジックを実行したりできます。
factoryキーワードで宣言されます。
Q:
constコンストラクタは何のためにありますか? A:constコンストラクタはコンパイル時定数オブジェクトを作成します。同じ引数を持つconstコンストラクタは同じ(同一の)インスタンスを返します。設定クラスや列挙値のようなイミュータブルオブジェクトに適しています。
Q:
abstract classとinterfaceのどちらを選ぶべきですか? A: 実装コードの共有が必要なときはextendsでabstract classを使います。コントラクトの定義だけが必要なときはinterface(implements経由)を使います。Dart には専用のinterfaceキーワードはなく、すべてのクラス自体がインターフェースです。
Q: いつ
@overrideを使うべきですか? A: 親クラスのメソッドをオーバーライドする際は@overrideを付けるべきです。厳密には必須ではありませんが、付けることでコンパイラが正しくオーバーライドしているかのチェックを支援します(親クラスにそのメソッドがない場合はエラーを報告します)。
Q: Dart の
assertは本番環境で有効ですか? A: いいえ。assertはデバッグモードでのみ実行され、リリースモードでは無視されます。開発時の検証に使われるものであり、適切なパラメータ検証の代替にはなりません。
Q: 名前付きコンストラクタはファクトリにできますか? A: はい。例えば
factory Order.fromCsv(String line)は有効です。パースに失敗した場合、例外をスローする代わりにデフォルトインスタンスを返すことができます。
📖 まとめ
- クラス定義(フィールド、コンストラクタ、メソッド、ゲッター/セッター)はデータモデルの中核。
- 5 種類のコンストラクタには異なる用途がある:名前付き(複数の作成方法)、ファクトリ(キャッシュ/サブクラス)、リダイレクト(便利さ)、定数(不変性)。
- 単一継承 + 複数インターフェース実装 + 複数 ミックスイン 構成により柔軟な組み合わせが可能。
- 抽象クラスは共有実装を提供し、暗黙的インターフェースはコントラクトを定義する。
- DataPipeline の Order/Product/Customer がドメインモデルを形成し、ビジネスロジックを支える。
📝 練習問題
- 基礎(難易度 ⭐):
name、price、categoryフィールドを持つProductクラスを定義してください。formatUSD()メソッドとisExpensiveゲッターを追加します。3 つの Product インスタンスを作成して出力しましょう。 - 中級(難易度 ⭐⭐):
Orderクラスに名前付きコンストラクタOrder.fromMap(Map から作成)とファクトリコンストラクタOrder.safe(パース失敗時にデフォルト注文を返す)を追加してください。エッジケースをテストします。 - 挑戦(難易度 ⭐⭐⭐):抽象クラス
ReportGeneratorを設計してください。2 つのサブクラスJsonReportとCsvReportを作成し、多態的なレポート生成を実装します。次に同じインターフェースをimplementsするMockReportを作成してテストします。