API Design Shawn M Moore Best Practical Solutions
http://sartak.org
Thursday, September 10, 2009 Presented YAPC::Asia, 2009-09-10. Tokyo Institute of Technology, Tokyo, Japan. GoogleのテックトークでもAPI設計の話が出てました
Thursday, September 10, 2009 There is a good Google Tech Talk on "how to design an API and why it matters". There isn't a whole lot of overlap between this talk and that one. Watch that one.
http://www.youtube.com/watch?v=aAb7hSCtvGw CC-BY-SA Yuval Kogman, 2006
Thursday, September 10, 2009 At YAPC::NA 2009, this guy, Hans Dieter Pearcey aka confound aka hdp, presented a talk about Dist::Zilla.
http://www.flickr.com/photos/nufn/179250512/ CC-BY-SA Yuval Kogman, 2006
Thursday, September 10, 2009 Dist::Zilla was written by this other guy, Ricardo Signes, aka rjbs.
http://www.flickr.com/photos/nufn/179250812/ CC-BY-SA Hans Dieter Pearcey, 2009
Thursday, September 10, 2009 Dieter presented this slide about Dist::Zilla's pluggable design. I loved it and I wanted to devote an entire talk to its glory.
http://weftsoar.net/~hdp/dzil/ Moose Path::Dispatcher HTTP::Engine Dist::Zilla IM::Engine
今日はこれらのプロジェクトが持つクールな APIの話をします Thursday, September 10, 2009 I'm here to highlight really cool API designs that these projects have. In particular, they design for extensibility and pluggability. Extensibility is really important to the current and future success of these projects. CC-BY-SA-NC Will Spaetzel, 2005
Thursday, September 10, 2009 If you haven't noticed yet, this talk is going to be very Moose-heavy. All those modules have the Moose nature.
http://www.flickr.com/photos/redune/6562798/ THE SECRET
秘訣、といってもみなさんご存じだと思いますが
Thursday, September 10, 2009 There is a poorly kept secret for designing great APIs. I hope that all of you already do this, but you probably do not do it enough. THE SECRET WRITE
大事なのはテストを書く、ということTESTS
Thursday, September 10, 2009 Write tests. WRITE TESTS WRITEWRITE TESTS TESTS WRITEWRITE TESTS TESTS 血管切れるまでテストを書いてください
Thursday, September 10, 2009 Write so many tests your ears bleed. I am not joking!
DO THIS
WRITE WRITE
WRITE WRITE WRITE WRITE WRITE WRITEWRITE WRITE WRITE WRITE WRITE WRITE
なにはなくとも、とにかくテスト
Thursday, September 10, 2009 If you remember nothing else, remember to write tests! Test!
use base 'Class::Accessor::Fast'; __PACKAGE__->mk_ro_accessors('birthday');
use Moose; has birthday => (is => 'ro');
テストを書けば使いやすいAPIかどうかがわかります
Thursday, September 10, 2009 Write tests so you can tell if your API is painful to use. Which of these would you rather be stuck with?
Make it painless for your users. Some of them might be using your module a lot. If it's tedious to use your module... Test!
使いづらいモジュールは使ってもらえませんから
Thursday, September 10, 2009 ... then you'll piss your users of. They'll leave and use some other module, or worse, find out where you live. Test!
Thursday, September 10, 2009 This is Jesse Vincent, the nicest guy in the world :) Test!
Thursday, September 10, 2009 Test!
Thursday, September 10, 2009 Moose
package Point; use Moose;
has x => ( is => 'ro', isa => 'Num', );
この先はMooseベースの話なので少し解説
Thursday, September 10, 2009 Moose serves as the foundation for the rest of the talk, so I want to explain what it "got right" in terms of its API. These next few slides are difcult but it will get clearer and less heady, so wake up soon if you space out. Metaobject Protocol
Class::MOP
MooseはClass::MOPのラッパです
Thursday, September 10, 2009 Moose is built on top of a metaobject protocol. This is Class::MOP.
See my "Extending Moose for Applications" talk for a proper introduction to the metaobject protocol http://sartak.org/talks/yapc-na-2009/extending-moose/ Metaobject Protocol
has cache => ( is => 'ro', );
要するにクラスの各パーツはすべてオブジェクトです
Thursday, September 10, 2009 The MOP is vital to Moose's operation. Basically, it means that every part of your class is represented by an object. Metaobject Protocol
has cache => ( is => 'ro', );
Moose::Meta::Attribute
hasはMoose::Meta::Attributeのインスタンスを作ります
Thursday, September 10, 2009 When you say "has" it creates an instance of the Moose::Meta::Attribute class, which holds information like the attribute's name, its type constraint, default value, etc. Metaobject Protocol
has cache => ( is => 'ro', );
Moose::Meta::Method::Accessor これでcacheというアクセサメソッドが作られます
Thursday, September 10, 2009 The is => 'ro' option creates a "cache" method in your class. It also creates an object of class Moose::Meta::Method::Accessor to represent that "cache" method. Metaobject Protocol
class PersistentAttr extends Moose::Meta::Attribute { … }
has cache => ( metaclass => 'PersistentAttr', is => 'ro', ); Mooseのクラスを拡張すれば独自機能も追加できます
Thursday, September 10, 2009 This is important because we can subclass Moose's class to add our own special logic, such as making the cache persist across processes. Subclassing and adding logic is ordinary object- oriented programming! Metaobject Protocol
role PersistentAttr { … }
has cache => ( traits => ['PersistentAttr'], is => 'ro', ); アトリビュートオブジェクトにロールを組み込むこと もできます Thursday, September 10, 2009 We can also specify roles to apply to cache's attribute object. This is slightly better because it means a single attribute can have many extensions. Just like how it's better to design with roles than subclasses in ordinary programming. MooseX
ほとんどのMooseXはMOPを利用しています
Thursday, September 10, 2009 The metaobject protocol powers most of the MooseX modules. In my opinion, the metaobject protocol is responsible for a very large part of Moose's popularity. The other reason for Moose's popularity is it enables concise class code. Sugar Layer
Mooseはシュガー層がきれいに分離しているのも特徴 です Thursday, September 10, 2009 Moose also makes a very clean separation between its sugar layer and the rest of the system. Sugar Layer
my $class = get_class();
あるクラスを利用したいとします
Thursday, September 10, 2009 Say you wanted to get ahold of some class... Sugar Layer
my $class = get_class();
$class->has( birthday => ( is => 'ro', ) );
hasはメソッドではないのでこれではだめです
Thursday, September 10, 2009 Then add an attribute to it. This doesn't work because "has" is not a method. Its first parameter is supposed to be the attribute name, not the class you're adding the attribute to. Sugar Layer
my $class = get_class();
no strict 'refs'; *{$class.'::has'}->( birthday => ( is => 'ro', ) );
だからといってhasを 関数として呼ぶのはばかげています Thursday, September 10, 2009 So we have to call $class's "has" as a function. This kind of thing is ridiculous. Maybe the other class has used "no Moose" so that "has" is deleted. Or perhaps it renamed "has". Sugar Layer
my $class = get_class();
no strict 'refs'; *{$class.'::has'}->( birthday => ( is => 'ro', ) );
それに見づらいですよね
Thursday, September 10, 2009 Not to mention how ugly this mess is. Sugar Layer
Class::MOP::Class ->initialize($class) ->add_attribute( $name, %options);
hasは基本的にはadd_attributeのラッパです
Thursday, September 10, 2009 If we look at the source code of Moose, we can see "has" is basically a wrapper around the "add_attribute" method of the Class::MOP::Class instance. Sugar Layer
my $class = get_class();
$class->meta->add_attribute( birthday => ( is => 'ro', ) );
これでずいぶんマシになりました
Thursday, September 10, 2009 Much better. There's no messy syntax. This can be used outside of $class's namespace just fine. This also works if class has cleaned up after Moose with "no Moose" or namespace::clean. Sugar Layer use MooseX::Declare;
class Point3D extends Point { has z => (…);
after clear { $self->z(0); } }
シュガー層が分離しているので修正も楽です
Thursday, September 10, 2009 Having a clean sugar layer means that other people can write better sugar. I like the idea of providing a separate Devel::Declare-powered sugar layer in a separate distribution. It forces you to cleanly separate the pieces. Path::Dispatcher
use Path::Dispatcher::Declarative -base;
on ['wield', qr/^\w+$/] => sub { wield_weapon($2); }
under display => sub { on inventory => sub { show_inventory }; on score => sub { show_score }; };
YourDispatcher->run('display score');
これはJifty::DispatcherをProphetで 使うために書きました Thursday, September 10, 2009 Path::Dispatcher is a standalone-URI dispatcher. I wrote it because I wanted Jifty::Dispatcher for Prophet's command-line interface.
This is its sugar layer. Like Moose, it has a clean, extensible API if you want the freedom to do unusual things. Path::Dispatcher::Declarative
use Sub::Exporter -setup => { exports => [ on => \&build_on, under => \&build_under, …, ], };
もともとはSub::Exporterを使っていました
Thursday, September 10, 2009 It used to be that Path::Dispatcher::Declarative was implemented as an ordinary Sub::Exporter-using module. Path::Dispatcher::Declarative
use Sub::Exporter -setup => { exports => [ on => \&build_on, under => \&build_under, …, ], };
でもこれではまったく拡張性がありません
Thursday, September 10, 2009 This is not at all extensible. You can't change the meaning of "on" or "under" because these are hardcoded. Reusing this sugar would be painful as well. Path::Dispatcher::Builder Robert Krimen "grink"
Robert Krimenが拡張したがったので
Thursday, September 10, 2009 This was fine for a few weeks, but then Robert Krimen started using Path::Dispatcher. And he wanted to extend it for a module he was writing called Getopt::Chain. Path::Dispatcher::Builder
return { on => sub { $builder->on(@_) }, under => sub { $builder->under(@_) }, …, };
サブクラス化してロジック変更できるようにしました
Thursday, September 10, 2009 Path::Dispatcher::Builder makes the sugar layer creation use OOP. This let Robert subclass Path::Dispatcher::Builder and use it for his own modules. He can reuse the regular dispatcher logic, tweak it by overriding methods, and add his own behavior. grink++
OOのシュガーは本当にいいですよ
Thursday, September 10, 2009 OO sugar is a really neat idea that I haven't seen anywhere else. HTTP::Engine HTTP::Engine->new( interface => { module => 'ServerSimple', args => { … }, request_handler => sub { … }, }, )->run;
HTTP::Engineを使うと好きなサーバを選べます
Thursday, September 10, 2009 HTTP::Engine abstracts away the various HTTP server interfaces that Perl has accumulated since HTTP was invented. The benefit is in letting the user pick which server interface best fits their particular needs. HTTP::Engine HTTP::Engine->new( interface => { module => 'ModPerl', args => { … }, request_handler => sub { … }, }, )->run;
マゾな人ならmod_perlを使えばいいですし
Thursday, September 10, 2009 For example, you can use mod_perl if you enjoy pain. HTTP::Engine HTTP::Engine->new( interface => { module => 'FCGI', args => { … }, request_handler => sub { … }, }, )->run;
クールな人はFastCGIを使います
Thursday, September 10, 2009 Or FastCGI if you're a cool dude. HTTP::Engine
request_handler => sub { my $request = shift; return $response; }
HTTP::Engineを使えばIOのリダイレクトなどは不要です
Thursday, September 10, 2009 HTTP::Engine works well because the code you write doesn't have to worry about redirecting I/O streams, making sense of %ENV, or any of the other crap you do when writing against a particular server module. HTTP::Engine
request_handler => sub { my $request = shift; return $response; }
HTTP::Engineなら最低限必要なことを書けばOK
Thursday, September 10, 2009 HTTP::Engine boils the web server cycle to the least common denominator. You take a request... HTTP::Engine
request_handler => sub { my $request = shift; return $response; }
リクエストを受け取ってレスポンスを返す
Thursday, September 10, 2009 … and return a response. HTTP::Engine++
新しいサーバが追加されても1行変えれば対応可能
Thursday, September 10, 2009 Can we please standardize on this? New server modules can implement an HTTP::Engine::Interface, then immediately every existing HTTP::Engine-based application can switch to it by changing only a single line of code. CC-BY-SA Hans Dieter Pearcey, 2009
Thursday, September 10, 2009 Now I want to explain why this is so awesome. Dist::Zilla
AllFiles ExtraTests InstallDirs License MakeMaker Manifest ManifestSkip MetaYAML PkgVersion PodTests PodVersion PruneCruft Readme UploadToCPAN これはよく使われるプラグインのリストです
Thursday, September 10, 2009 Here's a list of plugins used by a typical Dist::Zilla-based distribution. Dist::Zilla
AllFiles ExtraTests InstallDirs License $_->gather_files MakeMaker for Manifest $self->plugins_with( ManifestSkip -FileGatherer MetaYAML PkgVersion ); PodTests PodVersion PruneCruft Readme UploadToCPAN Dist::Zillaではよくこんなメソッド呼び出しをします
Thursday, September 10, 2009 Dist::Zilla itself occasionally calls methods like this. The key bit is "plugins_with". Dist::Zilla
AllFiles ExtraTests InstallDirs License $_->gather_files MakeMaker for Manifest $self->plugins_with( ManifestSkip -FileGatherer MetaYAML PkgVersion ); PodTests PodVersion PruneCruft Readme UploadToCPAN plugins_withに役割名を渡すと
Thursday, September 10, 2009 plugins_with takes a role name... Dist::Zilla
AllFiles ExtraTests InstallDirs License $_->gather_files MakeMaker for Manifest $self->plugins_with( ManifestSkip -FileGatherer MetaYAML PkgVersion ); PodTests PodVersion PruneCruft Readme UploadToCPAN その役割を持つ各種プラグインが選ばれ
Thursday, September 10, 2009 ...and selects the plugins that "do" the role. These plugins all do the "FileGatherer" role, which means the plugin adds files to a distribution. Dist::Zilla
AllFiles ExtraTests InstallDirs License $_->gather_files MakeMaker for Manifest $self->plugins_with( ManifestSkip -FileGatherer MetaYAML PkgVersion ); PodTests PodVersion PruneCruft Readme UploadToCPANgather_filesで各種プラグインが 追加したファイルをまとめます Thursday, September 10, 2009 Then, dzil calls gather_files on each of these plugins so it can actually add files to the distribution. "License", "Readme", and "MetaYAML" add the respective files, "AllFiles" adds every file the author wrote. "PodTests" adds pod testing files to the distribution. Dist::Zilla
AllFiles ExtraTests InstallDirs License $_->munge_files MakeMaker for Manifest $self->plugins_with( ManifestSkip -FileMunger MetaYAML PkgVersion ); PodTests PodVersion PruneCruft Readme UploadToCPAN Dist::Zillaではこの手法をあちこちで利用しています
Thursday, September 10, 2009 Dist::Zilla uses this architecture for all of the interesting parts of building a CPAN distribution. This is "munging files", which lets plugins edit files to increase the version number, or move tests around. Request Tracker
User/Prefs.html
$m->callback( CallbackName => 'FormEnd', UserObj => $UserObj, …, );
RTにも同じようなメカニズムがあります
Thursday, September 10, 2009 It turns out that RT has a very similar extension mechanism. Request Tracker
User/Prefs.html
$m->callback( CallbackName => 'FormEnd', UserObj => $UserObj, …, );
コールバックで関係するプラグインを探して
Thursday, September 10, 2009 This code exists in User/Prefs.html. The callback method selects all plugins that do the "User/ Prefs.html" "role". Request Tracker
User/Prefs.html
$m->callback( CallbackName => 'FormEnd', UserObj => $UserObj, …, );
それぞれのプラグインでFormEndというメソッドを実行
Thursday, September 10, 2009 Then it calls the FormEnd "method" (template) on these selected plugins. Request Tracker
User/Prefs.html
$m->callback( CallbackName => 'FormEnd', UserObj => $UserObj, …, );
メソッドには任意のパラメータを渡せます
Thursday, September 10, 2009 And you can pass arbitrary parameters to each method. Request Tracker
User/Prefs.html
$m->callback( CallbackName => 'FormEnd', UserObj => $UserObj, …, );
独自拡張はほとんどこのコールバックで 構築しています Thursday, September 10, 2009 This works extremely well for us! We try to build most customer extensions with callbacks. It's basically the same design as Dist::Zilla's. Request Tracker
commit 4c05a6835eef112701ac58dfd1b133e220059d4f Author: Jesse Vincent
Date: Fri Dec 27 18:50:06 2002 -0500
Attempting mason callouts
Ticket/Update.html <& /Elements/Callback, Name => 'BeforeTextarea', %ARGS &> この仕組みはもう7年近く使っています
Thursday, September 10, 2009 RT has had callbacks since 2002, first released in 3.0.0. This pattern has been the best mechanism for any kind of RT extension for almost seven years now. Dist::Zilla Choice
ModuleBuild or MakeMaker
MetaYAML or MetaJSON
こうしておくとユーザが自由に機能を選択できます
Thursday, September 10, 2009 This design gives the user choice over which behavior she wants. And in my experience, users really really want choice. Dist::Zilla Extensibility
Dist::Zilla::Plugin::CriticTests
Dist::Zilla::Plugin::Repository
Dist::Zilla::Plugin::PerlTidy 拡張も自由です
Thursday, September 10, 2009 This design is also extensible for free. These are some of the modules that have been written to extend Dist::Zilla. Dist::Zilla Extensibility
Dist::Zilla::Plugin::CriticTests InlineFiles Dist::Zilla::Plugin::Repository MetaProvider Dist::Zilla::Plugin::PerlTidy FileMunger 「ロール」に必要な条件さえ満たせばいいのですから
Thursday, September 10, 2009 All they need to do is fulfill the requirements of the roles they "do". I'm going to talk about that more in my (Parameterized) Roles talk.
http://sartak.org/talks/yapc-asia-2009/(parameterized)-roles/ Dist::Zilla Extensibility
Dist::Zilla::Plugin::BPS::Secret
拡張性は大事ですよ。公開できない コードもありますから Thursday, September 10, 2009 Extensibility is also important for code you can't share. We can't ask Ricardo to include company secrets for Dist::Zilla, and maintaining a fork really sucks. CC-BY-SA Hans Dieter Pearcey, 2009
Thursday, September 10, 2009 So now you know! IM::Engine
incoming_callback => sub { my $incoming = shift;
my $message = $incoming->plaintext; $message =~ tr[a-zA-Z][n-za-mN-ZA-M];
return $incoming->reply($message); }
私が今取り組んでいるのはIM::Engineという プロジェクト Thursday, September 10, 2009 IM::Engine is a project I'm working on. It's basically HTTP::Engine for IM. You can write a bot, once, that will run on any service IM::Engine can talk to, including IRC. IM::Engine smooths over the diferences in the protocols. IM::Engine
$self->plugin_collect( role => 'ExtendsObject::User', method => 'traits', );
とりわけ気に入っているのはplugin_collectです
Thursday, September 10, 2009 I've extended Ricardo's design with a number of helper methods. plugin_collect is the one I like best. IM::Engine
$self->plugin_collect( role => 'ExtendsObject::User', method => 'traits', );
この役割を持つプラグインのそれぞれについて
Thursday, September 10, 2009 For each plugin that does the ExtendsObject::User role... IM::Engine
$self->plugin_collect( role => 'ExtendsObject::User', method => 'traits', );
traitsメソッドを呼び出します
Thursday, September 10, 2009 ...call its "traits" method. IM::Engine
my @all_traits = $self->plugin_collect( role => 'ExtendsObject::User', method => 'traits', );
返り値はそれぞれのメソッドの結果を まとめたものになります Thursday, September 10, 2009 The return value of this call is the list of all return values of the "traits" methods. method plugin_collect { my @items;
$self->each_plugin( callback => sub { push @items, shift->$method }, );
return @items; } これがplugin_collectの肝の部分です
Thursday, September 10, 2009 This is the important part of plugin_collect's implementation. There's not much there. I like very layered APIs because they're easier to understand and reuse, especially by your users, than huge monolithic methods. Each layer does only a little bit of work. IM::Engine method new_with_plugins { my %args = ( $self->plugin_collect( role => …, method => 'ctor_args', ), @_, ); $self->new(%args); } これもお気に入りのデザインです
Thursday, September 10, 2009 Here's a piece of design I like a lot. This lets plugins participate in object construction. Each plugin can provide constructor arguments. IM::Engine
push @{ $args{traits} }, $self->plugin_collect( role => …, method => 'traits', );
$self->new_with_traits(%args);
こうしてプラグインにロールを 提供させることも可能です Thursday, September 10, 2009 This lets plugins participate even more in object construction. Now plugins can provide roles for the object you're constructing. This lets plugins add attributes and methods to the object. I use this in a plugin to give state management methods to User objects. MooseX::Traits
$object = Class->new_with_traits( traits => ['Counter'], );
$other = Class->new;
$object->counter; # 0 $other->counter; # Can't locate...
new_with_traitsにはMooseX::Traitsを利用しています
Thursday, September 10, 2009 new_with_traits comes from MooseX::Traits. It's a really nice module for designing pluggable and extensible systems. You just pass a list of roles to new_with_traits and it will arrange it so that the object does those roles. MooseX::Traits
$object = Class->new_with_traits( traits => ['Counter'], );
$other = Class->new;
$object->counter; # 0 $other->counter; # Can't locate... こうすると他の拡張に悪影響を 与えることはありません Thursday, September 10, 2009 Other objects of that class are not afected by new_with_traits. The way it works internally is by creating a new subclass of Class. This is vital because it maintains modularity. I don't want my extensions to screw up your extensions. Role = Trait
Mooseの世界ではRoleとTraitはほぼ同義語です
Thursday, September 10, 2009 In Moose land, roles and traits are basically synonymous. Some people will tell you there are subtle diferences, but there's no clear consensus. I just say "roles" except when I have to say "traits" for a module. Moose Path::Dispatcher HTTP::Engine Dist::Zilla IM::Engine
まだまだ紹介したい例は ありますが、そろそろ時間です Thursday, September 10, 2009 So that is all I have time to cover. There are plenty more nice examples in modules like KiokuDB, Fey, and the now-moosified Catalyst. Moose Extensibility Separation of sugar Dist::Zilla IM::Engine
Mooseは拡張性やシュガーの分離の大切さを 教えてくれました Thursday, September 10, 2009 Moose teaches us that extensibility can lead to a great corpus of extensions. Separation of sugar keeps you and your users flexible. Moose Path::Dispatcher OO sugar layer Dist::Zilla IM::Engine
OOシュガー層という考え方はもっと広まってほしいな
Thursday, September 10, 2009 The OO sugar layer is a new idea that I hope catches on. I'll have to dedicate more time to it. Moose Path::Dispatcher HTTP::Engine Omit inconsequential details IM::Engine
本質じゃない部分をそぎ落とせばアプリは 柔軟で簡潔になります Thursday, September 10, 2009 If you omit inconsequential details, then your application remains flexible and concise. Moose Path::Dispatcher HTTP::Engine Dist::Zilla Explicit pluggability
追加したい機能を明示的に指定できたっていい
Thursday, September 10, 2009 Pluggability does not have to be implicit, as in subclassing. Explicitly controlling pluggability lets you do more interesting things. Moose Path::Dispatcher HTTP::Engine Dist::Zilla IM::Engine Extreme pluggability DRY IM::Engineのようなやり方をすればDRYにできる
Thursday, September 10, 2009 … such as the things IM::Engine does, by letting plugins manipulate system objects. It also provides methods for common plugin operations so you don't have to repeat them everywhere. Moose WRITEPath::Dispatcher HTTP::Engine Dist::Zilla TESTS!!!IM::Engine
Thursday, September 10, 2009 I almost forgot... Moose おっと忘れるところPath::Dispatcher HTTP::Engine だった。テスト書けよ!Dist::Zilla IM::Engine
Thursday, September 10, 2009 I almost forgot... Thanks to my translator
Kenichi Ishigaki
83
Thursday, September 10, 2009 Thank you to Ishigaki-san for translating my slides!