我有一个用Moose构建的类,它本质上是文章列表的数据容器。所有属性(如name
,number
,price
,quantity
- 都是数据。 “好吧,还有什么?”,我能听到你说。还有什么呢?
不幸情况的邪恶阴谋现在迫使外部功能进入该包:此类中的数据的税收计算必须由外部组件执行。这个外部组件紧密耦合到整个应用程序,包括破坏组件可测试性的数据库和依赖项,将其拖入一切耦合在一起的炖菜。 (即使考虑从炖菜中重构税收成分也是完全不可能的。)
所以我的想法是让类接受包装税计算组件的coderef。然后,该类将保持独立于税收计算实现(及其可能的依赖性噩梦),同时它将允许与应用程序环境集成。
有'tax_calculator',是=> 'ro',isa => 'CODEREF';
但是,我已经在课堂上添加了非数据组件。为什么这是一个问题?因为我(ab)使用$self->meta->get_attribute_list
为我的班级组装数据导出:
my %data; # need a plain hash, no objects
my @attrs = $self->meta->get_attribute_list;
$data{ $_ } = $self->$_ for @attrs;
return %data;
现在coderef是属性列表的一部分。当然,我可以过滤掉它。但我不确定我在这里做的任何事情都是一种合理的方式。那么你如何处理这个问题,被认为是需要分离数据属性和行为属性?
答案 0 :(得分:7)
可能有一半经过深思熟虑的解决方案:使用继承。像今天一样创建你的类,但使用在调用时死亡的calculate_tax方法(即虚函数)。然后创建子类,覆盖该方法以调用外部系统。您可以测试基类并使用子类。
备用解决方案:使用角色添加calculate_tax方法。您可以创建两个角色:Calculate :: Simple :: Tax和Calculate :: Real :: Tax。在测试时添加简单角色,在生产中添加真正的角色。
我掀起了这个例子,但我没有使用Moose,所以我可能会对如何将角色应用到课堂上感到疯狂。 Moosey可能会采取更多的方式:
#!/usr/bin/perl
use warnings;
{
package Simple::Tax;
use Moose::Role;
requires 'price';
sub calculate_tax {
my $self = shift;
return int($self->price * 0.05);
}
}
{
package A;
use Moose;
use Moose::Util qw( apply_all_roles );
has price => ( is => "rw", isa => 'Int' ); #price in pennies
sub new_with_simple_tax {
my $class = shift;
my $obj = $class->new(@_);
apply_all_roles( $obj, "Simple::Tax" );
}
}
my $o = A->new_with_simple_tax(price => 100);
print $o->calculate_tax, " cents\n";
似乎在Moose中使用它的正确方法是使用两个角色。第一个应用于类并包含生产代码。第二个应用于您要在测试中使用的对象。它使用around方法颠覆了第一个方法,并且从不调用原始方法:
#!/usr/bin/perl
use warnings;
{
package Complex::Tax;
use Moose::Role;
requires 'price';
sub calculate_tax {
my $self = shift;
print "complex was called\n";
#pretend this is more complex
return int($self->price * 0.15);
}
}
{
package Simple::Tax;
use Moose::Role;
requires 'price';
around calculate_tax => sub {
my ($orig_method, $self) = @_;
return int($self->price * 0.05);
}
}
{
package A;
use Moose;
has price => ( is => "rw", isa => 'Int' ); #price in pennies
with "Complex::Tax";
}
my $prod = A->new(price => 100);
print $prod->calculate_tax, " cents\n";
use Moose::Util qw/ apply_all_roles /;
my $test = A->new(price => 100);
apply_all_roles($test, 'Simple::Tax');
print $test->calculate_tax, " cents\n";
答案 1 :(得分:1)
有几件事情浮现在脑海中:
TaxCalculation
类中实施税务计算逻辑,该类具有文章列表和税收计算器作为属性。答案 2 :(得分:1)
实际上,这并不是对get_attribute_list
的滥用,因为这与MooseX :: Storage的工作方式完全相同[^ 1]。 IF 您将继续使用get_attribute_list构建您想要执行MooseX :: Storage操作的直接数据,并为“DoNotSerialize”[^ 2]设置属性特征:
package MyApp::Meta::Attribute::Trait::DoNotSerialize;
use Moose::Role;
# register this alias ...
package Moose::Meta::Attribute::Custom::Trait::DoNotSerialize;
sub register_implementation { 'MyApp::Meta::Attribute::Trait::DoNotSerialize' }
1;
__END__
然后你可以在你的班级中使用它:
has 'tax_calculator' => ( is => 'ro', isa => 'CodeRef', traits => ['DoNotSerialize'] );
并在您的序列化代码中如下:
my %data; # need a plain hash, no objects
my @attrs = grep { !$_->does('MyApp::Meta::Attribute::Trait::DoNotSerialize') } $self->meta->get_all_attributes; # note the change from get_attribute_list
$data{ $_ } = $_->get_value($self) for @attrs; # note the inversion here too
return %data;
最终,尽管你最终会得到一个类似于Chas提议的角色的解决方案,我刚刚在这里回答了他的后续问题:How to handle mocking roles in Moose?。
希望这有帮助。
[^ 1]:由于MooseX :: Storage的最基本用例正在完全按照您的描述进行,我强烈建议您在此处手动执行此操作。
[^ 2]:或者只是重复使用MooseX::Storage
创建的那个。