【Flutter・Dart】abstract instanceを使用した抽象クラスの理解を深める

Flutter基礎

Flutter × Clean Architectureを現場や個人で使用していて、abstract interfaceを使用した抽象クラスの定義を時々拝見します。
abstract、そしてDart3から登場した abstract intefaceの使い方の理解を深め現場でよりうまく活用できればと思いまとめました。

下記のような観点で基本的な理解を深めたいかたは、ぜひご覧ください。

この記事でわかること

  • abstract interfaceの基本・基礎的な理解
  • abstract interfaceの簡単な使用方法 (Clean Architecture)

Dart3でinterfaceなど新しい修飾子(modifier)が登場し一部更新いたしました。

abstract

abstractとは

abstractとは、Dartにおいて、「インスタンス化できないクラス」を定義するためのキーワードです。

  • 使用する目的:
    • クラスを作る上で、「共通のルール(型)」を定義したい
    • ただ自分自身はインスタンス化されたくない
  • 特徴:
    • メソッドの実装を省略できる(中身のない関数定義だけ)
    • インスタンス化できない
    • implements や extends で具体クラスに引き継がれて実装される
Class modifiers
Modifier keywords for class declarations to control external library access.

簡単なサンプル

// a.dart
abstract class Vehicle {
  void moveForward(int meters);
}
import 'a.dart';

// Error: `Vehicle` can't be instantiated because
// it is marked as `abstract`.
Vehicle myVehicle = Vehicle();

// Can be extended.
class Car extends Vehicle {
  int passengers = 4;

  @override
  void moveForward(int meters) {
    // ...
  }
}

// Can be implemented.
class MockVehicle implements Vehicle {
  @override
  void moveForward(int meters) {
    // ...
  }
}

公式にあった記載があったサンプルです。

  • abstract修飾子を使っているためmyVehicle = Vehicle();のようなインスタンス化はできない
  • Carのようなextendsは可能
  • MockVehicleのようなimplementsも可能
  • moveForward関数のようにオーバライドしてVehicleクラスの関数をextends/implements先で使用できる

interface

interfaceとは

interfaceとは、「インターフェースとして使いたいクラス」につける修飾子です。

  • 使用する目的:
    • 実装から独立した“ルール(契約)”を作り、変更に強い設計にすることため
  • 特徴:
    • extendsで拡張することはできない
      • 同じライブラリ(ファイル内)であればextends可能
    • implementsで実装することは可能
    • インスタンス化も可能
Class modifiers
Modifier keywords for class declarations to control external library access.

簡単なサンプル

// a.dart
interface class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}
import 'a.dart';

// Can be constructed.
Vehicle myVehicle = Vehicle();

// ERROR: `Vehicle` can't be extended in a different library because
// it is marked with `interface`.
class Car extends Vehicle {
  int passengers = 4;
  // ...
}

// Can be implemented.
class MockVehicle implements Vehicle {
  @override
  void moveForward(int meters) {
    // ...
  }
}

同じく公式のサンプルです。

  • myVehicle = Vehicle()のようにインスタンス化することが可能
  • Carクラスのようにextentdすることはできない
  • MockVehicleクラスのようにimplementsすることは可能
  • moveForward関数のようにimplementsしたクラスでオーバーライドして関数を使用可能

abstract interface

abstract interfaceとは

abstract interfaceとは、先ほどの2つの修飾子を組み合わせて

  • 実装を一切持たない、継承できない形だけ定義するクラス
  • Dartで最も壊されにくい強固なインターフェース

です。

  • 使用する目的:
    • 「純粋にルール(インターフェース)だけを定義」したいとき
  • 特徴:
    • implementsで実装することのみ可能
    • extendsで拡張することはできない
    • インスタンス化できない

使用する3つのモチベーション

  • 依存の整理
    • UI層・ドメイン層がインフラ層(Firebaseなど)に依存しない構造にする
  • テスト容易性
    • 実装を差し替えてテスト用のモックに置き換えやすくする
  • スケーラビリティ
    • 実装を簡単に切り替えられるようにして将来の変更に強くする

abstract interfaceをClean Architectureで使用する場合

シチュエーション

  • Flutter × Clean Architecture
  • FirebaseのAuthを監視する
  • RiverpodのProviderで管理する

abstract interfaceを今回使う理由

  • Data層とPresentation層を繋ぐため(疎結合にする)
  • 別のDB(例: supabase)に切り替えても汎用できるようにするため

Data層のFirebaseのAuth情報取得

  • 実際の取得関数をData層で管理(authStateChanges)
  • implementsを使用してAuthRepository(abstract 使用した抽象クラス)を使用
// FirebaseのAuthの状態を取得
class FirebaseAuthRepositoryImpl implements AuthRepository {
  @override
  Stream<User?> authStateChanges() {
    return FirebaseAuth.instance.authStateChanges();
  }
}

今回の中継役となる抽象クラス

abstract interfaceを使用しData層とPresentation層の中継をする

// FirebaseAuthのData層とPresentation層の間のインターフェース
abstract interface class AuthRepository {
  Stream<User?> authStateChanges();
}

Presentation層で使用するProvider

UIに渡すためのProvider(Riverpodを使用)

// 中継役を型としたProvider
final authRepositoryProvider = Provider<AuthRepository>((ref) {
  return FirebaseAuthRepositoryImpl();
});

// 実際のxx_page.dartから呼ぶ出すためのStreamProvider(上記Providerを使用)
final authStateProvider = StreamProvider<User?>((ref) {
  return ref.watch(authRepositoryProvider).authStateChanges();
});

まとめ:abstract interfaceを使うメリット

  • 実装に依存せずUI側のコードを書ける(柔軟性アップ)
  • テストのためにMockに差し替え可能(テスト性アップ)
  • Clean Architecture的な設計と相性が良い(保守性アップ)

最後に

abstract interfaceを使用することで下記のようなメリットあります。

  • テストが容易に
  • 依存関係が整理しやすく
  • 別のサービスに展開する汎用性が上がる

ただ個人開発などの小さいアプリでは不要かもですが、実務や汎用性を高めたいアプリ開発においては、必要かもで理解しとくと便利かなと思います。

ではまた。

コメント

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