前回の振り返り
前回は、足し算を表すAddExprを作り、式を木構造で表現できるようになりました。
現在、NumberExpr、DiceExpr、AddExprの3つのクラスがあります。すべてevalメソッドを持っていますが、これは単なる約束事であり、強制力がありません。
今回は、Moo::Roleを使ってこの約束を明文化します。
問題: evalメソッドを実装し忘れたら
新しい式クラスを追加するとき、evalメソッドを実装し忘れたらどうなるでしょうか。
| |
evalを呼んだときに初めてエラーになります。コードを書いた時点では気づけません。

Roleでインターフェースを定義する
Moo::Roleを使うと、「このメソッドは必ず実装してね」という約束を強制できます。
| |
requiresで指定されたメソッドを実装していないクラスは、ロードした時点でエラーになります。

各クラスにRoleを適用する
NumberExpr、DiceExpr、AddExprにExpressionRoleを適用しましょう。
| |
with 'ExpressionRole'と書くことで、ロールを適用します。evalメソッドがないと、このクラスをロードした時点でエラーになります。
完成コード
すべてのクラスにロールを適用した完成コードです。
| |
ロールの効果
classDiagram
class ExpressionRole {
<<role>>
+eval()*
}
class NumberExpr {
+value: Int
+eval(): Int
}
class DiceExpr {
+count: Int
+sides: Int
+eval(): Int
}
class AddExpr {
+left: Expression
+right: Expression
+eval(): Int
}
ExpressionRole <|.. NumberExpr
ExpressionRole <|.. DiceExpr
ExpressionRole <|.. AddExpr
ExpressionRoleがあることで、以下のメリットが得られます。
1. 早期エラー検出
evalメソッドが未実装のクラスは、ロード時点(useした時点)でエラーになります。
| |
| |
2. ドキュメント効果
ロールを見れば、式クラスが満たすべき契約が明確にわかります。新しい開発者がコードを読むときの道しるべになります。
3. 型チェックへの応用
Roleは、Type::Tinyなどの型システムと組み合わせて、引数の型チェックにも使えます。
| |
この例を動かすにはType::Tiny(Types::Standard)を導入する必要があります。概念紹介として読み進めてください。
インターフェースという設計思想
ExpressionRoleのような「メソッドの約束だけを定義したロール」は、他の言語では「インターフェース」と呼ばれます。
- Java: interface
- TypeScript: interface
- Go: interface
- Perl: Moo::Role + requires
インターフェースを使うと、「何ができるか」に焦点を当てた設計ができます。具体的な実装は知らなくても、evalメソッドがあることだけ分かっていれば、その式を使えます。
今回のまとめ
今回は、ExpressionRoleを定義してすべての式クラスに適用しました。
requires 'eval'でメソッドの実装を強制- 未実装クラスはロード時にエラーになる
- ロールはインターフェースとして機能する
次回は、SubExprやMulExprを追加して、引き算と掛け算に対応します。開放閉鎖の原則(OCP)を実践します。
