Dart クラスとオブジェクト — オブジェクト指向の中核

クラスはオブジェクトの蓝图である — 良い蓝图は良いオブジェクトを作り、良いオブジェクトは良いシステムを構築する。

1. 学べること


2. 開発者のリアルな物語

(1) 課題:データモデルの混沌によるロジックの分散

Bob の DataPipeline の初期には、すべてのデータが Map で扱われ、型安全性がなかった。order['amout'](タイポ)はランタイムではエラーをスローする代わりに null を返し、10 万件の注文の金額統計が完全にゼロになった。さらに悪いことに、注文を作成するロジックが 5 つのファイルに散在しており、統一された検証がなかった。

(2) クラスによる解決

クラスを使ってデータモデルを定義し、コンストラクタで検証ロジックを一元化し、型システムを使ってコンパイル時にタイポや型の不一致を検出する。

DART
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;
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

(3) 効果


3. クラス定義の基礎

(1) クラスの構造

▶ サンプル:基本的なクラス定義

DART
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
}
TEXT
> 出力: ローカルの 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 ドメインモデル

100%
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
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:デフォルトコンストラクタと名前付きコンストラクタ

DART
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)';
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:ファクトリコンストラクタ

DART
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}');
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:リダイレクトコンストラクタ

DART
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);
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:定数コンストラクタ

DART
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
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
コンストラクタタイプ 構文 特徴 シナリオ
デフォルト ClassName() 自動生成またはカスタマイズ 標準作成
名前付き ClassName.name() 複数の作成方法 fromCsv、now
ファクトリ factory ClassName() サブクラスやキャッシュインスタンスを返せる シングルトン、キャッシュ
リダイレクト : this() メインコンストラクタに委譲 便利な作成
定数 const ClassName() コンパイル時に作成、イミュータブル 設定定数

5. 継承とメソッドオーバーライド

▶ サンプル:継承階層

DART
// ベースクラス
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
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

6. 抽象クラスとインターフェース

(1) 抽象クラス

▶ サンプル:抽象クラスと実装

DART
// 抽象クラス - インスタンス化できない
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');
  }
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

(2) 暗黙的インターフェース

Dart のすべてのクラスは暗黙的にインターフェースを定義し、すべてのインスタンスメソッドとフィールドを含む。他のクラスはこのインターフェースを implements できる。

▶ サンプル:暗黙的インターフェースの実装

DART
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;
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
観点 抽象クラス インターフェース(implements) ミックスイン
インスタンス化 不可 不可
メソッド実装 任意 すべて再実装必須 任意
多重継承 単一継承 複数実装可能 複数ミックスイン可能
コンストラクタ あり なし なし

7. 完全なサンプル:DataPipeline ドメインモデル

DART
// ============================================
// 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)');
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

出力:

TEXT
=== 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 classinterface のどちらを選ぶべきですか? A: 実装コードの共有が必要なときは extendsabstract class を使います。コントラクトの定義だけが必要なときは interfaceimplements 経由)を使います。Dart には専用の interface キーワードはなく、すべてのクラス自体がインターフェースです。

Q: いつ @override を使うべきですか? A: 親クラスのメソッドをオーバーライドする際は @override を付けるべきです。厳密には必須ではありませんが、付けることでコンパイラが正しくオーバーライドしているかのチェックを支援します(親クラスにそのメソッドがない場合はエラーを報告します)。

Q: Dart の assert は本番環境で有効ですか? A: いいえ。assert はデバッグモードでのみ実行され、リリースモードでは無視されます。開発時の検証に使われるものであり、適切なパラメータ検証の代替にはなりません。

Q: 名前付きコンストラクタはファクトリにできますか? A: はい。例えば factory Order.fromCsv(String line) は有効です。パースに失敗した場合、例外をスローする代わりにデフォルトインスタンスを返すことができます。


📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐)namepricecategory フィールドを持つ Product クラスを定義してください。formatUSD() メソッドと isExpensive ゲッターを追加します。3 つの Product インスタンスを作成して出力しましょう。
  2. 中級(難易度 ⭐⭐)Order クラスに名前付きコンストラクタ Order.fromMap(Map から作成)とファクトリコンストラクタ Order.safe(パース失敗時にデフォルト注文を返す)を追加してください。エッジケースをテストします。
  3. 挑戦(難易度 ⭐⭐⭐):抽象クラス ReportGenerator を設計してください。2 つのサブクラス JsonReportCsvReport を作成し、多態的なレポート生成を実装します。次に同じインターフェースを implements する MockReport を作成してテストします。

← 前のレッスン | 次のレッスン →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%