驼鹿属性:分离数据和行为

发布于 2024-12-29 02:18:09 字数 824 浏览 3 评论 0原文

我有一个用 Moose 构建的类,它本质上是文章列表的数据容器。所有属性 - 例如 namenumberpricequantity - 都是数据。 “好吧,还有什么?”,我能听到你说。那么还有什么呢?

现在,不幸情况的邪恶阴谋迫使外部功能进入该包:此类中数据的税收计算必须由外部组件执行。这个外部组件与整个应用程序紧密耦合,包括数据库和依赖项,这些依赖项破坏了组件的可测试性,将其拖入了一切耦合在一起的混乱之中。 (甚至考虑从炖菜中重构税收组件也是完全不可能的。)

所以我的想法是让类接受包装税收计算组件的 coderef。然后,该类将保持独立于税收计算实现(及其可能的依赖噩梦),同时它将允许与应用程序环境集成。

有“tax_calculator”,是 => 'ro', isa =>; '代码引用';

但是然后,我会在我的类中添加一个非数据组件。为什么这是一个问题?因为我(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 是属性列表的一部分。当然,我可以过滤掉它。但我不确定我在这里所做的一切是否是一个合理的方式。那么,您将如何处理这个需要分离数据属性和行为属性的问题呢?

I have a class built with Moose that's essentially a data container for an article list. All the attributes - like name, number, price, quantity - are data. "Well, what else?", I can hear you say. So what else?

An evil conspiration of unfortunate circumstances now forces external functionality into that package: Tax calculation of the data in this class has to be performed by an external component. This external component is tightly coupled to an entire application including database and dependencies that ruin the component's testability, dragging it into the everything-coupled-together stew. (Even thinking about refactoring the tax component out of the stew is completely out of the question.)

So my idea is to have the class accept a coderef wrapping the tax calculation component. The class would then remain independent of the tax calculation implementation (and its possible nightmare of dependencies), and at the same time it would allow integration with the application environment.

has 'tax_calculator', is => 'ro', isa => 'CodeRef';

But then, I'd have added a non-data component to my class. Why is that a problem? Because I'm (ab)using $self->meta->get_attribute_list to assemble a data export for my class:

my %data; # need a plain hash, no objects
my @attrs = $self->meta->get_attribute_list;
$data{ $_ } = $self->$_ for @attrs;
return %data;

Now the coderef is part of the attribute list. I could filter it out, of course. But I'm unsure any of what I'm doing here is a sound way to proceed. So how would you handle this problem, perceived as the need to separate data attributes and behaviour attributes?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

[浮城] 2025-01-05 02:18:09

一个可能的半深思熟虑的解决方案:使用继承。像今天一样创建您的类,但使用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";

A possible half thought out solution: use inheritance. Create your class as you do today but with a calculate_tax method that dies if called (i.e. a virtual function). Then create subclass that overrides that method to call into the external system. You can test the base class and use the child class.

Alternate solution: use a role to add the calculate_tax method. You can create two roles: Calculate::Simple::Tax and Calculate::Real::Tax. When testing you add the simple role, in production you add the real role.

I whipped up this example, but I don't use Moose, so I may be crazy with respect to how to apply the role to the class. There may be some more Moosey way of doing this:

#!/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";

It appears as if the right way to do it in Moose is to use two roles. The first is applied to the class and contains the production code. The second is applied to an object you want to use in testing. It subverts the first method using an around method and never calls the original method:

#!/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";
裸钻 2025-01-05 02:18:09

我想到了一些事情:

  • 在一个单独的 TaxCalculation 类中实现税收计算逻辑,该类将文章列表和税收计算器作为属性。
  • 测试时使用模拟对象作为税收计算器。税收计算器可以存储在默认情况下创建实际税收计算器的属性中。测试传入一个具有相同接口但不执行任何操作的模拟对象。

A couple of things come to mind:

  • Implement the tax calculation logic in a separate TaxCalculation class that has the article list and the tax calculator as attributes.
  • Use a mock object as the tax calculator when you test. The tax calculator could be stored in an attribute that by default creates the real tax calculator. The test passes in a mock object that has the same interface but doesn't do anything.
冰之心 2025-01-05 02:18:09

实际上,这并不是对 get_attribute_list 的滥用,因为这正是 MooseX::Storage 的工作原理[^1]。 如果您要继续使用 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 提出的角色的解决方案,我刚刚在这里回答了他的后续问题:如何处理 Moose 中的模拟角色?

希望这有帮助。

[^1]:由于 MooseX::Storage 最基本的用例就是完全按照您所描述的方式进行操作,因此我强烈建议您查看它以手动执行您正在做的事情。

[^2]:或者简单地重复使用 MooseX::Storage 创建的那个。

Actually that's not really an abuse of get_attribute_list since that's rather exactly how MooseX::Storage works[^1]. IF you are going to continue to use get_attribute_list to build your straight data you'll want to do what MooseX::Storage does and set up an attribute trait for "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__

You then can use this in your class like so:

has 'tax_calculator' => ( is => 'ro', isa => 'CodeRef', traits => ['DoNotSerialize'] );

and in your serialization code like so:

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;

Ultimately though you will end up in a solution similar to the Role one that Chas proposes, and I just answered his follow up question regarding it here: How to handle mocking roles in Moose?.

Hope this helps.

[^1]: And since the most basic use-case for MooseX::Storage is doing exactly what you describe, I highly suggest looking at it to do what you're doing by hand here.

[^2]: Or simply re-use the one from MooseX::Storage creates.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文