@nqounetです。
「Mooで覚えるオブジェクト指向プログラミング」シリーズの第9回です。
前回の振り返り
前回は、extendsを使った継承を学び、共通のコードを親クラスにまとめる方法を紹介しました。
MessageクラスからAdminMessageクラスを派生させて、コードの重複を解消しましたね。
今回は、親クラスのメソッドを子クラスで 上書き(オーバーライド) する方法を学びます。
if文で分岐だらけになる問題
前回のAdminMessageクラスには、is_important属性がありました。「重要なお知らせ」には「【重要】」ラベルを付けたいですよね。
安直に実装すると、こうなりがちです。
1
2
3
4
5
6
7
8
9
10
| sub show {
my $self = shift;
my $author = $self->user->display_name;
if ($self->isa('AdminMessage') && $self->is_important) {
print "【重要】[$author] " . $self->content . "\n";
} else {
print "[$author] " . $self->content . "\n";
}
}
|
クラスの種類によってif文で分岐する方法は、一見シンプルに見えます。しかし、クラスが増えるたびに分岐が増え、コードがどんどん複雑になっていきます。
- UrgentMessageクラスが増えたら?→ 新しい分岐を追加
- NoticeMessageクラスが増えたら?→ また分岐を追加
- 分岐を追加し忘れたらバグになる
これでは継承のメリットが台無しです。
if文での分岐を避けるために、「メッセージをフォーマットする」という責務を専用のメソッドに切り出しましょう。
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
| package User {
use Moo;
has name => (is => 'ro', required => 1);
sub display_name {
my $self = shift;
return $self->name;
}
};
package Message {
use Moo;
has content => (is => 'ro', required => 1);
has _like_count => (is => 'ro', default => 0);
has user => (is => 'ro', required => 1);
sub format {
my $self = shift;
my $author = $self->user->display_name;
return "[$author] " . $self->content;
}
sub show {
my $self = shift;
print $self->format . "\n";
}
};
my $user = User->new(name => 'nqounet');
my $msg = Message->new(content => 'こんにちは!', user => $user);
$msg->show; # [nqounet] こんにちは!
|
formatメソッドは「表示用の文字列を作る」役割だけを担います。showメソッドはformatの結果を画面に出力するだけです。
この分離がオーバーライドの伏線になります。
子クラスで同じ名前のメソッドを定義すると、親クラスのメソッドを 上書き(オーバーライド) できます。
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
47
48
49
50
51
52
53
| package User {
use Moo;
has name => (is => 'ro', required => 1);
sub display_name {
my $self = shift;
return $self->name;
}
};
package Message {
use Moo;
has content => (is => 'ro', required => 1);
has _like_count => (is => 'ro', default => 0);
has user => (is => 'ro', required => 1);
sub format {
my $self = shift;
my $author = $self->user->display_name;
return "[$author] " . $self->content;
}
sub show {
my $self = shift;
print $self->format . "\n";
}
};
package AdminMessage {
use Moo;
extends 'Message';
has is_important => (is => 'ro', default => 0);
sub format { # 親クラスのformat()を上書き!
my $self = shift;
my $author = $self->user->display_name;
my $label = $self->is_important ? '【重要】' : '';
return "$label\[$author] " . $self->content;
}
};
my $user = User->new(name => 'admin');
my $normal = Message->new(content => '通常の投稿', user => $user);
$normal->show; # [admin] 通常の投稿
my $important = AdminMessage->new(
content => 'システムメンテナンスのお知らせ',
user => $user,
is_important => 1,
);
$important->show; # 【重要】[admin] システムメンテナンスのお知らせ
|
AdminMessageクラスでformatメソッドを再定義しています。これがオーバーライドです。
$important->showを呼び出すと、内部で$self->formatが実行されます。この時、$selfはAdminMessageオブジェクトなので、AdminMessageのformatメソッドが呼ばれます。
ポイントは、showメソッドには手を加えていないことです。親クラスのMessageに定義されたshowがそのまま使われていますが、formatだけが子クラスの実装に差し替わっています。
これがオーバーライドの力です。if文の分岐は一切ありません。新しいクラスを追加しても、そのクラスでformatを定義するだけで済みます。
まとめ
- 子クラスで同じ名前のメソッドを定義すると、親クラスのメソッドを上書きできる
- この上書きを オーバーライド と呼ぶ
- if文でクラスの種類を判定して分岐するのは避ける
- 共通の呼び出し元(show)から上書きされたメソッド(format)を呼ぶ設計がスッキリする
次回予告
次回は、継承とは違う形で振る舞いを共有する方法を学びます。MessageもUserも「作成日時を持つ」という共通の機能を持たせたいとき、どうすればいいでしょうか? ロール の出番です。お楽しみに。