🔩

Strategy Pattern を Flutterで使ってみた

2025/01/26に公開

Strategy Pattern in Flutter with GoRouter

概要

このプロジェクトは、Flutterアプリケーションにおけるストラテジーパターン(Strategy Pattern)の実践的な実装例を示しています。

https://youtube.com/shorts/MhEeP8WTPfs

こちらが完成品

ストラテジーパターンとは

ストラテジーパターンは、一連のアルゴリズムをカプセル化し、実行時にそれらを交換可能にするデザインパターンです。主な特徴は以下の通りです:

  • 振る舞いを切り替える柔軟性の提供
  • 条件分岐の削減
  • 拡張性の向上
  • オープン・クローズドの原則の実践

この実装の特徴

1. 抽象戦略の定義

abstract class RoleValidationStrategy {
  bool validate(String role);
  String getResultEmoji(String role);
}

2. 具体的な戦略の実装

class AdminRoleValidation implements RoleValidationStrategy {
  
  bool validate(String role) {
    final adminRoles = ['admin', 'super_admin', 'root'];
    return adminRoles.contains(role.toLowerCase());
  }

  
  String getResultEmoji(String role) {
    return validate(role) ? '⭕️' : '❌';
  }
}

3. 戦略の選択と管理

class RoleValidator {
  static RoleType parseRole(String role) {
    final adminValidation = AdminRoleValidation();
    final nonAdminValidation = NonAdminRoleValidation();

    if (adminValidation.validate(role)) {
      return RoleType.admin;
    } else if (nonAdminValidation.validate(role)) {
      return RoleType.nonAdmin;
    } else {
      return RoleType.unknown;
    }
  }
}

4. GoRouterとの統合

GoRouterを使用して、異なる戦略(ロール)に基づくナビゲーションを実現:

GoRoute(
  path: 'role',
  builder: (context, state) {
    final role = state.uri.queryParameters['role'] ?? '';
    return RoleValidationPage(role: role);
  },
)

メリット

  • 柔軟性: 新しいロール検証戦略を簡単に追加可能
  • 拡張性: 既存のコードを変更せずに新しい振る舞いを導入可能
  • テスタビリティ: 各戦略を個別にテスト可能
  • 関心の分離: 異なる検証ロジックを明確に分離

使用例

// ロールを検証
final roleType = RoleValidator.parseRole('admin');

// 対応するUIを表示
switch (roleType) {
  case RoleType.admin:
    // 管理者用のUIを表示
  case RoleType.nonAdmin:
    // 一般ユーザー用のUIを表示
}

注意点

  • パフォーマンスに注意(多数の戦略がある場合)
  • 戦略の数が増えすぎないよう設計を慎重に

example

ロールのenumとインターフェースのクラスを定義。

abstract interface class UserCheck {
  bool validate(String input);
  String getResultEmoji();
}

abstract class RoleValidationStrategy {
  bool validate(String role);
  String getResultEmoji(String role);
}

enum RoleType {
  admin,
  nonAdmin,
  unknown
}

オーバーライドして機能を実装したクラスを定義する。このロジックを外から渡すことでページごとにアルゴリズムを切り替えることを実現できます。

import '../interface/user_check.dart';

class AdminRoleValidation implements RoleValidationStrategy {
  
  bool validate(String role) {
    final adminRoles = ['admin', 'super_admin', 'root'];
    return adminRoles.contains(role.toLowerCase());
  }

  
  String getResultEmoji(String role) {
    return validate(role) ? '⭕️' : '❌';
  }
}

class NonAdminRoleValidation implements RoleValidationStrategy {
  
  bool validate(String role) {
    final nonAdminRoles = ['user', 'guest', 'member'];
    return nonAdminRoles.contains(role.toLowerCase());
  }

  
  String getResultEmoji(String role) {
    return validate(role) ? '⭕️' : '❌';
  }
}

class UnknownRoleValidation implements RoleValidationStrategy {
  
  bool validate(String role) {
    return false;
  }

  
  String getResultEmoji(String role) {
    return '🙅‍♀️';
  }
}

class RoleValidator {
  static RoleType parseRole(String role) {
    final adminValidation = AdminRoleValidation();
    final nonAdminValidation = NonAdminRoleValidation();

    if (adminValidation.validate(role)) {
      return RoleType.admin;
    } else if (nonAdminValidation.validate(role)) {
      return RoleType.nonAdmin;
    } else {
      return RoleType.unknown;
    }
  }

  static RoleValidationStrategy getStrategy(RoleType roleType) {
    return switch (roleType) {
      RoleType.admin => AdminRoleValidation(),
      RoleType.nonAdmin => NonAdminRoleValidation(),
      RoleType.unknown => UnknownRoleValidation(),
    };
  }
}

Viewのコードを作成。うまいこと他のファイルにモジュールを分割してコードを削減しました🧑‍💻
渡されたクエリパラメーターによってネストした詳細ページのViewが切り替わる仕組みになっております。

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'get_user/get_user.dart';
import 'interface/user_check.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({super.key});

  final _router = GoRouter(
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => const HomePage(),
        routes: [
          GoRoute(
            path: 'role',
            builder: (context, state) {
              // クエリパラメーターからロールを取得
              final role = state.uri.queryParameters['role'] ?? '';
              return RoleValidationPage(role: role);
            },
          ),
        ],
      ),
    ],
  );

  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Role Validation Demo',
      routerConfig: _router,
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Role Validation')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => context.go('/role?role=admin'),
              child: const Text('Check Admin Role'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => context.go('/role?role=user'),
              child: const Text('Check Non-Admin Role'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => context.go('/role?role=anonymous'),
              child: const Text('Check Unknown Role'),
            ),
          ],
        ),
      ),
    );
  }
}

class RoleValidationPage extends StatelessWidget {
  final String role;

  const RoleValidationPage({super.key, required this.role});

  
  Widget build(BuildContext context) {
    // ロールタイプを解析
    final roleType = RoleValidator.parseRole(role);

    return Scaffold(
      appBar: AppBar(title: const Text('Role Validation')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Role: $role',
              style: const TextStyle(fontSize: 24),
            ),
            const SizedBox(height: 20),
            Text(
              () {
                return switch (roleType) {
                  RoleType.admin => '⭕️ 管理者ユーザーです',
                  RoleType.nonAdmin => '⭕️ 一般ユーザーです',
                  RoleType.unknown => '🙅‍♀️ 無効なロール',
                };
              }(),
              style: const TextStyle(fontSize: 15),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => context.go('/'),
              child: const Text('Back to Home'),
            ),
          ],
        ),
      ),
    );
  }
}

最後に

ストラテジーパターンは、Flutterアプリケーションにおいて振る舞いの切り替えを柔軟かつ拡張可能にする強力なデザインパターンです。

仕事ではじめて使う機会があったのですが、表示するページによってメソッドは同じだが外から渡すクラスによって、表示するWidgetとAPIのデータを変更するだけで履歴ページの表示を切り替えるのに使用していました。

他の活用例:

  • アニメーションの切り替え
  • 認証方法の切り替え(メール、Google、Appleなど)
  • データの永続化方法の切り替え(SQLite、Hive、SharedPreferences)

Discussion