Compositeパターンで柔軟なJSONデータ処理：条件付き要素抽出の実装

## はじめに
ソフトウェア開発において、JSONデータの処理は避けて通れない課題です。特に、深くネストされた複雑なJSONデータを扱う場合、コードが煩雑になりがちです。本記事では、Compositeパターンを活用して、複雑なJSONデータから特定の条件に合致する要素を抽出し、新しい構造を生成する柔軟な処理システムを実装します。

## なぜCompositeパターンを使うのか
構造に関するデザインパターン

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/ca63af70-309c-7342-cf1c-132b5032384f.png)

1. **統一的なインターフェース**: JSON要素（オブジェクト、配列、プリミティブ値）を同じインターフェースで扱えます。
2. **再帰的な処理**: ネストされた構造を簡単に走査できます。
3. **拡張性**: 新しい処理ルールや要素タイプの追加が容易です。

## 実装例：条件付きJSONデータ抽出器

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/f6e553ae-2f59-990e-ca5c-ce61c0363e6d.png)


以下の実装では、JSONデータを解析し、特定の条件に合致する要素のみを抽出して新しい構造を生成します。

```mermaid
classDiagram
    %% Compositeパターンを用いたJSONデータ抽出器のクラス図

    %% 抽象基底クラス
    class JSONComponent {
        <<abstract>>
        +process(condition: callable): Any
    }
    %% JSONComponentは全ての要素の基底クラスで、
    %% 共通のインターフェースを定義します。

    %% Leaf（葉）クラス
    class JSONPrimitive {
        -value: Any
        +process(condition: callable): Any
    }
    %% JSONPrimitiveは末端の要素（文字列、数値、真偽値）を
    %% 表現するクラスです。

    %% Composite（複合）クラス
    class JSONObject {
        -data: Dict
        +process(condition: callable): Dict
        -_wrap(item: Any): JSONComponent
    }
    %% JSONObjectは他の要素を含むことができる複合オブジェクトで、
    %% キーと値のペアを持ちます。

    %% Composite（複合）クラス
    class JSONArray {
        -data: List
        +process(condition: callable): List
        -_wrap(item: Any): JSONComponent
    }
    %% JSONArrayも他の要素を含むことができる複合オブジェクトで、
    %% 値のリストを持ちます。

    %% ファサードクラス
    class JSONProcessor {
        +extract(json_data: Dict, condition: callable): Dict
    }
    %% JSONProcessorはクライアントコードからの使用を簡略化する
    %% ファサードとして機能します。

    %% クラス間の関係
    JSONComponent <|-- JSONPrimitive
    JSONComponent <|-- JSONObject
    JSONComponent <|-- JSONArray
    JSONObject *-- JSONComponent : contains
    JSONArray *-- JSONComponent : contains
    JSONProcessor ..> JSONObject : uses

    %% Compositeパターンの説明
    class PatternExplanation {
        Compositeパターンの特徴:
        1. 統一的なインターフェース
        2. 再帰的な構造
        3. 単純オブジェクトと複合オブジェクトの同一視
        
        利点:
        - 複雑な階層構造を簡単に扱える
        - クライアントコードの簡素化
        - 新しい要素タイプの追加が容易
        - 柔軟な構造変更が可能
    }

    %% この図は、Compositeパターンの主要な特徴を示しています：
    %% 1. 共通のインターフェース（JSONComponent）
    %% 2. Leaf（JSONPrimitive）と複合オブジェクト（JSONObject, JSONArray）の統一的な扱い
    %% 3. 再帰的な構造（JSONObjectとJSONArrayが他のJSONComponentを含む）
```

```mermaid
sequenceDiagram
    participant Client
    participant JSONProcessor
    participant JSONObject
    participant JSONArray
    participant JSONPrimitive

    Client->>+JSONProcessor: extract(json_data, condition)
    JSONProcessor->>+JSONObject: new JSONObject(json_data)
    JSONProcessor->>JSONObject: process(condition)
    
    loop 各キー・値ペアに対して
        JSONObject->>JSONObject: _wrap(item)
        alt itemがオブジェクト
            JSONObject->>+JSONObject: new JSONObject(item)
            JSONObject->>JSONObject: process(condition)
        else itemが配列
            JSONObject->>+JSONArray: new JSONArray(item)
            JSONObject->>JSONArray: process(condition)
            loop 各要素に対して
                JSONArray->>JSONArray: _wrap(element)
                alt elementがプリミティブ
                    JSONArray->>+JSONPrimitive: new JSONPrimitive(element)
                    JSONArray->>JSONPrimitive: process(condition)
                    JSONPrimitive-->>-JSONArray: 処理結果
                else elementが複合型
                    JSONArray->>JSONObject: 再帰的に処理
                end
            end
            JSONArray-->>-JSONObject: 処理結果
        else itemがプリミティブ
            JSONObject->>+JSONPrimitive: new JSONPrimitive(item)
            JSONObject->>JSONPrimitive: process(condition)
            JSONPrimitive-->>-JSONObject: 処理結果
        end
    end
    
    JSONObject-->>-JSONProcessor: 処理結果
    JSONProcessor-->>-Client: 抽出されたJSONデータ

    Note over Client,JSONPrimitive: 条件に基づいてフィルタリング<br>されたデータが返却される
```

```python
import json
from abc import ABC, abstractmethod
from typing import Any, Dict, List

class JSONComponent(ABC):
    @abstractmethod
    def process(self, condition: callable) -> Any:
        pass

class JSONPrimitive(JSONComponent):
    def __init__(self, value: Any):
        self.value = value

    def process(self, condition: callable) -> Any:
        return self.value

class JSONObject(JSONComponent):
    def __init__(self, data: Dict):
        self.data = {k: self._wrap(v) for k, v in data.items()}

    def _wrap(self, item: Any) -> JSONComponent:
        if isinstance(item, dict):
            return JSONObject(item)
        elif isinstance(item, list):
            return JSONArray(item)
        else:
            return JSONPrimitive(item)

    def process(self, condition: callable) -> Dict:
        result = {}
        for key, value in self.data.items():
            processed = value.process(condition)
            if isinstance(processed, (dict, list)) or (isinstance(processed, (str, int, float, bool)) and condition(processed)):
                result[key] = processed
        return result if result else None

class JSONArray(JSONComponent):
    def __init__(self, data: List):
        self.data = [self._wrap(item) for item in data]

    def _wrap(self, item: Any) -> JSONComponent:
        if isinstance(item, dict):
            return JSONObject(item)
        elif isinstance(item, list):
            return JSONArray(item)
        else:
            return JSONPrimitive(item)

    def process(self, condition: callable) -> List:
        result = []
        for item in self.data:
            processed = item.process(condition)
            if isinstance(processed, (dict, list)) or (isinstance(processed, (str, int, float, bool)) and condition(processed)):
                result.append(processed)
        return result if result else None

class JSONProcessor:
    @staticmethod
    def extract(json_data: Dict, condition: callable) -> Dict:
        root = JSONObject(json_data)
        return root.process(condition)

# 使用例
if __name__ == "__main__":
    # サンプルJSONデータ
    sample_data = {
        "name": "John Doe",
        "age": 30,
        "contacts": [
            {"type": "email", "value": "john@example.com"},
            {"type": "phone", "value": "1234567890"}
        ],
        "address": {
            "street": "123 Main St",
            "city": "Anytown",
            "country": "USA",
            "zip": "12345"
        },
        "interests": ["programming", "reading", "traveling"]
    }

    # 条件: 文字列で、長さが5以上の要素のみを抽出
    condition = lambda x: isinstance(x, str) and len(x) >= 5

    result = JSONProcessor.extract(sample_data, condition)
    print("文字列で長さが5以上の要素:")
    print(json.dumps(result, indent=2))

    # 数値を含む要素のみを抽出
    numeric_condition = lambda x: isinstance(x, (int, float)) or (isinstance(x, str) and any(char.isdigit() for char in x))
    
    numeric_result = JSONProcessor.extract(sample_data, numeric_condition)
    print("\n数値を含む要素:")
    print(json.dumps(numeric_result, indent=2))
```

## 実装の解説

### JSONComponent
抽象基底クラスで、すべてのJSON要素の共通インターフェースを定義します。

### JSONPrimitive
文字列、数値、真偽値などのプリミティブ値を表します。`process`メソッドは値をそのまま返します。

### JSONObject
キーと値のペアを持つオブジェクトを表します。`process`メソッドは、各子要素に対して再帰的に処理を適用し、条件に合致する要素のみを含む新しいオブジェクトを生成します。

### JSONArray
値のリストを表します。`process`メソッドは、各要素に対して再帰的に処理を適用し、条件に合致する要素のみを含む新しいリストを生成します。

### JSONProcessor
クライアントコードからの使用を簡略化するためのファサードとして機能します。

## 実行結果

この実装を実行すると、以下のような出力が得られます：

1. 文字列で長さが5以上の要素を抽出した結果：

```json
{
  "name": "John Doe",
  "contacts": [
    {
      "value": "john@example.com"
    },
    {
      "value": "1234567890"
    }
  ],
  "address": {
    "street": "123 Main St",
    "city": "Anytown"
  },
  "interests": [
    "programming",
    "reading",
    "traveling"
  ]
}
```

2. 数値を含む要素を抽出した結果：

```json
{
  "age": 30,
  "contacts": [
    {
      "value": "1234567890"
    }
  ],
  "address": {
    "street": "123 Main St",
    "zip": "12345"
  }
}
```

## この実装のメリット

1. **柔軟性**: 異なる条件を簡単に適用できます。
2. **再利用性**: 様々なJSONデータ構造に対して同じ処理を適用できます。
3. **拡張性**: 新しい種類の要素や処理ルールを追加しやすい構造です。
4. **構造の保持**: JSON の階層構造を維持したまま処理できます。

## 使用シーン

- APIレスポンスのフィルタリングと正規化
- 大規模なJSONデータからの特定情報の抽出
- データ変換処理のパイプライン構築
- 複雑なJSONデータの条件付き要素抽出

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/77e3eea7-331d-9f0f-c0f5-2e38e61b1b42.png)

Compositeパターンを活用することで、複雑なJSONデータ処理を柔軟かつ効率的に実装できることがわかりました。この手法は、データの正規化、フィルタリング、変換など、多くの実際のユースケースで応用可能です。

実装を通じて、Compositeパターンがデータ処理においても強力なツールとなることが確認できました。読者の皆さんも、ぜひこの実装を基に、自身のプロジェクトでの活用を検討してみてください。JSONデータ処理の課題に直面した際、このCompositeパターンを用いたアプローチが有効な解決策となるでしょう。