モダンなPerl OOPへようこそ
@nqounetです。
Perl Advent Calendar 2025 の11日目は、MooとMooseを使ったモダンなオブジェクト指向プログラミングについて解説します。
Perlのオブジェクト指向プログラミング(OOP)というと、blessを使った伝統的な手法を思い浮かべる方も多いでしょう。しかし、現代のPerlには、より宣言的で保守性の高いOOPを実現するMooとMooseという強力なツールがあります。
伝統的なPerl OOPの問題点
まず、伝統的なblessベースのOOPを見てみましょう。
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
|
package User;
use strict;
use warnings;
sub new {
my ($class, %args) = @_;
my $self = {
name => $args{name} || die "name is required",
email => $args{email} || die "email is required",
age => $args{age},
};
bless $self, $class;
return $self;
}
sub name { $_[0]->{name} }
sub email { $_[0]->{email} }
sub age { $_[0]->{age} }
sub set_age {
my ($self, $age) = @_;
die "age must be positive" unless $age > 0;
$self->{age} = $age;
}
1;
|
このコードには、いくつかの問題があります:
- ボイラープレートが多い: コンストラクタやアクセサを毎回書く必要がある
- 型チェックが不十分: 手動でバリデーションを書く必要がある
- デフォルト値の管理: 初期化ロジックが散在しやすい
- 継承の扱いづらさ: 親クラスの初期化を忘れやすい
- 可読性: クラスの構造が一目でわからない
Mooの登場 - 軽量でモダンなOOP
Mooは、Mooseの軽量版として登場したモジュールです。Mooseの主要な機能を保ちつつ、依存関係を最小限に抑え、起動時間を短縮しています。
基本的なクラス定義
先ほどのUserクラスをMooで書き直してみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package User;
use Moo;
use Types::Standard qw(Str Int);
has name => (
is => 'ro',
isa => Str,
required => 1,
);
has email => (
is => 'ro',
isa => Str,
required => 1,
);
has age => (
is => 'rw',
isa => Int,
predicate => 'has_age',
);
1;
|
たったこれだけで、同等の機能が実現できます!
Mooの主要機能
属性定義(has)
hasキーワードで属性を宣言します。主なオプション:
is: アクセサのタイプ
ro (read-only): 読み取り専用
rw (read-write): 読み書き可能
lazy: 遅延初期化
isa: 型制約
required: 必須属性
default: デフォルト値
builder: ビルダーメソッド名
predicate: 値が設定されているかチェックするメソッド名
clearer: 値をクリアするメソッド名
coerce: 型強制
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
|
package Config;
use Moo;
use Types::Standard qw(Str Int Bool HashRef);
use Types::Path::Tiny qw(Path);
has config_file => (
is => 'ro',
isa => Path,
coerce => 1,
required => 1,
);
has debug => (
is => 'ro',
isa => Bool,
default => 0,
);
has max_retries => (
is => 'ro',
isa => Int,
default => 3,
);
has settings => (
is => 'lazy',
isa => HashRef,
builder => '_build_settings',
);
sub _build_settings {
my $self = shift;
# 設定ファイルを読み込む処理
return {};
}
1;
|
継承(extends)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package Employee;
use Moo;
extends 'User';
has employee_id => (
is => 'ro',
isa => Str,
required => 1,
);
has department => (
is => 'rw',
isa => Str,
);
1;
|
ロール(with)
ロール(Role)は、複数のクラスで共通する機能をまとめる仕組みです。多重継承の問題を避けつつ、コードの再利用を実現できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package Role::Timestamped;
use Moo::Role;
use Types::Standard qw(InstanceOf);
has created_at => (
is => 'ro',
isa => InstanceOf['DateTime'],
default => sub { DateTime->now },
);
has updated_at => (
is => 'rw',
isa => InstanceOf['DateTime'],
);
sub touch {
my $self = shift;
$self->updated_at(DateTime->now);
}
1;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package Article;
use Moo;
with 'Role::Timestamped';
has title => (
is => 'rw',
isa => Str,
required => 1,
);
has body => (
is => 'rw',
isa => Str,
);
1;
|
型制約(Types::Standard)
Mooでは、Types::Standardモジュールを使って強力な型チェックを行えます。
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
|
package Product;
use Moo;
use Types::Standard qw(Str Num Int ArrayRef HashRef Maybe);
has name => (
is => 'ro',
isa => Str,
required => 1,
);
has price => (
is => 'rw',
isa => Num,
);
has quantity => (
is => 'rw',
isa => Int,
default => 0,
);
has tags => (
is => 'ro',
isa => ArrayRef[Str],
default => sub { [] },
);
has metadata => (
is => 'ro',
isa => HashRef,
default => sub { {} },
);
has discount_rate => (
is => 'rw',
isa => Maybe[Num], # NumまたはundefinedOK
);
1;
|
カスタム型も定義できます:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
use Type::Library -base;
use Type::Utils -all;
use Types::Standard qw(Str);
declare "Email",
as Str,
where { $_ =~ /^[^@]+@[^@]+\.[^@]+$/ },
message { "Invalid email format: $_" };
declare "PositiveInt",
as Int,
where { $_ > 0 },
message { "Must be a positive integer" };
|
メソッド修飾子(Method Modifiers)
Mooでは、メソッドの実行前後に処理を挟み込むメソッド修飾子が使えます。
before - メソッド実行前
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
|
package BankAccount;
use Moo;
has balance => (
is => 'rw',
default => 0,
);
has transaction_log => (
is => 'ro',
default => sub { [] },
);
before withdraw => sub {
my ($self, $amount) = @_;
push @{$self->transaction_log}, {
action => 'withdraw',
amount => $amount,
time => time,
};
};
sub withdraw {
my ($self, $amount) = @_;
die "Insufficient balance" if $self->balance < $amount;
$self->balance($self->balance - $amount);
}
1;
|
after - メソッド実行後
1
2
3
4
5
6
|
after withdraw => sub {
my ($self, $amount) = @_;
if ($self->balance < 1000) {
warn "Warning: Low balance";
}
};
|
around - メソッドを完全にラップ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
around withdraw => sub {
my ($orig, $self, $amount) = @_;
# 引数のバリデーション
die "Amount must be positive" unless $amount > 0;
# 元のメソッドを実行
my $result = $self->$orig($amount);
# 後処理
$self->notify_withdrawal($amount);
return $result;
};
|
MooとMooseの違い
Mooの利点
- 軽量: 依存モジュールが少ない
- 高速起動: Mooseより起動が速い
- メモリ効率: 使用メモリが少ない
- Pure Perl: XSに依存しない(オプションでXS版も使用可)
Mooseの利点
- 高度な機能: より豊富なメタプログラミング機能
- 強力な型システム: より複雑な型制約
- MooseXエコシステム: 多数の拡張モジュール
- イントロスペクション: 実行時のクラス情報取得
使い分けの指針
1
2
3
4
5
6
7
8
9
10
11
|
# Mooを選ぶケース
# - Webアプリケーション(起動速度重視)
# - コマンドラインツール
# - 軽量なライブラリ
# - シンプルなクラス設計
# Mooseを選ぶケース
# - 大規模アプリケーション
# - 複雑なドメインモデル
# - メタプログラミングが必要
# - MooseX::*モジュールが必要
|
MooからMooseへの移行
Mooの良い点は、必要に応じてMooseに移行できることです:
1
2
3
4
5
6
7
8
9
|
package MyClass;
use Moo;
# この1行を追加するだけでMooseに移行
use namespace::autoclean;
# 実行時にMooseが必要になった時だけ自動変換
require Moose;
Moo::Role->_install_tracked('MyClass');
|
実践例:ユーザー管理システム
実際のアプリケーションでの使用例を見てみましょう。
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
|
package UserManager;
use Moo;
use Types::Standard qw(ArrayRef HashRef InstanceOf);
has users => (
is => 'ro',
isa => HashRef[InstanceOf['User']],
default => sub { {} },
);
has search_index => (
is => 'lazy',
isa => ArrayRef,
clearer => 'clear_search_index',
);
sub add_user {
my ($self, $user) = @_;
$self->users->{$user->email} = $user;
$self->clear_search_index; # 検索インデックスを再構築
return $user;
}
sub find_by_email {
my ($self, $email) = @_;
return $self->users->{$email};
}
sub find_by_name {
my ($self, $name) = @_;
return grep { $_->name eq $name } values %{$self->users};
}
sub _build_search_index {
my $self = shift;
return [ sort { $a->name cmp $b->name } values %{$self->users} ];
}
1;
|
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
|
package User;
use Moo;
use Types::Standard qw(Str Int Enum);
with 'Role::Timestamped';
has name => (
is => 'ro',
isa => Str,
required => 1,
);
has email => (
is => 'ro',
isa => Str,
required => 1,
);
has age => (
is => 'rw',
isa => Int,
predicate => 'has_age',
);
has status => (
is => 'rw',
isa => Enum[qw(active inactive suspended)],
default => 'active',
);
sub is_active {
my $self = shift;
return $self->status eq 'active';
}
sub deactivate {
my $self = shift;
$self->status('inactive');
$self->touch;
}
1;
|
使用例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
use User;
use UserManager;
my $manager = UserManager->new;
my $user = User->new(
name => 'Taro Yamada',
email => 'taro@example.com',
age => 30,
);
$manager->add_user($user);
my $found = $manager->find_by_email('taro@example.com');
print $found->name; # => Taro Yamada
$found->deactivate;
print $found->status; # => inactive
|
パフォーマンス比較
Moo、Moose、そして素のPerlでのパフォーマンスを比較してみましょう。
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
|
#!/usr/bin/env perl
use strict;
use warnings;
use Benchmark qw(:all);
# 素のPerl
{
package PlainUser;
sub new {
my ($class, %args) = @_;
bless \%args, $class;
}
sub name { $_[0]{name} }
}
# Moo
{
package MooUser;
use Moo;
has name => (is => 'ro');
}
# Moose
{
package MooseUser;
use Moose;
has name => (is => 'ro');
__PACKAGE__->meta->make_immutable;
}
# ベンチマーク
cmpthese(100000, {
'Plain' => sub {
my $u = PlainUser->new(name => 'test');
$u->name;
},
'Moo' => sub {
my $u = MooUser->new(name => 'test');
$u->name;
},
'Moose' => sub {
my $u = MooseUser->new(name => 'test');
$u->name;
},
});
|
典型的な結果(環境により異なります):
1
2
3
4
|
Rate Moose Moo Plain
Moose 50000/s -- -33% -50%
Moo 75000/s 50% -- -25%
Plain 100000/s 100% 33% --
|
注意: make_immutableを使うとMooseは大幅に高速化されます。また、実際のアプリケーションでは、オブジェクト生成は全体の一部に過ぎないため、この差が問題になることは稀です。
ベストプラクティス
1. イミュータブルを優先する
可能な限りis => 'ro'を使い、不変オブジェクトを作りましょう。
1
2
3
4
|
has name => (
is => 'ro', # read-only
required => 1,
);
|
2. 型制約を活用する
1
2
3
4
5
6
|
use Types::Standard qw(Str Int);
has age => (
is => 'rw',
isa => Int,
);
|
3. ビルダーを使う
デフォルト値が複雑な場合はビルダーメソッドを使いましょう。
1
2
3
4
5
6
7
8
9
|
has config => (
is => 'lazy',
builder => '_build_config',
);
sub _build_config {
my $self = shift;
# 複雑な初期化処理
}
|
4. ロールで機能を分割する
1
2
3
4
5
6
7
8
9
10
11
|
# 良い例
package User;
use Moo;
with 'Role::Timestamped';
with 'Role::Validatable';
# 避けるべき
package User;
use Moo;
extends 'BaseModel'; # 多重継承は避ける
extends 'Timestamped';
|
5. Predicateとclearerを活用する
1
2
3
4
5
6
7
8
9
10
11
|
has cache => (
is => 'lazy',
predicate => 'has_cache',
clearer => 'clear_cache',
);
if ($self->has_cache) {
return $self->cache;
}
$self->clear_cache; # キャッシュをクリア
|
6. コアションは慎重に使う
1
2
3
4
5
6
7
|
use Types::Standard qw(Str Int);
has port => (
is => 'ro',
isa => Int,
coerce => 1, # 文字列を数値に変換
);
|
まとめ
Moo/Mooseを使うことで、Perlでもモダンで保守性の高いオブジェクト指向プログラミングが可能になります。
Mooの主な利点
- 宣言的なクラス定義
- 強力な型システム
- 再利用可能なロール
- メソッド修飾子
- 高いパフォーマンス
選択の指針
- 小〜中規模: Mooがおすすめ(軽量で十分な機能)
- 大規模: Mooseも検討(高度な機能が必要な場合)
- レガシーコード: 段階的にMooに移行可能
次のステップ
明日のPerl Advent Calendar 2025もお楽しみに!