Featured image of post 第9回-完成!データエクスポーター - Mooを使ってデータエクスポーターを作ってみよう

第9回-完成!データエクスポーター - Mooを使ってデータエクスポーターを作ってみよう

いよいよ完成!全機能を統合したデータエクスポーターの動作確認と、各形式での出力テストを行います。

@nqounetです。

前回までで、データエクスポーターの主要な機能がすべて揃いました。

今回は、YAML形式も追加して全機能を統合し、完成させましょう!

これまでに作った機能

  1. ExporterRole - エクスポーターが持つべきメソッドの約束
  2. CsvExporter - CSV形式で出力
  3. JsonExporter - JSON形式で出力
  4. DataExporter - エクスポーターを管理し、動的に切り替え可能
  5. exporter_for - 形式名からエクスポーターを自動選択
	classDiagram
    class ExporterRole {
        <<Role>>
        +export(data)* string
    }
    class CsvExporter {
        +export(data) string
    }
    class JsonExporter {
        +export(data) string
    }
    class YamlExporter {
        +export(data) string
    }
    class DataExporter {
        -exporter: ExporterRole
        +exporter_for(format)$ ExporterRole
        +supported_formats()$ list
        +export_data(data) string
    }
    
    DataExporter o-- ExporterRole : has exporter
    ExporterRole <|.. CsvExporter : with
    ExporterRole <|.. JsonExporter : with
    ExporterRole <|.. YamlExporter : with

YamlExporterを追加する

まず、YAML形式で出力するエクスポーターを追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package YamlExporter {
    use Moo;
    use v5.36;
    use YAML::Tiny;
    with 'ExporterRole';

    sub export ($self, $data) {
        my $yaml = YAML::Tiny->new($data);
        return $yaml->write_string;
    }
}

既存のコードを変更する必要はありません。ExporterRoleの約束に従ったクラスを追加し、%exporter_mapにエントリを追加するだけです。

完成したデータエクスポーター

全機能を統合した完成版コードです。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#!/usr/bin/env perl
use v5.36;
use JSON::PP;
use YAML::Tiny;

# ========================================
# ExporterRole - エクスポーターの約束
# ========================================
package ExporterRole {
    use Moo::Role;
    requires 'export';
}

# ========================================
# CsvExporterクラス
# ========================================
package CsvExporter {
    use Moo;
    use v5.36;
    with 'ExporterRole';

    sub export ($self, $data) {
        my $output = "name,email,phone\n";
        for my $contact (@$data) {
            $output .= "$contact->{name},$contact->{email},$contact->{phone}\n";
        }
        return $output;
    }
}

# ========================================
# JsonExporterクラス
# ========================================
package JsonExporter {
    use Moo;
    use v5.36;
    use JSON::PP;
    with 'ExporterRole';

    sub export ($self, $data) {
        return JSON::PP->new->pretty->encode($data);
    }
}

# ========================================
# YamlExporterクラス
# ========================================
package YamlExporter {
    use Moo;
    use v5.36;
    use YAML::Tiny;
    with 'ExporterRole';

    sub export ($self, $data) {
        my $yaml = YAML::Tiny->new($data);
        return $yaml->write_string;
    }
}

# ========================================
# DataExporterクラス(エクスポーター管理)
# ========================================
package DataExporter {
    use Moo;
    use v5.36;

    has exporter => (
        is       => 'rw',
        required => 1,
        does     => 'ExporterRole',
    );

    # 形式名からエクスポーターを取得するためのマッピング
    my %exporter_map = (
        csv  => 'CsvExporter',
        json => 'JsonExporter',
        yaml => 'YamlExporter',
    );

    # 対応形式の一覧を取得
    sub supported_formats ($class) {
        return sort keys %exporter_map;
    }

    # 形式名からエクスポーターを生成
    sub exporter_for ($class, $format) {
        my $exporter_class = $exporter_map{$format};
        die "未対応の形式です: $format\n対応形式: " . join(', ', $class->supported_formats) . "\n"
            unless $exporter_class;
        return $exporter_class->new;
    }

    sub export_data ($self, $data) {
        return $self->exporter->export($data);
    }
}

# ========================================
# メイン処理
# ========================================
package main;

# アドレス帳データ
my @contacts = (
    { name => '田中太郎', email => 'tanaka@example.com', phone => '090-1234-5678' },
    { name => '鈴木花子', email => 'suzuki@example.com', phone => '080-2345-6789' },
    { name => '佐藤次郎', email => 'sato@example.com',   phone => '070-3456-7890' },
);

# コマンドライン引数から形式を取得
my $format = $ARGV[0] // 'csv';

# ヘルプ表示
if ($format eq '--help' || $format eq '-h') {
    say "使い方: perl exporter.pl [形式]";
    say "対応形式: " . join(', ', DataExporter->supported_formats);
    exit 0;
}

# エクスポート実行
my $exporter = DataExporter->exporter_for($format);
my $data_exporter = DataExporter->new(exporter => $exporter);
print $data_exporter->export_data(\@contacts);

動作確認

3つの形式すべてで出力してみましょう。

CSV形式:

1
perl exporter.pl csv
1
2
3
4
name,email,phone
田中太郎,tanaka@example.com,090-1234-5678
鈴木花子,suzuki@example.com,080-2345-6789
佐藤次郎,sato@example.com,070-3456-7890

JSON形式:

1
perl exporter.pl json
1
2
3
4
5
6
7
8
[
   {
      "email" : "tanaka@example.com",
      "name" : "田中太郎",
      "phone" : "090-1234-5678"
   },
   ...
]

YAML形式:

1
perl exporter.pl yaml
1
2
3
4
5
---
- email: tanaka@example.com
  name: 田中太郎
  phone: 090-1234-5678
...

すべての形式で正しく出力されました!

振り返り:第1回との比較

第1回では、if/elseで形式を切り替える素朴な実装でした。

1
2
3
4
5
6
7
# 第1回のコード(抜粋)
if ($format eq 'csv') {
    # CSV出力処理
}
elsif ($format eq 'json') {
    # JSON出力処理
}

今は、形式ごとに専用クラスがあり、追加も簡単です。

1
2
3
4
# 今のコード
my $exporter = DataExporter->exporter_for($format);
my $data_exporter = DataExporter->new(exporter => $exporter);
print $data_exporter->export_data(\@contacts);

この設計の特徴

  1. 拡張が容易 - 新しい形式はクラスを追加してマッピングに登録するだけ
  2. 変更が安全 - 1つの形式の変更が他に影響しない
  3. テストが容易 - 各エクスポーターを個別にテスト可能
  4. 型安全 - does制約で間違ったオブジェクトを防止
  5. 動的切り替え - 実行時にエクスポーターを変更可能
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
完成したシステムの全体像

┌─────────────────────────────────────────────────────────────┐
                        メイン処理                           
  perl exporter.pl [csv|json|yaml]                          
└─────────────────────────────────────────────────────────────┘
                              
                              
┌─────────────────────────────────────────────────────────────┐
                      DataExporter                           
  ┌───────────────────────────────────────────────────────┐  
    exporter_for($format)                                  
    ┌─────────────────────────────────────────────────┐    
      %exporter_map                                      
      csv  => 'CsvExporter'                              
      json => 'JsonExporter'                             
      yaml => 'YamlExporter'                             
    └─────────────────────────────────────────────────┘    
  └───────────────────────────────────────────────────────┘  
                                                             
  has exporter => (does => 'ExporterRole')                   
└─────────────────────────────────────────────────────────────┘
                              
                              
┌─────────────────────────────────────────────────────────────┐
                      ExporterRole                           
                   requires 'export'                         
└─────────────────────────────────────────────────────────────┘
                              
          ┌───────────────────┼───────────────────┐
                                                
    ┌───────────┐       ┌───────────┐       ┌───────────┐
    CsvExporter       JsonExporter      YamlExporter
    export()          export()          export()   
    └───────────┘       └───────────┘       └───────────┘

まとめ

  • YAML形式を追加して、データエクスポーターを完成させました
  • 3つの形式(CSV、JSON、YAML)に対応しています
  • 新しい形式の追加は、クラス作成とマッピング登録だけでOK
  • メイン処理はシンプルで、if/elseがありません

次回「第10回-これがStrategyパターンだ!」は最終回です。実は、ここまで作ってきた設計には名前があります。その名前を明かし、なぜこの設計が良いのかを解説します。お楽しみに!

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。