Переписать атрибуты объекта - лучший способ сделать это с помощью Moose? - PullRequest
4 голосов
/ 24 мая 2011

Посмотрим, сбудется ли предсказание робота SO для ввода вопроса, которое, по-видимому, составлено на основе только названия вопроса:

Вопрос, который вы задаете, кажется субъективным и, вероятно, будет закрыт.

Используя Perl / Moose, я бы хотел устранить несоответствие между двумя способами представления торговых статей. Пусть статья имеет name, quantity и price. Первый способ, которым это представляется, с количеством, установленным на любое числовое значение, включая десятичные значения, так что вы можете иметь 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;

Мне интересно, однако, есть ли лучшая идиома, чтобы сделать это в Музе. Но, возможно, мой вопрос носит субъективный характер и заслуживает быстрого закрытия. : -)

ОБНОВЛЕНИЕ на основании ответа перигрина

Я адаптировал пример кода Перигрина (мелкие ошибки и синтаксис 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;

Я согласен, что это обеспечивает лучшее разделение интересов. С другой стороны, я не уверен, что это лучшее решение для моей цели, так как оно добавляет сложности и не предусматривает автоматического преобразования (для которого мне пришлось бы добавить больше кода).

1 Ответ

4 голосов
/ 25 мая 2011

На основании информации, предоставленной вами в комментариях, вы фактически моделируете две разные , но связанные вещи.Вы столкнулись с уродством, пытаясь сохранить эти две вещи в одном классе.В итоге вы не разделяете свои проблемы должным образом и у вас отвратительная логика рассылки.

Вам нужно иметь два класса с общим 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]: Возможно, вы заметили, что я сделал атрибуты в статье External только для чтения.Исходя из того, что вы сказали, эти объекты должны быть «потреблять только», но если вам нужно атрибутов для записи, вам нужно определить приведение количества, чтобы убедиться, что хранятся только целые числа.Я оставлю это как упражнение читателю.

...