😇

デザインパターンを久しぶりに学んでみた

2024/01/24に公開

記事の目的

プログラミングをやっていると、デザインパターンなる言葉を聞くようになった。初心者だった頃にシングルトンとか勉強したけど、仕事ではちょっと使っただけでよく分かってない。実はあまり使わないとか???
パッケージを作るときは使うのだとか?

最近設計の本を読んでいるのですが、こちらの本でオニオンアーキテクチャの解説のところで登場したシングルトンAdapterパターンについて解説したいと思います。

これ昔書いた記事です。懐かしい。でも新しく解説する。
https://zenn.dev/joo_hashi/articles/0e7a3fd76b4f4e

シングルトン

シングルトンパターンは、特定のクラスのインスタンスが1つしか存在しないことを保証するデザインパターンです。このパターンは、グローバルな状態を管理したり、リソース共有を制御したりする場合によく使用されます。

メリット:

インスタンスが1つしか存在しないことを保証できます。これにより、一貫性と制御が保たれます。
グローバルなアクセスポイントを提供します。これにより、どこからでもシングルトンオブジェクトにアクセスできます。

デメリット:

グローバル状態を持つと、コードが予測しにくくなり、バグを生じやすくなる可能性があります。
シングルトンが多くのクラス間で共有されると、それらのクラスが密結合になる可能性があります。これは、コードの再利用性とテスト性を低下させます。
マルチスレッド環境では、同時アクセスによる問題を防ぐために追加の同期処理が必要になる場合があります。
シングルトンパターンは、適切に使用されると非常に強力ですが、不適切に使用されるとコードの複雑性と予測不能性を増加させる可能性があります。そのため、必要性とトレードオフを慎重に評価することが重要です。

class Singleton {
  static Singleton? _instance;

  Singleton._privateConstructor();

  static Singleton getInstance() {
    _instance ??= Singleton._privateConstructor();

    return _instance!;
  }
}

// 普通のクラス
class NormalClass {
  final String name;
  NormalClass(this.name);
}

void main() {
  // 同じインスタンスを生成しないクラス
  var instance1 = Singleton.getInstance();
  var instance2 = Singleton.getInstance();
  // どちらも同じインスタンスを参照しているので、trueになる
  print(instance1 == instance2);  // true

  // 普通のクラス
  var normalClass1 = NormalClass('normal1');
  var normalClass2 = NormalClass('normal1');

  // どちらも別のインスタンスを参照しているので、falseになる
  print(normalClass1 == normalClass2);  // false
}

Adapterパターン

Adapterパターンは、デザインパターンの一つで、既存のクラスのインターフェースを他のインターフェースに変換することで、インターフェースが互換性のないクラスが一緒に動作することを可能にします。これは、既存のクラスを修正せずに新しいインターフェースに適応させるためのパターンです。

メリット:

既存のクラスを修正せずに新しいインターフェースに適応させることができます。これにより、既存のコードを再利用しやすくなります。
クラス間の疎結合を促進します。一部のクラスが他のクラスの具体的なインターフェースに依存せず、抽象的なインターフェースにのみ依存することができます。
デメリット:

コードが複雑になる可能性があります。新しいアダプタクラスを作成する必要があるため、システム全体のコード量が増える可能性があります。
パフォーマンス問題が発生する可能性があります。アダプタが間接的な呼び出しを行うため、直接呼び出すよりもパフォーマンスが低下する可能性があります。
Adapterパターンは、既存のクラスを新しいシステムで再利用する必要がある場合や、クラス間の疎結合を保つことが重要な場合に特に有用です。

// インターフェースを実装したクラスをアダプターとして利用する
abstract interface class Target {
  // CSVデータを返すメソッド
  String getCsvData();
}

// JSONデータを返すライブラリ
class JsonLibrary {
  // JSONデータを返すメソッド。List<Map<String, String>>を返す
  List<Map<String, String>> getJsonData() {
    return [
      {
        "1": "apple",
        "2": "orange",
      },
      {
        "1": "太郎",
        "2": "二郎",
      },
    ];
  }
}

/* JSONデータをCSVデータに変換するアダプター
JsonLibraryを継承して、Targetを実装する
*/
class JsonToCsvAdapter extends JsonLibrary implements Target {
  // Targetを実装するために、getCsvDataメソッドを実装する。override(上書きをする)
  
  String getCsvData() {
    // JsonLibraryのgetJsonDataメソッドを呼び出して、JSONデータを取得する
    var jsonData = getJsonData();
    // Listを.keys.join(",")で、キーをカンマ区切りの文字列に変換する
    var header = "${jsonData[0].keys.join(",")}\n";
    // mapメソッドでJSONデータをCSVデータに変換する
    var body = jsonData.map((data) {
      // returnで返す値は、.values.join(",")で、値をカンマ区切りの文字列に変換する
      return data.values.join(",");
    }).join('\n');
    // ヘッダーとボディを結合して、CSVデータを返す
    return header + body;
  }
}

void main() {
  // JsonToCsvAdapterをインスタンス化して、getCsvDataメソッドを呼び出す
  var adapter = JsonToCsvAdapter();
  // CSVデータを出力する
  print(adapter.getCsvData());
}

実行結果:

1,2
apple,orange
太郎,二郎

Exited.

最後に

シングルトンパターンは、演算子で比較しないと同じインスタンを生成していないか確認できませんでした。以前はインスタンス化して、print文でログを出していましたが分かりずらかったですね💦

Adapterパターンは、抽象クラス(インターフェース)を定義して、ロジックを書いたクラスを継承して、インターフェースを継承して、オーバーライドする複雑なロジックになりました。

私が普段こんなことしてるかと言うとしてなくて、抽象クラスを上書きしたり、グローバル変数はriverpodのプロバイダーを使っていますね。

Discussion