Primary About Development

クラスのデザインパターン

2023-12-14

ソフトウェア開発において、デザインパターンは複雑な問題を解決するための重要なツールです。一般的にデザインパターンは、複数のクラスやオブジェクト間の相互作用に焦点を当てています。しかし、単一のクラスにおいても、さまざまな利用方法やパターンが存在し、これらはしばしば見過ごされがちです。本記事では、単一のクラスの利用方法を3つのパターンに分類し、それぞれの特徴、利用シナリオ、および利点と欠点を詳細に解説します。

この分類により、開発者はクラス設計の際により多くの選択肢と明確なガイドラインを持つことができます。また、これらのパターンを適切に適用することで、ソフトウェアの柔軟性、再利用性、およびメンテナンス性が大幅に向上します。本記事では、データクラスパターン、サービスクラスパターン、およびステートホルダークラスパターンの3つを取り上げ、それぞれのパターンについて解説していきます。これらのパターンは、データの構造化、ビジネスロジックの集中化、そして状態管理とUIへの通知という異なる側面に焦点を当てています。

それでは、単一のクラスの使い方を変えるだけで、ソフトウェア設計の可能性がどのように広がるかを見ていきましょう。

全体の概要

  1. データクラスパターン:

    • イミュータブルなフィールドを持つクラスを用いて、副作用を避け、データを構造体的に扱う。
    • ビジネスロジックを含む、シンプルなデータ表現が必要な場合に適用。
    • ファクトリーメソッドを用いた柔軟なオブジェクト生成が特徴。
    • 例として、Personクラスが挙げられており、イミュータブルな性質がFlutterなどのフレームワークで有利に働く。
  2. サービスクラスパターン:

    • 状態を持たず、主にビジネスロジックやドメイン固有の操作を実行する。
    • ビジネスロジックの集中化、テストの容易さ、再利用性の向上が目的。
    • PersonServiceの例では、PersonRepositoryを通じてPersonオブジェクトの操作を行う。
  3. ステートホルダークラスパターン:

    • クラスが内部状態を持ち、状態変化に基づいて特定のアクションをトリガーする。
    • ビジネスロジックは外部モジュールに委ね、状態管理に注力。
    • ChangeNotifierを使用して、FlutterのUIに状態変更を通知する。
    • PersonStateHolderの例では、PersonServiceを使用してPersonの状態を変更し、UIへ通知する。

データクラスパターン

概要:

  • 目的: クラスを構造体的に扱い、イミュータブルなフィールドを通じて副作用を避ける。
  • カテゴリ: 生成、構造、振る舞い

適用可能性:

  • ビジネスロジックを含むPOJO(Plain Old Java Object)なドメインモデルを構築する際。
  • 何らかのデータ構造が必要な場合、基本的にはこのパターンの適用を考える。
  • データの不変性と一貫性を保ちつつ、シンプルなデータの表現が求められるシナリオ。

構成要素:

  • フィールド: クラスのフィールドは全てイミュータブル(変更不可)であるべき。
  • コンストラクタ: オブジェクトの初期化に使用。
  • Getterメソッド: 各フィールドの値を取得するためのメソッド。計算が必要なフィールドに対しては、計算を行って値を返す。
  • フィールド変更メソッド: 特定のフィールドを変更する必要がある場合、元のオブジェクトは変更せず、変更を含む新しいオブジェクトを生成して返す。
  • ファクトリーメソッド: クラス自身によってオブジェクトを生成するスタティックメソッド。

実装手順:

  1. クラスのフィールドをイミュータブルとして定義。
  2. 必要なgetterメソッドを提供。
  3. フィールドの変更が必要な場合は、新しいインスタンスを生成して返す。
  4. 特定の条件下でのオブジェクト生成を容易にするため、ファクトリーメソッドを提供。

利点と欠点:

  • 利点:
    • イミュータブルなオブジェクトは、サイドエフェクトを減らし、扱いやすい。
    • 計算可能な値はgetterで提供し、インスタンス変数を持つように見せることができる。
    • ファクトリーメソッド(言語でサポートしてない場合, staticメソッド)にて生成を行うことで、複数の方法でのデータの初期化を表現できる。
    • Flutterなどのフレームワークで変更を検知する際に、イミュータブルなオブジェクトは有利。
  • 欠点:
    • オブジェクトの変更が頻繁に発生する場合、新しいインスタンスの生成がオーバーヘッドになることがある。

実例:

class Person {
  final String id;
  final DateTime birthday;
  final String familyName;

  // プライベートコンストラクタ
  Person._(this.id, this.birthday, this.familyName);

  // 誕生時のファクトリーメソッド
  factory Person.born({String familyName}) {
    return Person._(Uuid().v4() DateTime.now(), familyName);
  }

  // 結婚による家族名の変更を反映するメソッド
  Person changeFamilyNameUponMarriage(String newFamilyName) {
    return Person._(this.id, this.birthday, newFamilyName);
  }

  // 年齢の計算
  int get age {
    var now = DateTime.now();
    var age = now.year - birthday.year;
    if (now.month < birthday.month || (now.month == birthday.month && now.day < birthday.day)) {
      age--;
    }
    return age;
  }
}

このクラス定義では以下の点に注意しています:

  • Person._ はプライベートコンストラクタです。これにより、クラスの外部から直接インスタンス化することはできません。
  • factory Person.born は、生まれたとき(現在の日時)のPersonオブジェクトを生成するファクトリーメソッドです。
  • changeFamilyNameUponMarriage メソッドは、結婚による家族名の変更を処理します。新しい家族名を受け取り、新しいPersonインスタンスを生成して返します。

サービスクラスパターン

概要:

  • 目的: ドメインロジックを集中させ、状態を持たないクラスを通じて処理を実行する。
  • カテゴリ: 振る舞い

適用可能性:

  • ビジネスロジックやドメイン固有の操作をカプセル化する必要がある場合。
  • データや状態を直接持たずに処理を行うシナリオ。
  • 再利用可能で、テストしやすいコードを必要とする場合。

構成要素:

  • サービスメソッド: クラスに定義される具体的なビジネスロジックや処理。
  • 依存関係の注入: 必要なオブジェクトやサービスをコンストラクタ経由またはセッターメソッドを使用して注入。
  • イミュータブルなフィールド: インスタンスが持つフィールドはイミュータブル(変更不可)。

実装手順:

  1. クラスに必要なビジネスロジックを定義。
  2. コンストラクタまたはセッターを通じて依存するオブジェクトを注入。
  3. イミュータブルなフィールドを使用して、状態の変更を防ぐ。

利点と欠点:

  • 利点:
    • ロジックの集中化により、コードの再利用性とメンテナンス性が向上。
    • DIにより、テストのしやすさと、異なる環境への適応性が高まる。
    • 状態を持たないため、サイドエフェクトのリスクが低減。
  • 欠点:
    • 複数の依存関係が存在する場合、クラスが複雑になる可能性。
    • ビジネスロジックの密集により、適切な分離が行われないと可読性が低下することがある。

了解しました。サービスクラスパターンの例として、先ほどのPersonクラスに関連するビジネスロジックを扱うPersonServiceクラスを示します。このクラスはPersonオブジェクトに関する操作やロジックをカプセル化し、依存関係を注入して処理を行います。

実例

class PersonRepository {
  // このリポジトリクラスは、Personオブジェクトの保存や取得を担う
  Future<Person> save(Person person) async {
    // データベースにPersonを保存するロジック
    // 保存したPersonオブジェクトを返す
  }

  Future<Person> findById(String id) async {
    // IDに基づいてPersonオブジェクトを取得するロジック
    // 取得したPersonオブジェクトを返す
  }
}

class PersonService {
  final PersonRepository personRepository;

  PersonService(this.personRepository);

  Future<Person> createPerson(String familyName) async {
    var newPerson = Person.born(familyName: familyName);
    return personRepository.save(newPerson);
  }

  Future<Person> loadPerson(String personId) async {
    return personRepository.findById(personId);
  }

  Future<Person> changeFamilyName(String personId, String newFamilyName) async {
    var person = await personRepository.findById(personId);
    var updatedPerson = person.changeFamilyNameUponMarriage(newFamilyName);
    return personRepository.save(updatedPerson);
  }
}

説明:

  • PersonRepository: Personオブジェクトの永続化(保存や取得)を担当するクラスです。データベースや他のデータソースとのやり取りを抽象化しています。
  • PersonService: Personに関するビジネスロジックを実装するクラスです。PersonRepositoryを使用して、Personオブジェクトの作成や更新などの処理を行います。

ステートホルダークラスパターン

概要:

  • 目的: オブジェクトの状態を内部に保持し、その状態の変化に応じて特定のアクションをトリガーする。ビジネスロジックや複雑な処理は外部モジュールに移譲し、状態管理に注力する。
  • カテゴリ: ビヘイビオラル(振る舞い)

適用可能性:

  • 状態の管理とそれに伴うアクションの実行が主要な責務であり、ビジネスロジックは別のクラスやモジュールで管理する場合。
  • シンプルな状態管理が必要で、その状態変化に基づくアクションのトリガーが求められるシナリオ。

構成要素:

  • 内部状態: クラスが保持するミュータブルな状態(例:Personオブジェクト)。
  • 状態変更メソッド: 状態を変更するメソッド。
  • アクショントリガー: 状態の変化に応じて特定のアクション(例:UIの更新)をトリガーするメカニズム。
  • ビジネスロジックの移譲: 複雑な処理やビジネスロジックは、外部のサービスクラスやユーティリティクラスに移譲。

実装手順:

  1. クラス内に状態を表すミュータブルなフィールドを定義。
  2. 状態を変更するメソッドを実装し、状態の変更時に関連アクションをトリガー。
  3. ビジネスロジックや複雑な処理は、外部モジュールに委ね、クラス内では状態管理に集中。

利点と欠点:

  • 利点:
    • 状態の管理に集中することで、クラスの責務が明確になり、シンプルでメンテナンスしやすい。
    • 状態変化に応じて自動的にアクションがトリガーされるため、効率的。
    • ビジネスロジックの分離により、再利用性とテストのしやすさが向上。
  • 欠点:
    • 状態管理とビジネスロジックの分離が不十分な場合、クラスが複雑になる可能性。

了解しました。このシナリオでは、PersonServiceを利用してPersonオブジェクトの状態を変更し、その変更をUIに通知するPersonStateHolderクラスを設計します。このクラスでは、FlutterのChangeNotifierを利用してUIへの通知を行います。ChangeNotifierは、状態の変更をリスナーに通知するためのFlutterのメカニズムです。

実例

import 'package:flutter/material.dart';

class PersonStateHolder extends ChangeNotifier {
  final PersonService personService;
  Person? person;

  PersonStateHolder(this.personService);

  Future<void> createAndLoadPerson(String familyName) async {
    person = await personService.createPerson(familyName);
    notifyListeners(); // UIに状態変更を通知
  }

  Future<void> loadPerson(String personId) async {
    person = await personService.loadPerson(personId);
    notifyListeners(); // UIに状態変更を通知
  }

  Future<void> changeFamilyName(String newFamilyName) async {
    if (person == null) return;
    person = await personService.changeFamilyName(_person.id, newFamilyName);
    notifyListeners(); // UIに状態変更を通知
  }
}

説明:

  • PersonStateHolderクラスはChangeNotifierを継承しています。これにより、状態の変更をリスナー(通常はUIウィジェット)に通知できます。
  • createAndLoadPerson, loadPerson, changeFamilyName の各メソッドは非同期処理を行い、PersonServiceを通じてPersonオブジェクトを作成、読み込み、更新します。
  • 状態が変更されるたびにnotifyListeners()を呼び出すことで、UIに状態の変更を通知します。

まとめ

この記事は、単一のクラスにおける多様なデザインパターンの利用方法を3つのカテゴリーに分類し、具体的な例を用いてそれぞれのパターンを明確に説明しています。これらのパターンは、データの構造化(データクラスパターン)、ビジネスロジックの実行(サービスクラスパターン)、状態管理とUI通知(ステートホルダークラスパターン)に焦点を当てており、単一のクラスの柔軟で効果的な利用を可能にします。これにより、ソフトウェア設計における明確なガイドラインと、多様なシナリオに適応するための強力なツールが提供されています。

プロフィール写真

Soraef

ソフトウェアエンジニア。趣味は競馬、写真、ゲーム。

お問い合わせはTwitterのDMでお願いします。