北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

Moose::Roleのメソッドの競合

再びTraits: Composable Units of Behaviorのネタです。

Moose::Roleを使った場合、メソッドの優先順位は 自クラス → Role → スーパークラス ... となりますが、ここで Role だけは複数持てるため、競合がありえます。


以下のTaxとTotalの二つのRoleは、どちらもtotal_priceを持っています。

# 価格から税金計算
package Tax;
use Moose::Role

requires 'price';

sub tax {
	my $self = shift;
	return $self->price * 0.05;
}

sub total_price {
	my $self = shift;
	return $self->price + $self->tax;
}

no  Moose::Role;


# 単価から合計計算
package Total;
use Moose::Role

requires 'price', 'number';

sub total_price {
	my $self = shift;
	return $self->price * $self->number;
}

no  Moose::Role;

この二つのRoleを合成して、Reciptクラスを作るとどうなるでしょう? *1

BEGIN {
	package Recipt;
	use Moose;

	with 'Total', 'Tax';

	has 'price' => (
		isa      => 'Int',
		is       => 'ro',
		required => 1,
	);

	has 'number' => (
		isa      => 'Int',
		is       => 'ro',
		required => 1,
	);

	no  Moose;
}
'Total|Tax' requires the method 'total_price' to be implemented by 'Recipt' at XXXX.pm line YY

怒られました。ここでは以下の2点のことに注目して下さい。

  • 「メソッドが競合している」ではなく、「クラス側にメソッドを実装しなさい」と怒られる
    • しかもtotal_priceはrequiresしてるわけじゃないのでちょっとわかりにくい
  • requiresでpriceが重複してるが怒られない(これはTraitsでは当然の仕様)

この競合の解決法として、aliasとexcludesが用意されてます。Reciptのwithを以下のように書き換えます。

	with 
		'Total',
		'Tax' => {
			alias    => {total_price => 'tax_total'},
			excludes => ['total_price'],
		};

これでエラーが出なくなりました。*2

または、言われるが侭にクラス側にtotal_priceメソッドを定義してもエラーは止まります。これは最初に書いたように、クラスのメソッドはRoleのメソッドより優先されるため、呼ぶべきメソッドが一意に定まるようになるためです。

詳しいことは、Moose::Cookbook::Roles::Recipe2や t/030_roles/005_role_conflict_detection.t とか見れば出ています。

*1:この例では with と has の評価タイミングの問題で、BEGINブロックによってcompileフェーズで実行させないと price と number もエラーになります。

*2:けど、これだとtotal_price 呼んでも税込みの合計が出せないので、例としてかなり駄目でした。。。