Featured image of post 第10回-継承しないで振る舞いを共有 - Mooで覚えるオブジェクト指向プログラミング

第10回-継承しないで振る舞いを共有 - Mooで覚えるオブジェクト指向プログラミング

MessageもUserも作成日時を記録したい。でも継承関係はない…。そんなとき、Moo::Roleを使えば継承なしで同じ機能を複数のクラスに追加できます。ロールによる振る舞いの共有を学びましょう。

@nqounetです。

「Mooで覚えるオブジェクト指向プログラミング」シリーズの第10回です。

前回の振り返り

前回は、子クラスで同じ名前のメソッドを定義して親クラスのメソッドを上書きする オーバーライド を学びました。

if文でクラスの種類を判定して分岐するのではなく、各クラスが自分のformatメソッドを持つ設計にすることで、コードがスッキリしましたね。

今回は、継承とは別の方法で振る舞いを共有する ロール を学びます。

問題:継承関係にないクラスで同じ機能が必要

掲示板アプリに「作成日時を記録する」機能を追加したくなりました。MessageとUserの両方にcreated_at属性を持たせたいとします。

しかし、ここで困った問題があります。

  • MessageとUserは継承関係にない
  • Userは投稿ではないので、Messageの子クラスにするのはおかしい
  • 逆に、MessageがUserを継承するのもおかしい
  • かといって、両方のクラスに同じコードを書くのはコードの重複になる

継承は「AはBの一種である」(is-a関係)のときに使うべきです。UserはMessageの一種ではありません。無理に継承関係を作ると、設計が歪んでしまいます。

このような「継承関係にないが、同じ振る舞いを持たせたい」ケースで活躍するのが ロール です。

解決策:Moo::Roleでロールを定義

ロールとは、クラスに「合成(compose)」できる振る舞いの集まりです。継承ではなく、機能の断片を複数のクラスで共有する仕組みです。

PerlではMoo::Roleを使ってロールを定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package Timestampable {
    use Moo::Role;

    has created_at => (
        is      => 'ro',
        default => sub { time },  # 現在のエポック秒
    );

    sub created_date {
        my $self = shift;
        my @t = localtime($self->created_at);
        return sprintf('%04d-%02d-%02d', $t[5]+1900, $t[4]+1, $t[3]);
    }
};

use Mooではなくuse Moo::Roleと書くのがポイントです。

このTimestampableロールは、created_at属性とcreated_dateメソッドを提供します。しかし、ロール単体ではオブジェクトを作ることはできません。クラスに合成して初めて使えるようになります。

解決策:withでロールを適用

ロールをクラスに適用するには、withを使います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package Timestampable {
    use Moo::Role;

    has created_at => (
        is      => 'ro',
        default => sub { time },
    );

    sub created_date {
        my $self = shift;
        my @t = localtime($self->created_at);
        return sprintf('%04d-%02d-%02d', $t[5]+1900, $t[4]+1, $t[3]);
    }
};

package User {
    use Moo;
    with 'Timestampable';  # ロールを適用!

    has name => (is => 'ro', required => 1);

    sub display_name {
        my $self = shift;
        return $self->name;
    }
};

package Message {
    use Moo;
    with 'Timestampable';  # 同じロールを適用!

    has content => (is => 'ro', required => 1);
    has user    => (is => 'ro', required => 1);

    sub show {
        my $self = shift;
        my $author = $self->user->display_name;
        print "[$author] " . $self->content . "\n";
    }
};

my $user = User->new(name => 'nqounet');
my $msg = Message->new(content => 'こんにちは!', user => $user);

print "User作成日: " . $user->created_date . "\n";
print "Message作成日: " . $msg->created_date . "\n";

with 'Timestampable'と書くだけで、UserクラスにもMessageクラスにもcreated_at属性とcreated_dateメソッドが追加されます。

  • UserはMessageを継承していない
  • MessageもUserを継承していない
  • しかし、両方とも「作成日時を持つ」という振る舞いを共有している

これがロールの力です。継承が「AはBの一種である」(is-a)なら、ロールは「Aは〜できる」「Aは〜を持つ」(has-a、can-do)という関係を表現します。

まとめ

  • Moo::Roleでロールを定義できる
  • with 'RoleName'でロールをクラスに適用できる
  • ロールは継承関係にないクラスで振る舞いを共有するのに使う
  • 継承は「AはBの一種である」、ロールは「Aは〜できる」という関係を表す

次回予告

次回は、属性として持っているオブジェクトに仕事を任せる 委譲(delegation) を学びます。クラスが肥大化してきたとき、handlesオプションで責務を分離する方法を紹介します。お楽しみに。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。