取势 明道 优术

作者为 扶 凯 发表

概要

package Address;
  use Moose;
  use Moose::Util::TypeConstraints;

  use Locale::US;
  use Regexp::Common 'zip';

  my $STATES = Locale::US->new;
  subtype 'USState'
      => as Str
      => where {
             (    exists $STATES->{code2state}{ uc($_) }
               || exists $STATES->{state2code}{ uc($_) } );
         };

  subtype 'USZipCode'
      => as Value
      => where {
             /^$RE{zip}{US}{-extended => 'allow'}$/;
         };

  has 'street'   => ( is => 'rw', isa => 'Str' );
  has 'city'     => ( is => 'rw', isa => 'Str' );
  has 'state'    => ( is => 'rw', isa => 'USState' );
  has 'zip_code' => ( is => 'rw', isa => 'USZipCode' );

  package Company;
  use Moose;
  use Moose::Util::TypeConstraints;

  has 'name' => ( is => 'rw', isa => 'Str', required => 1 );
  has 'address'   => ( is => 'rw', isa => 'Address' );
  has 'employees' => (
      is      => 'rw',
      isa     => 'ArrayRef[Employee]',
      default => sub { [] },
  );

  sub BUILD {
      my ( $self, $params ) = @_;
      foreach my $employee ( @{ $self->employees } ) {
          $employee->employer($self);
      }
  }

  after 'employees' => sub {
      my ( $self, $employees ) = @_;
      return unless $employees;
      foreach my $employee ( @$employees ) {
          $employee->employer($self);
      }
  };

  package Person;
  use Moose;

  has 'first_name' => ( is => 'rw', isa => 'Str', required => 1 );
  has 'last_name'  => ( is => 'rw', isa => 'Str', required => 1 );
  has 'middle_initial' => (
      is        => 'rw', isa => 'Str',
      predicate => 'has_middle_initial'
  );
  has 'address' => ( is => 'rw', isa => 'Address' );

  sub full_name {
      my $self = shift;
      return $self->first_name
          . (
          $self->has_middle_initial
          ? ' ' . $self->middle_initial . '. '
          : ' '
          ) . $self->last_name;
  }

  package Employee;
  use Moose;

  extends 'Person';

  has 'title'    => ( is => 'rw', isa => 'Str',     required => 1 );
  has 'employer' => ( is => 'rw', isa => 'Company', weak_ref => 1 );

  override 'full_name' => sub {
      my $self = shift;
      super() . ', ' . $self->title;
  };

描述

这个指南介绍了 Moose::Util::TypeConstraints 中的 subtype .这次这个 subtype 的功能主要是让我们可以在创建和进入类的时候来自定义一些类型约束.
在这个指南,我们也借助 Locale::US 和 Regexp::Common 二个第三方模块建立一些特别的约束条件.可以展示介绍怎么样使用现有的 CPAN 上的工具来用做约束的功能,进行传送参数的数据验证.

在这个地址类(Address class)中,我们定义了二个 subtypes. 首先使用 Locale::US 模块检查州(state)是否有效,它只接受州(state)的全名缩写.

州(state)将作为字符串来处理,所以要在我们在 Moose 中创建 UUState 的子类型(subtype) 内部使用是 Str 类型 .这是一个语法糖,我们在这个地方实际是在定义约束.这个 where 中的内容,其实是接受一个子函数的引用.这个子函数会检查 $_(1) 的内容,它会通过返回 true 和 false 来表示这个值的属性和内容是否正确;
这样操作后,现在这个新的 USState 的子类型很象 Moose 内置的类型.可以象内置的一样使用.

has 'state' => ( is => 'rw', isa => 'USState' );

当 state 属性设置的时候,就会检查这个的值是否违反了 USState 的类型约束.如果值是有违反类型约束,就不可用的,就会报出一个错来.

 

下一个子类型 USZipCode ,使用的是 Regexp::Common 模块来检查属性 ,Regexp::Common 包含了一些验证的美国(US)邮政编码的正则代码.我们使用这个来约束这个 zip_code 邮政编码的属性.

subtype 'USZipCode'
      => as Value
      => where {
             /^$RE{zip}{US}{-extended => 'allow'}$/;
         };

 使用这种子类型可以大大简化了代码,其实我们并不真的需要在我们的类中有这些类型,只是因为它们是字符串,所以我们才要确保他们是有效的,我们只需要我们所要的字符串.这样保证类更好的封装.
这个类型约束也是可以重用的,类型约束会根据自己的名字注册到全局(global)中来存着.所以我们可以在其它的类中调用这个.因为这个子类型约束的注册是全局的名字空间中.我们建议您实际应用中对使用下面这样的以命名空间来排的名字.象 MyApp::Type::USState(就像你做的类名).

这两个子类型让我们很方便的定义一个简单的 Address 类.

接下来,我们定义我们的 Company 类,类中有 address 的属性, 正如我们在前面指南看到的, Moose 自动动创建每个类名的类型约束,所以我们可以在 Company 类中使用 Address 属性类型约束,所以这个参数只能传一个类进去:

has 'address' => ( is => 'rw', isa => 'Address' );

公司必须有一个名字:

has 'name' => ( is => 'rw', isa => 'Str', required => 1 );

这个是一个新的属性的选项,如果属性中 required 是真,在类构造时,一定需要提供这个属性,不然就会出错.有一个重要的地方需要我们明白,一个 required 的属性传送进来,就象有类型约束,还是有可能仍然是假的或者 undef,类型约束不保证内容,这也是为什么上面为什么使用子类型的原因,因为只能说明它通过了的子类型约束才是允许的.

 

在接下来的类中的一个属性,employees ,使用参数的类型的约束:

has 'employees' => (
      is      => 'rw',
      isa     => 'ArrayRef[Employee]'
      default => sub { [] },
  );

这个约束指出 employees 属性在类生成时一定需要传一个数组的引用,其中每个数组元素都是 Employee 对象.值得一提的是,一个空数组的引用也满足这个约束, 确保这里给出默认的值.
参数类型的约束(或 "容器类型"),例如 ArrayRef[`a],能被用来创建更多的参数类型,事实上,我们可以任意嵌套这些类型,产生类似 HashRef[ArrayRef[Int]].,另外,你也可以只使用自己的类型,所以 ArrayRef 是合法的.(2)

如果你跳到下面来看 Employee 类的定义,你会看到它有一个 employer 属性.
我们来为公司来设置 employees 属性时,我们要确保这些 employee 的对象引用会是正确的公司中 emplayer 的属性.
要做到这一点,我们必须 hook 对象的构造,Moose 可以让我们为我们自己的类来写 BUILD 的方法.当你的类定义了 BUILD 方法时,这将调用对象的构造函数后立即执行,但在此之前的对象就返回给调用者了.请注意,在你的类的层次结构中所有 BUILD 方法将被自动调用,而不需要(和你不应该)调用父类的 BUILD 方法.
这个 Company 类使用 BUILD 方法,用来确保公司中的每个雇员有适合的在 Company 对象中的 emplayer 的属性中.

sub BUILD {
      my ( $self, $params ) = @_;
      foreach my $employee ( @{ $self->employees } ) {
          $employee->employer($self);
      }
  }

类型约束的检查之后,才会执行这个 BUILD 的方法,所以默认是假设 $self->employees 一定有值,这还是比较安全.这个是一个数组的引用,并且该数组元素是引用 Employee 对象.
我们也想确保无论何时 Company 中的 employees 的属性改变,也能更新到各自的 employer 中的 employee 中.
要做到这一点,我们可以使用 after 修饰:

after 'employees' => sub {
      my ( $self, $employees ) = @_;
      return unless $employees;
      foreach my $employee ( @$employees ) {
          $employee->employer($self);
      }
};

这个地方与上面的 BUILD 的方法一样,我们知道,类型约束检查已经检查过了,所以我们知道,如果 $employees 是定义的,它将包含一个 Employee 对象数组的引用.
注意 employees 是一个读/写的访问器,如果是作为一个 reader 调用,我们必须尽早返回.

这个 Person 类并没有真正什么新的东西了,他有几个必须的属性,他也有 predicate 的方法,我们可以看看指南 3.
这个唯一的新特性是 Employee 类中的 override 方法修饰.

override 'full_name' => sub {
      my $self = shift;
      super() . ', ' . $self->title;
};

这仅仅是一个可以选择 Perl 语法糖,用来使用 SUPER:: 的特性,但有一点不同,您不能传递任何参数给 super() , Moose 是简单给相同的参数传递给 super() 的方法.
更多的详细的内容,可以看看 t/recipes/moose_cookbook_basics_recipe4.t.

结论

这个指南特意地写得长些,并更加复杂,用来说明 Moose 类如何使用类型约束,
这个指南也展示了子类型的功能和 required 属性还有 override 的方法修饰
我们在指南中会反复提到类型约束的特性,也会强制转换类型.

注释

(1)被检查的值,也作为了第一个参数传递到块中,所以它可以作为通过 $_[0] 来访问.
(2)注意 ArrayRef[] 是不能工作,Moose 不会解析为一个容器类型,而不是你将有一个新的类型命名为“ArrayRef[]”,其中没有任何意义.

来了就留个评论吧! 2个评论