Атрибуты Moose: разделение данных и поведения - PullRequest
2 голосов
/ 25 января 2012

У меня есть класс, созданный с помощью Moose, который по сути является контейнером данных для списка статей. Все атрибуты - такие как name, number, price, quantity - являются данными. «Ну, что еще?», Я слышу, как вы говорите. И что еще?

Злой заговор несчастных обстоятельств теперь вынуждает внешнюю функциональность в этот пакет: расчет налога данных в этом классе должен выполняться внешним компонентом. Этот внешний компонент тесно связан со всем приложением, включая базу данных и зависимости, которые разрушают тестируемость компонента, перетаскивая его в рагу из всех соединений. (Даже мысль о рефакторинге налогового компонента из рагу совершенно исключена.)

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

имеет 'tax_calculator', is => '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;

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

Ответы [ 3 ]

7 голосов
/ 25 января 2012

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

Альтернативное решение: используйте роль для добавления метода Calculate_tax. Вы можете создать две роли: Calculate :: Simple :: Tax и Calculate :: Real :: Tax. При тестировании вы добавляете простую роль, в производстве вы добавляете реальную роль.

Я описал этот пример, но я не использую Moose, поэтому я могу быть без ума от того, как применить эту роль к классу. Может быть, есть еще способ Муси сделать это:

#!/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 - использовать две роли. Первый применяется к классу и содержит производственный код. Второй применяется к объекту, который вы хотите использовать в тестировании. Он подрывает первый метод, используя метод round, и никогда не вызывает оригинальный метод:

#!/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 голос
/ 27 января 2012

На самом деле это не совсем злоупотребление 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;

В конечном итоге, в конечном итоге вы получитерешение, похожее на ролевое, которое предлагает Час, и я только что ответил на его следующий вопрос: Как справиться с насмешливыми ролями в Moose? .

Надеюсь, это поможет.

[^ 1]: И поскольку самый простой вариант использования MooseX :: Storage делает именно то, что вы описываете, я настоятельно рекомендую посмотреть на него, чтобы сделать то, что вы делаете здесь.

[^ 2]: или просто повторно использовать тот, который создается из MooseX::Storage.

1 голос
/ 25 января 2012

На ум приходит пара вещей:

  • Реализация логики расчета налога в отдельном классе TaxCalculation, в котором список товаров и калькулятор налогов используются в качестве атрибутов.
  • Используйте фиктивный объект в качестве налогового калькулятора при тестировании.Налоговый калькулятор может храниться в атрибуте, который по умолчанию создает реальный налоговый калькулятор.Тест проходит в фиктивном объекте, который имеет тот же интерфейс, но ничего не делает.
...