Featured image of post 第7回-does制約でバグを防ごう - Mooを使ってデータエクスポーターを作ってみよう

第7回-does制約でバグを防ごう - Mooを使ってデータエクスポーターを作ってみよう

間違ったオブジェクトを設定してバグが発生…。does制約を使った型チェックで、実行時エラーを未然に防ぐ方法を学びます。

@nqounetです。

前回は、is => 'rw'を使ってエクスポーターを動的に切り替えられるようにしました。

しかし、間違ったオブジェクトを設定できてしまう問題がありましたね。今回は、この問題を解決します。

問題の再確認

現在のコードでは、こんな間違いが起こり得ます。

1
2
3
# 文字列を設定してしまった
$data_exporter->exporter("これはエクスポーターではない");
$data_exporter->export_data(\@contacts);  # エラー!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
問題: 設定時にはエラーにならない

┌────────────────┐      ┌────────────────────────────────┐
  DataExporter        "これはエクスポーターではない" 
  ┌──────────┐                (ただの文字列)           
   ????????         └────────────────────────────────┘
  └──────────┘  
└────────────────┘
        
         export_data() を呼び出すと...
        
    ╔══════════════════════════════════════════════════════════╗
       エラー!(ここで初めて気づく)                       
      Can't locate object method "export"                     ║
    ╚══════════════════════════════════════════════════════════╝

実行すると:

1
Can't locate object method "export" via package "これはエクスポーターではない"

問題は、export_dataメソッドを呼び出したときに初めてエラーになることです。設定した時点でエラーになってくれれば、もっと早く問題に気づけます。

does制約を追加する

Mooのdoesオプションを使うと、「この属性にはExporterRoleを実装したオブジェクトのみ設定できる」という制約を追加できます。

	flowchart TD
    A[オブジェクトを設定] --> B{ExporterRole を\n実装している?}
    B -->|Yes| C[✅ 設定成功]
    B -->|No| D[❌ エラー!\n設定時に即座に検出]
    
    style C fill:#dfd
    style D fill:#fdd
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package DataExporter {
    use Moo;
    use v5.36;

    has exporter => (
        is       => 'rw',
        required => 1,
        does     => 'ExporterRole',  # ExporterRoleを実装していること
    );

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

does => 'ExporterRole'と書くことで、exporter属性にはExporterRoleを実装したオブジェクトしか設定できなくなります。

型チェックが効く様子を確認

間違ったオブジェクトを設定しようとすると、設定した時点でエラーになります。

1
2
# 間違ったオブジェクトを設定しようとする
my $data_exporter = DataExporter->new(exporter => "文字列");

エラーメッセージ:

1
isa check for "exporter" failed: "文字列" doesn't do ExporterRole

これで、「設定時にエラーがわかる」ようになりました!

完成したコード

does制約を追加した完成コードです。

 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
#!/usr/bin/env perl
use v5.36;
use JSON::PP;

# ========================================
# 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);
    }
}

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

    has exporter => (
        is       => 'rw',
        required => 1,
        does     => 'ExporterRole',  # 型チェック追加
    );

    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 $data_exporter = DataExporter->new(exporter => CsvExporter->new);
say "=== CSV形式 ===";
print $data_exporter->export_data(\@contacts);

# JSON形式に切り替え(これもOK)
$data_exporter->exporter(JsonExporter->new);
say "\n=== JSON形式 ===";
print $data_exporter->export_data(\@contacts);

# 以下はエラーになる(コメントアウトして試してみてください)
# $data_exporter->exporter("文字列");  # エラー!

does制約のメリット

1. 早期発見

問題を設定時に発見できるため、デバッグが容易になります。

2. ドキュメント効果

コードを読むだけで「この属性にはExporterRoleを実装したオブジェクトが入る」とわかります。

3. 安全な動的切り替え

is => 'rw'で動的に切り替えても、型チェックが効くので安心です。

今回のポイント

今回学んだdoes制約は、「Mooで覚えるオブジェクト指向プログラミング」シリーズの第12回でも詳しく解説しています。

doesはRoleを実装しているかをチェックします。クラスをチェックしたい場合はisaを使います。

まとめ

  • does制約でRoleを実装しているかチェックできます
  • 間違ったオブジェクトを設定すると、設定時にエラーになります
  • 早期にバグを発見でき、デバッグが容易になります
  • コードのドキュメント効果も向上します

次回「第8回-形式名から自動でエクスポーターを選ぼう」では、形式名(“csv”、“json"など)から自動でエクスポーターを選ぶ機能を追加します。お楽しみに!

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