[僕] 2009年10月12日 アーカイブ

僕ト云フ事

たろマークはてなブックマーク

2009年10月12日

[designpattern][moose][perl] perl で Observer パターン

Ruby で Observer パターンをやってみたところで、Perl でもやってみたくなったので書いてみました。お題はコードの世界に載っているテキスト表示の時計です。

ruby にある observer モジュールは include で mixin して使うので、Perl では Moose の Role を使って実装してみます。(モダン Perl 入門にもデザパタの章で Observer パターンがありますが、あえて ruby の observer モジュールを模してみます。)

で、これが Observable な Role です。これは時計の心臓部 Tick の Role なので Tick::Role::Observable としました。

package Tick::Role::Observable;
 
use Moose::Role;
 
has observer => (
    is => 'rw',
    default => sub { {} },
);
 
has state => (
    is => 'rw',
    default => 0,
);
 
sub is_changed {
    my ($self) = @_;
    return $self->state();
}
 
sub changed {
    my ($self) = @_;
    $self->state(1);
}
 
sub notify_observers {
    my ($self, @args) = @_;
 
    return unless $self->is_changed;
    $self->state(0);
 
    foreach my $package ( keys %{$self->observer} ) {
        eval {
            $self->observer->{$package}->update(@args);
        };
        if ( $@ ) {
            confess $@;
        }
    }
 
    return 1;
}
 
sub add_observer {
    my ($self, $obj) = @_;
 
    my $package = ref $obj;
    confess $obj . 'is not object'
        unless $package;
 
    confess $package . 'can not update'
        unless $obj->can('update');
 
    $self->observer->{$package} = $obj;
}
 
1;

このモジュールを with 'Tick::Role::Observable'; するとそのモジュールに Observer な振る舞いが付与されます。

元のお題の時計モジュールはこんな感じ。

package Tick;
 
use Moose;
 
with 'Tick::Role::Observable';
 
__PACKAGE__->meta->make_immutable();
 
no Moose;
 
use DateTime;
use DateTime::TimeZone;
use Time::HiRes qw(sleep gettimeofday);
 
sub start {
    my ($self, @args) = @_;
 
    my $tz = DateTime::TimeZone->new( name => 'Asia/Tokyo' );
    while (1) {
        my $now = DateTime->now( time_zone => $tz );
        $self->changed();
        $self->notify_observers($now->hour, $now->minute, $now->second);
 
        # micro second 単位のズレを調整
        sleep 1.0 - (gettimeofday)[1] / 1000000.0;
    }
}
 
1;

Tick->new->start すると1秒ごと sleep するループに陥ります。
1秒ごとに changed メソッドで更新フラグを立てて、notify_observers メソッドで監視者のオブジェクトに更新を通知します。

では、監視者として時刻をテキスト表示するオブジェクトを用意します。

package TextClock;
 
use Moose;
 
__PACKAGE__->meta->make_immutable();
 
no Moose;
 
sub update {
    my ($self, @args) = @_;
 
    local $| = 1;
    printf "\e[8D%02d:%02d:%02d", @args;
}
 
1;

Observable なモジュールの監視者になるには update メソッドが必要です。
notify_observer メソッドで更新通知される際には update メソッドが呼ばれるからです。
なので、 TextClock モジュールにも update メソッドを用意します。
引数には notify_observer メソッドに渡された引数が全部渡ってきます。

さて、これで時計の心臓部である Tick モジュールと TextClock モジュールができました。この二つを監視者と被監視者としてスクリプトにまとめてみます。

use Tick;
use TextClock;
 
my $tick = Tick->new();
$tick->add_observer( TextClock->new() );
$tick->start();

add_observer に update メソッドを持ったオブジェクトを渡してあげると、監視者と被監視者の関係が簡単にできます。
このスクリプトを実行すると現在時刻がテキストで表示され、1秒ごとに更新されます。

肝は接点が add_observer メソッドだけなところですね。
時計の表示を変えたくなったら update メソッドを持ったオブジェクトを作って add_observer してあげるだけで良くなります。(とコードの世界に書いてあった)

自分が普段使ってる言語で書き直せたらそれなりに理解できてるかしら?

まつもとゆきひろ コードの世界‾スーパー・プログラマになる14の思考法
まつもとゆきひろ
日経BP出版センター
売り上げランキング: 44091
おすすめ度の平均: 4.0
4 Rubyに導入された思考法
モダンPerl入門 (CodeZine BOOKS)
牧 大輔
翔泳社
売り上げランキング: 18356
おすすめ度の平均: 5.0
5 perl経験者は読んで損はしない
5 Perl中級者におすすめしたい

[irc][lazy-people][ruby] irc.lazy-people.org#project に thanksbot を復帰させました。

少し間が開いてしまいましたが、irc.lazy-people.org#project にありがとうを伝える thanksbot を放流しました。
この bot に対してありがとうを伝えると http://lazy-people.org/#ありがとう新着順 の一覧に載ります。まぁ、単純なボットですね。

このチャンネルには元々 thanksbot がいたのですが、lazy-people.org Xen 化作戦失敗以降復帰させていなかったのでした。
旧 thanksbot は mojunc という tomyhero 製 Web API を使っていたのですが、これを復帰させるのがめんどうだったので Wedata を使うことにしました。サーセン >_<
Wedata の使い方はこれで良いのかも謎w

そして、せっかくだからと覚え立ての ruby を使って書くことにしました。
IRC の bot 化には Net::IRC::Client を使い、Wedata への投稿には、こちらのモジュールを使わせていただきました。オリジナルの wedata.rb は、JsonParser に依存していたのですが、gem でインストールできる JSON を使うように少し修正しています。(修正したコード

新規追加しかしないから wedata.rb は大げさだったかも。

#!/usr/bin/env ruby
# encoding: utf-8
 
$LOAD_PATH << "lib"
 
require 'rubygems'
require 'net/irc'
require 'wedata'
require 'observer'
require 'yaml'
 
require 'pp'
 
thanks_config = YAML.load_file('config.yaml')
 
class ThanksBot < Net::IRC::Client
  include Observable
 
 
  def initialize(*args)
    p args
    @channel = args[2]
    args.delete_at 2
    super
  end
 
  def on_rpl_welcome(m)
    post JOIN, @channel
  end
 
  def on_privmsg(m)
    if match = m[1].match(/^thanksbot:\s*(.*)/)
      changed
      notify_observers('', match[0])
      post NOTICE, m[0], "#{m.prefix.nick}: #{match[0]}"
    end
  end
end
 
class ThanksDB < WedataDatabase
  def update(name, message)
    create_item(name, { 'message' => message })
  end
end
 
thanksbot = ThanksBot.new(thanks_config['irc']['host'], thanks_config['irc']['port'], thanks_config['irc']['channel'],thanks_config['irc']['nicks'])
 
thanksdb = ThanksDB.new('thanks', thanks_config['wedata_api'])
 
thanksbot.add_observer(thanksdb)
thanksbot.start

ソースはここにあります。

Net::IRC::Client を継承した ThankBot と WedataDatabase を継承した ThanksDB は ovserver ライブラリを使って observer パターンで動作をひもづけてます。まつもとゆきひろ コードの世界を読んでいて、これを試して見たかったというのが大きいかもw

たぶんなにげに初めて書いた ruby プログラムのような気がします。
既存のライブラリを組み合わせただけでお手軽でした。ありがとうありがとう!

プログラミング言語 Ruby
まつもと ゆきひろ David Flanagan
オライリージャパン
売り上げランキング: 13443
おすすめ度の平均: 4.0
5 基本書にふさわしい本
5 rubyマスターになるために
3 鬱にされる鬱然たるRuby教科書
2 For Rubist
まつもとゆきひろ コードの世界‾スーパー・プログラマになる14の思考法
まつもとゆきひろ
日経BP出版センター
売り上げランキング: 40925
おすすめ度の平均: 4.0
4 Rubyに導入された思考法