日向夏特殊応援部隊

俺様向けメモ

Class::C3::Componentised Source Code Reading

DBIC関連の基底クラスとも言えるモジュールがClass::C3::Componentisedです。

C3ベースのコンポーネント化されたクラスモジュールを作る際のベースですね。
1.0003をテキストとします。

load_components()

sub load_components {
  my $class = shift;
  my $base = $class->component_base_class;
  my @comp = map { /^\+(.*)$/ ? $1 : "${base}::$_" } grep { $_ !~ /^#/ } @_;
  $class->_load_components(@comp);
}

component_base_classってのはSynopsisにもありますが、基本的には決めうちの何か。例えばDBIx::Classとかって言う文字列を返す。

load_componentsに渡したリストの各要素で、

  • #をprefixに持つものはスキップ
  • +をprefixに持つならそのまま
  • そうでないならcomponent_base_classのsuffixとして

モジュール名として@compに格納して、_load_components()に渡してる。

_load_components()
sub _load_components {
  my ($class, @comp) = @_;
  foreach my $comp (@comp) {
    $class->ensure_class_loaded($comp);
  }
  $class->inject_base($class => @comp);
  Class::C3::reinitialize();
}

まぁここはそうして抽出したcomponentのリストに対して、

  • ensure_class_loadedして
  • inject_baseをコールして
  • Class::C3::reinitialize()

なんだけど、それぞれ簡単に。
ちなみにClass::C3::reinitialize()はもっかい継承ツリーを総なめする処理だと思うし、読むのがしんどそうなのでそれはパスする。

ensure_class_loaded()
sub ensure_class_loaded {
  my ($class, $f_class) = @_;

  croak "Invalid class name $f_class"
      if ($f_class=~m/(?:\b:\b|\:{3,})/);
  return if Class::Inspector->loaded($f_class);
  my $file = $f_class . '.pm';
  $file =~ s{::}{/}g;
  eval { CORE::require($file) }; # require needs a bareword or filename
  if ($@) {
    if ($class->can('throw_exception')) {
      $class->throw_exception($@);
    } else {
      croak $@;
    }
  }
}

そのモジュールがロードされてれば何もしなくて、そうでなければrequireしてみてコケたらthrow_exceptionメソッドが実装されてればそれを叩いて、そうじゃなきゃcroakするだけですね。

inject_base()
sub inject_base {
  my ($class, $target, @to_inject) = @_;
  {
    no strict 'refs';
    foreach my $to (reverse @to_inject) {
      unshift ( @{"${target}::ISA"}, $to )
        unless ($target eq $to || $target->isa($to));
    }
  }

  # Yes, this is hack. But it *does* work. Please don't submit tickets about
  # it on the basis of the comments in Class::C3, the author was on #dbix-class
  # while I was implementing this.

  eval "package $target; import Class::C3;" unless exists $Class::C3::MRO{$target};
}

特定のクラスに対して複数のコンポーネントをinjectする。

一回reverseしてからその特定のクラスがコンポーネントを継承していないならば、継承順位で先頭にどんどん突っ込んで行く。

つまり、

package Hoge;

use base qw(Class::C3::Componentised);

sub component_base_class { __PACKAGE__ };

__PACKAGE__->load_component(/Foo Bar Baz/);

ならば@ISAは、

@ISA = qw(Hoge::Foo Hoge::Bar Hoge::Baz Class::C3::Componentised);

ってなる。
最後の%Class::C3::MROに特定のクラスが無い場合のevalもパス。

load_own_components()

後はバリエーションですね。

sub load_own_components {
  my $class = shift;
  my @comp = map { "${class}::$_" } grep { $_ !~ /^#/ } @_;
  $class->_load_components(@comp);
}

なんで、

package Jitensya;

use base qw(Hoge); # Hogeはさっきの奴

__PACKAGE__->load_own_components(qw/Foo Bar Baz/);

は、

@ISA = qw(Jitensya::Foo Jitensya::Bar Jitensya::Baz);

になる。

load_optional_components()

sub load_optional_components {
  my $class = shift;
  my $base = $class->component_base_class;
  my @comp = grep { $class->load_optional_class( $_ ) }
             map { /^\+(.*)$/ ? $1 : "${base}::$_" } 
             grep { $_ !~ /^#/ } @_;

  $class->_load_components( @comp ) if scalar @comp;
}

#と+の取り扱いは今までと同じだけど、読み込み方が違う。load_optional_class()を使う。
これって実装が見当たらないのでサブクラスが実装してるかどうかなんでしょうな。多分。

これまったく持ってundocumentだなー。

まとめ

原則としてClass::C3::Componentisedの子孫クラスでは、load_*を読んだ場合は、指定したコンポーネントを自分の親クラスとして持つようになり、さらにClass::C3が使えるようになるんだけども、コンポーネントの略す際のベースが異なる。

  • load_components(@comp)は指定したコンポーネントをcomponent_base_classのsuffixとしたモジュールと見なしてload
  • load_own_components(@comp)は指定したコンポーネントを呼び出したpackage名のsuffixとしたモジュールと見なしてload

一方でload_optional_componentsはほとんどload_componentsと同じ仕組みなんだけど、最後にload_optional_class()でフィルタリング出来るよって代物だと思って良さそう。