Catalyst Source Code Walking #02
d:id:ZIGOROu:20061006:1160166429からの続き
とりあえず書き終えました。
- Catalyst->import
- Catalyst->setup_home
- MyApp->config
- MyApp->setup
- MyApp->setup_home
- MyApp->setup_log
- MyApp->setup_plugins
- MyApp->setup_dispatcher
- MyApp->setup_engine
- MyApp->setup_flags (あれば)
- MyApp->plugins->setup
- MyApp->setup_components
- MyApp->setup_actions
setupメソッドから呼び出している各種setup_*なメソッドに関してまだ見てないので、
さらに追ってみましょう。
追記
component辺りを少し修正。
MyApp->setup_log
これも結局はCatalystクラスのsetup_logメソッドが実行されます。
sub setup_log { my ( $class, $debug ) = @_; unless ( $class->log ) { $class->log( Catalyst::Log->new ); } my $app_flag = Catalyst::Utils::class2env($class) . '_DEBUG'; if ( ( defined( $ENV{CATALYST_DEBUG} ) || defined( $ENV{$app_flag} ) ) ? ( $ENV{CATALYST_DEBUG} || $ENV{$app_flag} ) : $debug ) { no strict 'refs'; *{"$class\::debug"} = sub { 1 }; $class->log->debug('Debug messages enabled'); } }
Logを取る際にCatalyst::Logクラス以外を使いたい場合はsetupメソッドが走る前に予めlogに設定しておく必要がありますね。
あとlogメソッドはclassdataなんで、Catalystクラスを継承しているクラスなら自由に使えます。
このifの条件部に3項演算子って微妙っすね。意見の分かれる所かも。
とは言え中身が結局同じ処理ならば条件部が多少複雑になるのは止むを得ないのかな。
でこの条件部ですけど
ならばデバッグモードって扱いなんですね。次の記述がちょっとうっとり。
{ no strict 'refs'; *{"$class\::debug"} = sub { 1 }; $class->log->debug('Debug messages enabled'); }
型グロブへの代入時に変数展開があるんですけど、\を入れないと${class::debug}って扱われてしまうので、
あえて::の直前をエスケープするって始めて知りました。
多分僕なら、
{ no strict 'refs'; *{$class . "::debug"} = sub { 1 }; $class->log->debug('Debug messages enabled'); }
って書いてたと思う。
ちょっと脇道それてCatalyst::Logクラスも見てみましょう。と言うのも非常に冒頭の処理がDRYだなと思った次第なので。
package Catalyst::Log; use strict; use base 'Class::Accessor::Fast'; use Data::Dump; our %LEVELS = (); __PACKAGE__->mk_accessors('level'); __PACKAGE__->mk_accessors('body'); __PACKAGE__->mk_accessors('abort'); { my @levels = qw[ debug info warn error fatal ]; for ( my $i = 0 ; $i < @levels ; $i++ ) { my $name = $levels[$i]; my $level = 1 << $i; $LEVELS{$name} = $level; no strict 'refs'; *{$name} = sub { my $self = shift; if ( $self->{level} & $level ) { $self->_log( $name, @_ ); } }; *{"is_$name"} = sub { my $self = shift; return $self->{level} & $level; }; } }
レベル値の設定にビットシフト使うのは嫌がらせでつか?orz...
論理演算が苦手な子*1の為にワンライナーで確かめられるコードがこんなんです。
$ perl -e '$LEVEL = 4; for (my $i = 0; $i < 5; $i ++) { my $level = 1 << $i; print $level . "\n"; if ($LEVEL & $level) { print "debug level is $level\n" } }' 1 2 4 debug level is 4 8 16
でもその後の名前付きなアクセサの定義は定番っちゃ定番だけど、きちんとDRYに仕上げてますね。
MyApp->setup_plugins
ここもCatalyst->setup_pluginsが呼び出されます。ここら辺のソースに関しては本当に秀逸だなーと思います。
{ sub registered_plugins { my $proto = shift; return sort keys %{ $proto->_plugins } unless @_; my $plugin = shift; return 1 if exists $proto->_plugins->{$plugin}; return exists $proto->_plugins->{"Catalyst::Plugin::$plugin"}; } sub _register_plugin { my ( $proto, $plugin, $instant ) = @_; my $class = ref $proto || $proto; unless (Class::Inspector->loaded($plugin)) { require Class::Inspector->filename($plugin); } $proto->_plugins->{$plugin} = 1; unless ($instant) { no strict 'refs'; unshift @{"$class\::ISA"}, $plugin; } return $class; } sub setup_plugins { my ( $class, $plugins ) = @_; $class->_plugins( {} ) unless $class->_plugins; $plugins ||= []; for my $plugin ( reverse @$plugins ) { unless ( $plugin =~ s/\A\+// ) { $plugin = "Catalyst::Plugin::$plugin"; } $class->_register_plugin($plugin); } } }
まずpluginの設定ですけど、先頭文字として+を加えるとpackage名として扱われ、省略するとprefixとしてCatalyst::Plugin::が付きます。従ってそのprojectのみで使うようなPluginに関しては+をつけてsetupして下さい。
そしてちょっと綺麗だなーと思ったのがClass::Inspectorの使い方。
require Class::Inspector->filename($plugin);
これは出てこないなー、勉強になるですよ。
また注目すべきなのは_register_pluginメソッドの呼び出し時に第2引数をtrueにしておくと、pluginが継承されないって点が大きいですね。pluginを作りたいって時には特に意識しないとダメっぽぃ。
registerd_pluginメソッドの実装も素敵…。一粒で3度美味しいっていうなんか古いネタをつい思い起こすかのような書き方っすね。
MyApp->setup_dispatcher
ここもCatalyst->setup_dispatcherが呼び出されます。この処理自体は別に大した事は無いです。
sub setup_dispatcher { my ( $class, $dispatcher ) = @_; if ($dispatcher) { $dispatcher = 'Catalyst::Dispatcher::' . $dispatcher; } if ( $ENV{CATALYST_DISPATCHER} ) { $dispatcher = 'Catalyst::Dispatcher::' . $ENV{CATALYST_DISPATCHER}; } if ( $ENV{ uc($class) . '_DISPATCHER' } ) { $dispatcher = 'Catalyst::Dispatcher::' . $ENV{ uc($class) . '_DISPATCHER' }; } unless ($dispatcher) { $dispatcher = $class->dispatcher_class; } unless (Class::Inspector->loaded($dispatcher)) { require Class::Inspector->filename($dispatcher); } # dispatcher instance $class->dispatcher( $dispatcher->new ); }
dispatcherには冒頭でデフォルト値としてCatalyst::Dispatcherが入ってるんで、余程の拡張をしたいと思わなければ特に必要なさげ。
ちなみにCPANにもCatalyst::Dispatcher::*なクラスは現時点で存在しないので、まだ誰も必要性を感じて無いんでしょうね。*2
Catalystの拡張の一手段でやっても良いと思います。^^;
Catalyst::Dispatcherに関してはまた別のエントリーで詳しく書きます。
MyApp->setup_engine
Catalyst->setup_engineが呼び出されます。で、ここのEngineってのはどういうhttpdで動かすかっていうのを決めているだけですね。デフォルトはCGI動作になってます。
ソース自体は別段不思議な点が無いので割愛。w
MyApp->plugins->setup
前のエントリでも書いたけど、Catalystクラスの中でのsetup中にあるpluginsの初期化処理
# Call plugins setup { no warnings qw/redefine/; local *setup = sub { }; $class->setup; }
の謎がまだ解けて無かった。ん〜、
{ no warnings qw/redefine/; local *setup = sub { my $self = shift; $self->NEXT::setup(@_); }; $class->setup; }
ってなってれば理解出来るんだけどな。ってか内部的にそうなってたりして。
理解出来てる方いらっしゃれば是非教えて下さい。
追記
これってpluginのsetupのときにunshiftで@ISAに突っ込んでるから、
Catalystクラス自体は(恐らく)継承ツリーの最後に来るんで、優先的にpluginsのsetupが叩かれるでOKだと思われる。
なのでid:amachangの指摘通りですね。
MyApp->setup_components
これもCatalyst->setup_componentsが呼び出されます。
sub setup_components { my $class = shift; my @paths = qw( ::Controller ::C ::Model ::M ::View ::V ); my $config = $class->config->{ setup_components }; my $extra = delete $config->{ search_extra } || []; push @paths, @$extra; my $locator = Module::Pluggable::Object->new( search_path => [ map { s/^(?=::)/$class/; $_; } @paths ], %$config ); for my $component ( sort { length $a <=> length $b } $locator->plugins ) { Catalyst::Utils::ensure_class_loaded( $component, { ignore_loaded => 1 } ); my $module = $class->setup_component( $component ); my %modules = ( $component => $module, map { $_ => $class->setup_component( $_ ) } Devel::InnerPackage::list_packages( $component ) ); for my $key ( keys %modules ) { $class->components->{ $key } = $modules{ $key }; } } }
このcomponentsのセットアップに影響を与えるconfigはsetup_components, search_extraってキーの値ですね。
search_extraは配列レファレンスである必要があるみたいです。
ここでの肝はModule::Pluggable::Objectですね。
Module::Pluggable::Objectに渡しているキーsearch_pathの値のmapの使い方を見るに、必ずこのアプリケーションのpackageのsuffix + @pathsの中にある文字列っていうpackage階層じゃないとcomponent扱いされないようですね。
では無くてsearch_extraで指定されたpackageツリーもcomponentの対象となります。
またconfigの設定値はModule::PluggableのOPTIONSを参考にすればOKでしょう。
こうして@paths, search_extraで指定したpackage名を持つpluggableなモジュール一覧が$locater->pluginsで取得する事が出来るって感じです。
Catalyst::Utils::ensure_class_loadedメソッドを見てみましょう。
sub ensure_class_loaded { my $class = shift; my $opts = shift; return if !$opts->{ ignore_loaded } && Class::Inspector->loaded( $class ); # if a symbol entry exists we don't load again # this hack is so we don't overwrite $@ if the load did not generate an error my $error; { local $@; eval "require $class"; $error = $@; } die $error if $error; die "require $class was successful but the package is not defined" unless Class::Inspector->loaded($class); return 1; }
$classって書かれてるとCLASS->method()呼び出しだと思いたくなるけど、普通の関数呼び出しって罠。
これ良くないなぁ。せめて$class_nameとかにすればいいのに。
$optsには{ ignore_loaded => 1 }がセットされてますね。でこれがもし0で尚且つ実際にloadされていたら何もせずに終了。
その後の記述は面白いですね。$@の汚染を回避するって事みたいです。
確かにこうしてrequireで動的にモジュールをロードする際のエラーハンドリングとしては、妥当な処理ですね。
でまたrequireは成功したけどそのpackage名で表現されるクラスが定義されてないとエラーになります。
setup_componentsではensure_class_loadedメソッドはただモジュールのロードをやってるだけに過ぎないですね。
でついに具体的にここのcomponentのsetupを行うsetup_componentメソッドの呼び出しになります。
sub setup_component { my( $class, $component ) = @_; unless ( $component->can( 'COMPONENT' ) ) { return $component; } my $suffix = Catalyst::Utils::class2classsuffix( $component ); my $config = $class->config->{ $suffix } || {}; my $instance = eval { $component->COMPONENT( $class, $config ); }; if ( my $error = $@ ) { chomp $error; Catalyst::Exception->throw( message => qq/Couldn't instantiate component "$component", "$error"/ ); } Catalyst::Exception->throw( message => qq/Couldn't instantiate component "$component", "COMPONENT() didn't return an object-like value"/ ) unless eval { $instance->can( 'can' ) }; return $instance; }
まずComponent足りえる為にCOMPONENTメソッドが定義されてないとダメです。*3これはCatalyst::Componentに定義してあるんで、Catalyst::Componentを継承してあればComponentになると考えて良さそうですね。
またこのComponentのsuffixをキーにしてconfigに代入するみたいです。
でこのsuffixがどのように決定しているかと言えば、Catalyst::Utilsを見ないとダメっすね。
sub class2classsuffix { my $class = shift || ''; my $prefix = class2appclass($class) || ''; $class =~ s/$prefix\:\://; return $class; }
と、これが依存してるのがclass2appclassメソッドなので、
sub class2appclass { my $class = shift || ''; my $appname = ''; if ( $class =~ /^(.*)::([MVC]|Model|View|Controller)?::.*$/ ) { $appname = $1; } return $appname; }
Model, View, Controllerとそれのショートネームってのは省略されて、
その接頭辞部分ってのをappnameって言うみたいですね。
なので例えばMyApp::Model::FooなんてComponentがあったとしたら、
こいつのappnameはMyAppですって事になりますね。
なのでprefixを削除した部分って事だから、Model::Fooってのがcomponentの接頭辞になるって寸法。ただこれはCatalystの重要な概念を示唆してるとも考えられるかな。configのキーがこのようなsuffixって事は、Catalyst::Model::Foo, MyApp::Model::Fooってのは同じconfigを共有する事になる。つまり、これってNEXTによる継承chainに組み込むような設計をしなければならないんじゃないかなと思ったり。*4
だいぶ遠回りになりましたけど、evalの部分で$componentに定義されているCOMPONENTメソッドを使ってinstanceの生成をしているみたいです。従って今度はCatalyst::Componentのソースに飛びます。
sub COMPONENT { my ( $self, $c ) = @_; # Temporary fix, some components does not pass context to constructor my $arguments = ( ref( $_[-1] ) eq 'HASH' ) ? $_[-1] : {}; if ( my $new = $self->NEXT::COMPONENT( $c, $arguments ) ) { return $new; } else { if ( my $new = $self->new( $c, $arguments ) ) { return $new; } else { my $class = ref $self || $self; my $new = $self->merge_config_hashes( $self->config, $arguments ); return bless $new, $class; } } }
MyApp->setup_actions
では早速当該部分を見ると、
sub setup_actions { my $c = shift; $c->dispatcher->setup_actions( $c, @_ ) }
と単なるdelegateになってるんで、Catalyst::Dispatcherのsetup_actionsを見てみましょう。
その前にCatalyst::Dispatcherはインスタンス化されるので、newを見ておきます。
sub new { my $self = shift; my $class = ref($self) || $self; my $obj = $class->SUPER::new(@_); # set the default pre- and and postloads $obj->preload_dispatch_types( \@PRELOAD ); $obj->postload_dispatch_types( \@POSTLOAD ); $obj->action_hash( {} ); $obj->container_hash( {} ); # Create the root node of the tree my $container = Catalyst::ActionContainer->new( { part => '/', actions => {} } ); $obj->tree( Tree::Simple->new( $container, Tree::Simple->ROOT ) ); return $obj; }
Actionの管理に関してはCatalyst::ActionContainerで固定になってますね。
また恐らくTree::Simpleクラスによって実際に起動されるアクションの解決を行ってるんじゃないかと推測。
@PRELOAD, @POSTLOADに関しては冒頭で宣言してあります。
# Preload these action types our @PRELOAD = qw/Index Path Regex/; # Postload these action types our @POSTLOAD = qw/Default/;
いわゆるattributesで良く見るアレですね。
その次に本題のsetup_actionsを見ていきます。このソースも些か長いので、分割していきます。
sub setup_actions { my ( $self, $c ) = @_; $self->dispatch_types( [] ); $self->registered_dispatch_types( {} ); $self->method_action_class('Catalyst::Action'); $self->action_container_class('Catalyst::ActionContainer');
まず冒頭部分はインスタンス変数の初期化。newでClass::Accessor::Fastで定義しているフィールドの初期化が出来る物の、結局固定値を代入してる罠。気になる点は当然、method_action_class, action_container_classが決めうちになってるところ。
つまるところ、これらに何らかの大幅な変更を施したい場合は、setup_actionsが呼び出される前に、dispatcherを別の物にしないとダメってオチになる。しかもどーもDRYなコードには思えないんだけども。
my @classes = $self->_load_dispatch_types( @{ $self->preload_dispatch_types } ); @{ $self->registered_dispatch_types }{@classes} = (1) x @classes;
2行目のHASHスライス代入はoperator「x」が登場してますが、これかなりトリッキーだよな。
$ perl -MData::Dumper -e 'use Data::Dumper; @classes = ("a" .. "e"); %types = (); @types{@classes} = (1) x @classes; print Dumper \%types;' $VAR1 = { 'e' => 1, 'c' => 1, 'a' => 1, 'b' => 1, 'd' => 1 };
こんなサンプルコードで十分理解出来るでしょう。
やりたい事はつまるところ、
- load_dispatch_typesにpreload分をぶち込む
- registered_dispatch_typesにはキーにpreloadしたaction名、値に1を代入してるだけ。
って事ですね。
foreach my $comp ( values %{ $c->components } ) { $comp->register_actions($c) if $comp->can('register_actions'); }
この部分のソースに関しては少し難しいかもしれません。
register_actionsの処理の実態はComponentsの中で尚且つregister_actionsメソッドを持つ物に依存しているようです。
Catalyst::Base->register_actions
実際にはCatalyst::Controllerの親クラスになっているCatalyst::Baseにこのメソッドが存在します。
見てみましょう。
ちと長いのでここも分割します。
sub register_actions { my ( $self, $c ) = @_; my $class = ref $self || $self; my $namespace = $self->action_namespace($c); my %methods; $methods{ $self->can($_) } = $_ for @{ Class::Inspector->methods($class) || [] };
冒頭は慣例的に$protoにして欲しい。。。
そして%methodsは中々面白いキャッシュですね。コードがキーになって値がメソッド名になってますね。
# Advanced inheritance support for plugins and the like my @action_cache; { no strict 'refs'; for my $isa ( @{"$class\::ISA"}, $class ) { push @action_cache, @{ $isa->_action_cache } if $isa->can('_action_cache'); } }
ここもHackポイントですね。
Catalyst::Baseの継承ツリーを遡り、その中で_action_cacheメソッドを持つ物があれば@action_cacheに追加してくれるみたいです。Catalyst::Baseの親クラスの中でそれが存在するのはCatalyst::AttrContainerだけですね。*7
恐らくこういう事でしょう。
- 起動されたactionはCatalyst::AttrContainerの_action_cacheに登録されていく
- 一度、起動されたactionに関してはAttrContainerから引っ張ってくる
じゃないかな。もっと読み進めば確たる物になるんじゃないかな。
foreach my $cache (@action_cache) { my $code = $cache->[0]; my $method = delete $methods{$code}; # avoid dupe registers next unless $method;
なるほど、_action_cacheのデータ構造が[ $code, [@attrs] ]って言う構造だから、
コード自体は->[0]で取得してさっきのコードをキーにした%methodsで、実際のメソッド名を決定してる訳ですね。
my $attrs = $self->_parse_attrs( $c, $method, @{ $cache->[1] } ); if ( $attrs->{Private} && ( keys %$attrs > 1 ) ) { $c->log->debug( 'Bad action definition "' . join( ' ', @{ $cache->[1] } ) . qq/" for "$class->$method"/ ) if $c->debug; next; }
_parse_attrsはそういう事から定義づけられてるattributesを解析。
またこのときPrivate attributeが定義されていて、尚且つ他のattributeも定義している時は、
定義として不適切なんで、怒られるみたいですね。*8
これってPrivateって言うattributeの意味する所って、こうしたattributeハンドラで影響受けるようなコードじゃなくて、
純粋な内部的なPerlネイティブのAPIとして定義しろって事なんじゃないかな。だからこんな制約付いてるんじゃないかと思う。*9
my $reverse = $namespace ? "$namespace/$method" : $method; my $action = $self->create_action( name => $method, code => $code, reverse => $reverse, namespace => $namespace, class => $class, attributes => $attrs, ); $c->dispatcher->register( $c, $action ); } }
そして諸々キャッシュから生成された設定値を元にcreate_actionして、生成されたactionをdispatcherに登録して終了って流れですね。
従ってCatalyst::Base->create_action, Catalyst::Dispatcher->registerと見ていきましょう。
Catalyst::Base->create_action
sub create_action { my $self = shift; my %args = @_; my $class = (exists $args{attributes}{ActionClass} ? $args{attributes}{ActionClass}[0] : $self->_action_class); unless ( Class::Inspector->loaded($class) ) { require Class::Inspector->filename($class); } return $class->new( \%args ); }
これは中々興味深い。
attributesにはActionClassって言う値が入ってるらしい。
あるいは入ってなければCatalyst::Base自身の_action_classからクラス名の解決をしている模様。
さらには元々create_actionに渡した値を丸投げしてinstanceの生成を行ってるって事ですね。
Catalyst::Dispatcher->register
ここも些か長いですね。
sub register { my ( $self, $c, $action ) = @_; my $registered = $self->registered_dispatch_types;
$registerdには@preloadで定義されてた値をキーとしたハッシュリファレンスが返ります。
つまりIndex, Path, Regexですな。
my $priv = 0; foreach my $key ( keys %{ $action->attributes } ) { next if $key eq 'Private'; my $class = "Catalyst::DispatchType::$key"; unless ( $registered->{$class} ) { eval "require $class"; push( @{ $self->dispatch_types }, $class->new ) unless $@; $registered->{$class} = 1; } }
ここはPrivateは空振りしつつ、未解決なDispatchTypeがあれば動的に読み込もうとしてますね。
恐らくカスタムのDispatchTypeもここで解決されるんでしょう。*10
prefixはCatalyst::DispatchTypeで固定です。
# Pass the action to our dispatch types so they can register it if reqd. foreach my $type ( @{ $self->dispatch_types } ) { $type->register( $c, $action ); }
そしてDispatchTypeごとにactionを登録していきます。
my $namespace = $action->namespace; my $name = $action->name; my $container = $self->_find_or_create_action_container($namespace); # Set the method value $container->add_action($action); $self->action_hash->{"$namespace/$name"} = $action; $self->container_hash->{$namespace} = $container; }
んー、ここはTree::Simple周りの実装見ないと何とも言えませんね。次回以降掘り下げたいと思います。
まぁURI階層をツリー化してるんでしょうね。そこにactionを追加して終了って感じですな。
実際に呼び出している_find_or_create_action_containerの中身を見ると、
sub _find_or_create_action_container { my ( $self, $namespace ) = @_; my $tree ||= $self->tree; return $tree->getNodeValue unless $namespace; my @namespace = split '/', $namespace; return $self->_find_or_create_namespace_node( $tree, @namespace ) ->getNodeValue; }
ってなっててTree::Simpleにかなり依存してるんで、詳細は何とも言えないけど、
namespace付きの場合はnamespace_nodeからActionContainerを探してくるような実装になってますね。
なので_find_or_create_namespace_nodeを見てみると。
sub _find_or_create_namespace_node { my ( $self, $parent, $part, @namespace ) = @_; return $parent unless $part; my $child = ( grep { $_->getNodeValue->part eq $part } $parent->getAllChildren )[0]; unless ($child) { my $container = Catalyst::ActionContainer->new($part); $parent->addChild( $child = Tree::Simple->new($container) ); } $self->_find_or_create_namespace_node( $child, @namespace ); }
うぁ、再起処理だorz......
つまるところ渡した@namespaceのtreeが作り終えるまでひたすらTree::Simpleでツリーを作っていくって事ですね。そこまで分かれば十分で、実際にはCatalyst::ActionContainerがその都度のtreeに突っ込まれるのが重要。
$partを伴って生成されてるんで、treeの要素ごとにactionを管理するような構成じゃなかろうか。
ここら辺を次回以降掘り下げて行きたいと思います。
さてCatalyst::Dispatcherのsetup_actionsの続き。
$self->_load_dispatch_types( @{ $self->postload_dispatch_types } ); return unless $c->debug;
紆余曲折を経て最終的にpostloadされるべきactionをロードして終了って事になります。
デバッグフラグが立ってないとここで終わりになります。
まとめ(仮)
とりあえず起動時の大まかな流れってのは、この2回で大体つかめたかなって思いますが、実際の肝となる部分に関してはまだまだありそう。
次回以降もっと核心に迫って行きたいと思います。
*1:自分がまさにそうなんですけども
*2:と思ったら次期散財.comでは専用のdispatcher定義してた。
*3:実際にはCOMPONENTメソッド無くてもcomponentとしてsearchされる可能性があります。
*4:この辺りどうなんすかね…、自由なのか自由じゃないのか。浅学だとどうもトラップに感じちゃうんですけども。
*5:Temporary fixってどーよw
*6:Module::Pluggable::Objectへのオプションでコンストラクタ名をinstantiateオプションに指定しないと多分newとかは呼ばれないと思う。(未確認))) でinstance化の手法としてはCOMPONENTメソッドのNEXT Chainで生成出来ればそれを返して、 またnewでinstance化出来ればそれを返す。それでもダメならconfigの値をHASHリファレンスとしてblessしてやる。 そんな実装になってる模様。 従ってController, Model, View, Pluginsなんかはここでinstance化されます。 但し、悲しい事にpackage名の接頭辞に制約を受けてしまうことが判明しました。orz...((とは言えなんか解決策があるとは思うけど
*7:他にもあれば追加出来る仕組みみたいだけど、具体的な拡張方法って思いつかない...
*8:Private, Publicとかって組み合わせはあり得んって事ですな
*9:例えばもっと便利なattributeを作ったとしてもPrivateとの組み合わせじゃ併用出来ないって事ですし。
*10:先のpreloadな処理をどうにかすれば別