重写对象属性 - 使用 Moose 实现这一点的最佳方法?

发布于 2024-11-09 09:08:23 字数 5113 浏览 0 评论 0原文

让我们看看显然仅根据问题标题发布的 SO 问题输入机器人预测是否会实现:

您提出的问题似乎很主观,可能会被关闭。

我想使用 Perl/Moose 来弥补商家文章的两种表示方式之间的不匹配。让一篇文章具有名称数量价格。第一种表示方式是将数量设置为任何数值,包括小数值,因此您可以拥有 3.5 米的绳索或电缆。我必须与之交互的第二个,唉,不灵活,并且要求 quantity 为整数。因此,我必须重写我的对象,将 quantity 设置为 1,并在 name 中包含实际数量。 (是的,这是一种黑客攻击,但我想让示例保持简单。)

因此,这里的故事是一个属性的值会影响其他属性的值。

这是工作代码:

#!perl
package Article;
use Moose;

has name        => is => 'rw', isa => 'Str', required => 1;
has quantity    => is => 'rw', isa => 'Num', required => 1;
has price       => is => 'rw', isa => 'Num', required => 1;

around BUILDARGS => sub {
    my $orig = shift;
    my $class = shift;
    my %args = @_ == 1 ? %{$_[0]} : @_;
    my $q = $args{quantity};
    if ( $q != int $q ) {
        $args{name}    .= " ($q)";
        $args{price}   *= $q;
        $args{quantity} = 1;
    }
    return $class->$orig( %args );
};

sub itemprice { $_[0]->quantity * $_[0]->price }

sub as_string {
    return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_,
    qw/quantity name price itemprice/;
}

package main;
use Test::More;

my $table = Article->new({ name => 'Table', quantity => 1, price => 199 });
is $table->itemprice, 199, $table->as_string;

my $chairs = Article->new( name => 'Chair', quantity => 4, price => 45.50 );
is $chairs->itemprice, 182, $chairs->as_string;

my $rope = Article->new( name => 'Rope', quantity => 3.5, price => 2.80 );
is $rope->itemprice, 9.80, $rope->as_string;
is $rope->quantity, 1, 'quantity set to 1';
is $rope->name, 'Rope (3.5)', 'name includes original quantity';

done_testing;

但是,我想知道是否有更好的习惯用法可以在 Moose 中执行此操作。但也许我的问题都是主观的,值得迅速结束。 :-)

基于 perigrin 的答案更新

我已经改编了 perigrin 的代码示例(小错误和 5.10 语法)并将我的测试标记到它的末尾:

package Article::Interface;
use Moose::Role;
requires qw(name quantity price);

sub itemprice { $_[0]->quantity * $_[0]->price }

sub as_string {
        return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_,
        qw/quantity name price itemprice/;
}


package Article::Types;
use Moose::Util::TypeConstraints;
class_type 'Article::Internal';
class_type 'Article::External';
coerce 'Article::External' =>
  from 'Article::Internal' => via
{
        Article::External->new(
                name        => sprintf( '%s (%s)', $_->name, $_->quantity ),
                quantity    => 1,
                price       => $_->quantity * $_->price
        );
};


package Article::Internal;
use Moose;
use Moose::Util::TypeConstraints;
has name        => isa => 'Str', is => 'rw', required => 1;
has quantity    => isa => 'Num', is => 'rw', required => 1;
has price       => isa => 'Num', is => 'rw', required => 1;

my $constraint = find_type_constraint('Article::External');

=useless for this case
# Moose::Manual::Construction - "You should never call $self->SUPER::BUILD,
# nor"should you ever apply a method modifier to BUILD."
sub BUILD {
        my $self = shift;
        my $q = $self->quantity;
    # BUILD does not return the object to the caller,
    # so it CANNOT BE USED to trigger the coercion.
        return $q == int $q ? $self : $constraint->coerce( $self );
}
=cut

with qw(Article::Interface); # need to put this at the end


package Article::External;
use Moose;
has name        => isa => 'Str', is => 'ro', required => 1;
has quantity    => isa => 'Int', is => 'ro', required => 1;
has price       => isa => 'Num', is => 'ro', required => 1;

sub itemprice { $_[0]->price } # override

with qw(Article::Interface); # need to put this at the end


package main;
use Test::More;

my $table = Article::Internal->new(
        { name => 'Table', quantity => 1, price => 199 });
is $table->itemprice, 199, $table->as_string;
is $table->quantity, 1;
is $table->name, 'Table';

my $chairs = Article::Internal->new(
        name => 'Chair', quantity => 4, price => 45.50 );
is $chairs->itemprice, 182, $chairs->as_string;
is $chairs->quantity, 4;
is $chairs->name, 'Chair';

my $rope = Article::Internal->new(
        name => 'Rope', quantity => 3.5, price => 2.80 );
# I can trigger the conversion manually.
$rope = $constraint->coerce( $rope );
# I'd like the conversion to be automatic, though.
# But I cannot use BUILD for doing that. - XXX
# Looks like I'd have to add a factory method that inspects the
# parameters and does the conversion if needed, and it is always
# needed when the `quantity` isn't an integer.

isa_ok $rope, 'Article::External';
is $rope->itemprice, 9.80, $rope->as_string;
is $rope->quantity, 1, 'quantity set to 1';
is $rope->name, 'Rope (3.5)', 'name includes original quantity';

done_testing;

我同意它提供了更好的关注点分离。另一方面,我不相信这对我的目的来说是更好的解决方案,因为它增加了复杂性并且不提供自动转换(为此我必须添加更多代码)。

Let's see whether the SO question entry robot prediction, apparently issued based on just the question title, will come true:

The question you're asking appears subjective and is likely to be closed.

Using Perl/Moose, I'd like to bridge a mismatch between two ways merchant articles are represented. Let an article have name, quantity and price. The first way this is represented is with quantity set to any numeric value, including decimal values, so you can have 3.5 meters of rope or cable. The second one, which I have to interface with, is, alas, inflexible, and requires quantity to be an integer. Hence I have to rewrite my object to set quantity to 1 and include the actual quantity in the name. (Yes, this is a hack, but I wanted to keep the example simple.)

So the story here is that one property's value affects other properties' values.

Here's working code:

#!perl
package Article;
use Moose;

has name        => is => 'rw', isa => 'Str', required => 1;
has quantity    => is => 'rw', isa => 'Num', required => 1;
has price       => is => 'rw', isa => 'Num', required => 1;

around BUILDARGS => sub {
    my $orig = shift;
    my $class = shift;
    my %args = @_ == 1 ? %{$_[0]} : @_;
    my $q = $args{quantity};
    if ( $q != int $q ) {
        $args{name}    .= " ($q)";
        $args{price}   *= $q;
        $args{quantity} = 1;
    }
    return $class->$orig( %args );
};

sub itemprice { $_[0]->quantity * $_[0]->price }

sub as_string {
    return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_,
    qw/quantity name price itemprice/;
}

package main;
use Test::More;

my $table = Article->new({ name => 'Table', quantity => 1, price => 199 });
is $table->itemprice, 199, $table->as_string;

my $chairs = Article->new( name => 'Chair', quantity => 4, price => 45.50 );
is $chairs->itemprice, 182, $chairs->as_string;

my $rope = Article->new( name => 'Rope', quantity => 3.5, price => 2.80 );
is $rope->itemprice, 9.80, $rope->as_string;
is $rope->quantity, 1, 'quantity set to 1';
is $rope->name, 'Rope (3.5)', 'name includes original quantity';

done_testing;

I'm wondering, however, whether there's a better idiom to do this in Moose. But maybe my question is all subjective and deserves swift closing. :-)

UPDATE based on perigrin's answer

I've adapted perigrin's code sample (minor errors, and 5.10 syntax) and tagged my tests onto the end of it:

package Article::Interface;
use Moose::Role;
requires qw(name quantity price);

sub itemprice { $_[0]->quantity * $_[0]->price }

sub as_string {
        return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_,
        qw/quantity name price itemprice/;
}


package Article::Types;
use Moose::Util::TypeConstraints;
class_type 'Article::Internal';
class_type 'Article::External';
coerce 'Article::External' =>
  from 'Article::Internal' => via
{
        Article::External->new(
                name        => sprintf( '%s (%s)', $_->name, $_->quantity ),
                quantity    => 1,
                price       => $_->quantity * $_->price
        );
};


package Article::Internal;
use Moose;
use Moose::Util::TypeConstraints;
has name        => isa => 'Str', is => 'rw', required => 1;
has quantity    => isa => 'Num', is => 'rw', required => 1;
has price       => isa => 'Num', is => 'rw', required => 1;

my $constraint = find_type_constraint('Article::External');

=useless for this case
# Moose::Manual::Construction - "You should never call $self->SUPER::BUILD,
# nor"should you ever apply a method modifier to BUILD."
sub BUILD {
        my $self = shift;
        my $q = $self->quantity;
    # BUILD does not return the object to the caller,
    # so it CANNOT BE USED to trigger the coercion.
        return $q == int $q ? $self : $constraint->coerce( $self );
}
=cut

with qw(Article::Interface); # need to put this at the end


package Article::External;
use Moose;
has name        => isa => 'Str', is => 'ro', required => 1;
has quantity    => isa => 'Int', is => 'ro', required => 1;
has price       => isa => 'Num', is => 'ro', required => 1;

sub itemprice { $_[0]->price } # override

with qw(Article::Interface); # need to put this at the end


package main;
use Test::More;

my $table = Article::Internal->new(
        { name => 'Table', quantity => 1, price => 199 });
is $table->itemprice, 199, $table->as_string;
is $table->quantity, 1;
is $table->name, 'Table';

my $chairs = Article::Internal->new(
        name => 'Chair', quantity => 4, price => 45.50 );
is $chairs->itemprice, 182, $chairs->as_string;
is $chairs->quantity, 4;
is $chairs->name, 'Chair';

my $rope = Article::Internal->new(
        name => 'Rope', quantity => 3.5, price => 2.80 );
# I can trigger the conversion manually.
$rope = $constraint->coerce( $rope );
# I'd like the conversion to be automatic, though.
# But I cannot use BUILD for doing that. - XXX
# Looks like I'd have to add a factory method that inspects the
# parameters and does the conversion if needed, and it is always
# needed when the `quantity` isn't an integer.

isa_ok $rope, 'Article::External';
is $rope->itemprice, 9.80, $rope->as_string;
is $rope->quantity, 1, 'quantity set to 1';
is $rope->name, 'Rope (3.5)', 'name includes original quantity';

done_testing;

I agree it provides a better separation of concerns. On the other hand, I'm not convinced this is a better solution for my purpose, as it adds complexity and does not provide for an automatic conversion (for which I would have to add more code).

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

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

发布评论

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

评论(1

故乡的云 2024-11-16 09:08:23

根据您在评论中提供的信息,您实际上正在建模两个不同但相关的事物。您已经遇到过试图将这两个东西保留为一个类的丑陋。你最终没有正确地分离你的关注点并且有丑陋的调度逻辑。

您需要有两个具有通用 API 的类(角色将强制执行此操作)和一组强制转换,以便在两者之间轻松进行转换。

首先,API 非常简单。

 package Article::Interface {
        use Moose::Role;

        requires qw(name quantity price);

        sub itemprice { $_[0]->quantity * $_[0]->price }

        sub as_string {
            return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_,
            qw/quantity name price itemprice/;
        }
 }

然后你有一个类来代表你的内部文章,这又是相当微不足道的。

 package Article::Internal {
      use Moose;

      has name => ( isa 'Str', is => 'rw', required => 1);
      has [qw(quantity price)] => ( isa => 'Num', is => 'rw', required => 1); 

      # because of timing issues we need to put this at the end
      with qw(Article::Interface);
 }

最后,您有一个类来代表您的外部文章。在这个例子中,您必须重写接口中的一些方法来处理您的属性将被专门化的事实[^1]。

 package Article::External {
      use Moose;

      has name => ( isa 'Str', is => 'ro', required => 1);
      has quantity => ( isa => 'Int', is => 'ro', required => 1); 
      has price => (isa => 'Num', is => 'ro', required => 1);

      sub itemprice { $_[0]->price }

      # because of timing issues we need to put this at the end
      with qw(Article::Interface);
 }

最后,您定义一个简单的强制例程来在两者之间进行转换。

package Article::Types {
    use Moose::Util::TypeConstraints;
    class_type 'Article::Internal';
    class_type 'Article::External';

    coerce 'Article::Exteral' => from 'Article::Internal' => via {          
         Article::External->new(
            name => $_->name,
            quantity => int $_->quantity,
            price => $_->quantity * $_->price
         );
    }
}

您可以通过以下方式手动触发此强制:

find_type_constraint('Article::External')->coerce($internal_article);

另外,MooseX::Types 可用于最后一部分以提供更清洁的糖,但我在这里选择坚持使用纯 Moose。

[^1]:您可能已经注意到我已将外部文章中的属性设置为只读。根据您的说法,这些对象应该“仅使用”,但如果您需要属性可写,则需要定义数量强制以确保仅存储整数。我将把它作为练习留给读者。

Based on the information you provided in the comments, you're actually modeling two different but related things. You've encountered the ugliness of trying to keep these two things as a single Class. You end up not properly separating your concerns and have ugly dispatch logic.

You need to have two classes with a common API (a Role will enforce this) and a set of coercions to easily translate between the two.

First the API is really straight forward.

 package Article::Interface {
        use Moose::Role;

        requires qw(name quantity price);

        sub itemprice { $_[0]->quantity * $_[0]->price }

        sub as_string {
            return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_,
            qw/quantity name price itemprice/;
        }
 }

Then you have a Class to represent your internal Articles, again this is pretty trivial.

 package Article::Internal {
      use Moose;

      has name => ( isa 'Str', is => 'rw', required => 1);
      has [qw(quantity price)] => ( isa => 'Num', is => 'rw', required => 1); 

      # because of timing issues we need to put this at the end
      with qw(Article::Interface);
 }

Finally you have a class to represent your external articles. In this one you have to override some methods from the interface to deal with the fact that your attributes are going to be specialized[^1].

 package Article::External {
      use Moose;

      has name => ( isa 'Str', is => 'ro', required => 1);
      has quantity => ( isa => 'Int', is => 'ro', required => 1); 
      has price => (isa => 'Num', is => 'ro', required => 1);

      sub itemprice { $_[0]->price }

      # because of timing issues we need to put this at the end
      with qw(Article::Interface);
 }

Finally you define a simple coercion routine to translate between the two.

package Article::Types {
    use Moose::Util::TypeConstraints;
    class_type 'Article::Internal';
    class_type 'Article::External';

    coerce 'Article::Exteral' => from 'Article::Internal' => via {          
         Article::External->new(
            name => $_->name,
            quantity => int $_->quantity,
            price => $_->quantity * $_->price
         );
    }
}

You can trigger this coercion manually with:

find_type_constraint('Article::External')->coerce($internal_article);

Additionally MooseX::Types can be used for this last part to provide cleaner sugar, but I chose to stick with pure Moose here.

[^1]: You may have noticed that I've made the attributes in the External article read-only. From what you've said these objects should be "consume only" but if you need the attributes to be writeable you'll need to define a coercion on quantity to deal with making sure that only Integers are stored. I'll leave that as an exercise to the reader.

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