おさらい
この記事は区分をモデリングしよう(その1) の続きです。
前回は四則演算ができるコマンドラインツールをベースに、区分を定義し、それをコードで表現することで見通しが良くなったり、責務の分離が自然に行えるようになることを見てきました。
今回はヘルプテキストを表示する機能を追加します。
演算ごとに説明が異なるので、ここでも区分に対して分岐が生じることになります。
これをどう扱っていけば見通しがよく拡張しやすいコードになるのかを見ていきます。
ヘルプの追加
コマンドに引数が与えられなかった場合に、次のようなヘルプを表示することにします。
$ python calc.py
使い方: <operator> num1 num2
operator:
add: 2つの整数を足します
sub: 最初の整数から2番目の整数を引きます
mul: 2つの整数を掛けます
div: 最初の整数を2番目の整数で割ります
まずは何も考えずにメッセージをそのまま表示してみます。
ちょっと長いですが、前回のプログラムと合わせて全体を示します。
import enum
import sys
class Operator(enum.Enum):
ADD = 'add'
SUB = 'sub'
MUL = 'mul'
DIV = 'div'
def add(a: int, b: int) -> int:
return a + b
def sub(a: int, b: int) -> int:
return a - b
def mul(a: int, b: int) -> int:
return a * b
def div(a: int, b: int) -> int:
return a // b
op_func = {
Operator.ADD: add,
Operator.SUB: sub,
Operator.MUL: mul,
Operator.DIV: div,
}
# 使い方を表示する
def print_usage():
print('使い方: <operator> num1 num2')
print('')
print('operator:')
print(' add: 2つの整数を足します')
print(' sub: 最初の整数から2番目の整数を引きます')
print(' mul: 2つの整数を掛けます')
print(' div: 最初の整数を2番目の整数で割ります')
def main(argv):
if len(argv) < 2:
print_usage()
return
op = Operator(argv[1])
a = int(argv[2])
b = int(argv[3])
result = calc(op, a, b)
print(result)
def calc(op: Operator, a: int, b: int) -> int:
f = op_func[op]
return f(a, b)
main(sys.argv)
ひとまず出したいメッセージは出せるようになりました。
ヘルプテキストの中で、1行ずつ各四則演算についての説明があります。
これは演算という区分ごとに関連した知識なので、各区分に対応づけてあげましょう。
そうすれば区分が追加、削除されたときヘルプテキストだけそのままになってしまうというようなことはなくなります。
区分ごとに説明を定義する
# 前半省略 ...
op_func = {
Operator.ADD: add,
Operator.SUB: sub,
Operator.MUL: mul,
Operator.DIV: div,
}
op_help = {
Operator.ADD: '2つの整数を足します',
Operator.SUB: '最初の整数から2番目の整数を引きます',
Operator.MUL: '2つの整数を掛けます',
Operator.DIV: '最初の整数を2番目の整数で割ります',
}
# 使い方を表示する
def print_usage():
print('使い方: <operator> num1 num2')
print('')
print('operator:')
for op in Operator:
print(f' {op.value}: {op_help[op]}')
出力は先ほどと同じままです。
説明が区分に依存していることが先ほどより明確になりました。
関連性の高いものをまとめる
修正したものは演算同士やヘルプメッセージ同士でグルーピングされています。しかし、それぞれのグループ内の要素同士の関連性はあまりありません。
例えば「足し算の説明」は「足し算」に関連していますが、「引き算の説明」とは直接関係ありません。
そこで、演算とヘルプメッセージと関連の強い、区分を中心にグルーピングし直してみましょう。
class Add:
def desc(self) -> str:
return '2つの整数を足します'
def calc(self, a: int, b: int) -> int:
return a + b
class Sub:
def desc(self) -> str:
return '最初の整数から2番目の整数を引きます'
def calc(self, a: int, b: int) -> int:
return a - b
class Mul:
def desc(self) -> str:
return '2つの整数を掛けます'
def calc(self, a: int, b: int) -> int:
return a * b
class Div:
def desc(self) -> str:
return '最初の整数を2番目の整数で割ります'
def calc(self, a: int, b: int) -> int:
return a // b
関連性の高いもの同士がまとまりました。
このオブジェクトを使って先ほどのコードをリファクタリングしてみましょう。
import enum
import sys
class Operator(enum.Enum):
ADD = 'add'
SUB = 'sub'
MUL = 'mul'
DIV = 'div'
class Add:
def desc(self) -> str:
return '2つの整数を足します'
def calc(self, a: int, b: int) -> int:
return a + b
class Sub:
def desc(self) -> str:
return '最初の整数から2番目の整数を引きます'
def calc(self, a: int, b: int) -> int:
return a - b
class Mul:
def desc(self) -> str:
return '2つの整数を掛けます'
def calc(self, a: int, b: int) -> int:
return a * b
class Div:
def desc(self) -> str:
return '最初の整数を2番目の整数で割ります'
def calc(self, a: int, b: int) -> int:
return a // b
op_object = {
Operator.ADD: Add(),
Operator.SUB: Sub(),
Operator.MUL: Mul(),
Operator.DIV: Div(),
}
def print_usage():
print('使い方: <operator> num1 num2')
print('')
print('operator:')
for op in Operator:
o = op_object[op]
print(f' {op.value}: {o.desc()}')
def main(argv):
if len(argv) < 2:
print_usage()
return
op = Operator(argv[1])
a = int(argv[2])
b = int(argv[3])
result = calc(op, a, b)
print(result)
def calc(op: Operator, a: int, b: int) -> int:
o = op_object[op]
return o.calc(a, b)
main(sys.argv)
前より少しだけ見通しが良くなりました。
修正前は、どの振る舞いを呼びたいかによって、どのマッピング(op_func
、op_help
)を利用するかを覚えておく必要がありました。
修正後は 区分に応じて何かしたければ、とりあえず op_object
からオブジェクトを取り出し、そのオブジェクトに対してやりたいことをお願いすればよくなりました。
微妙な違いですが、修正前のようなコードが複数箇所になってくると覚えることが増え、途端に読みにくくなっていきます。
本当にやりたいことは何?
さて、次は区分に対応した処理を実行しているところをもう少し詳しくみてみましょう。
例えば、演算を実行したい場合次の手順で行っています。
- 区分(足し算か引き算か、etc)に応じて、オブジェクトを取得する
- 区分に応じたオブジェクトに演算をお願いする
ヘルプメッセージを表示したい時も同じ流れです。
この中で、本当に行いたいのは2番目です。1番目の手順は2番目を実行するために必要な手続きだから行っているものの、本当にやりたいことではありません。
これは、やりたいことに対して余分な手続きを、呼び出し側が覚えておかなければならないことを意味しています。
この余分な手続きをどうにか、呼び出し元から隠蔽できないでしょうか。
そうすれば、呼び出し元は何がしたいかだけを書けばよいことになります。
それができるのは区分そのものです。当たり前ですが、区分からしか区分に応じた処理を特定できないからです。
区分それ自身が振る舞いに関する知識を持てば(振る舞いのインタフェースとそれを実装しているオブジェクトを知れば)、区分自身でその振る舞いを実行できるようになります。
実際にコードで表現すると以下のようになります。
# class Add, Sub, Mul, Divの定義までは変更ないため省略
class Operator(enum.Enum):
ADD = 'add'
SUB = 'sub'
MUL = 'mul'
DIV = 'div'
def calc(self, a: int, b: int) -> int:
# self.delegate_ofはOperator.delegate_ofを参照しているのと同じ
o = self.delegate_of[self]
return o.calc(a, b)
def desc(self) -> str:
o = self.delegate_of[self]
return f'{self.value}: {o.desc()}'
# Operatorクラス内で delegate_of = ... と書くとEnumの定数宣言に
# なってしまうため、クラス宣言の外で定義
Operator.delegate_of = {
Operator.ADD: Add(),
Operator.SUB: Sub(),
Operator.MUL: Mul(),
Operator.DIV: Div(),
}
def print_usage():
print('使い方: <operator> num1 num2')
print('')
print('operator:')
for op in Operator:
print(f' {op.desc()}')
def main(argv):
if len(argv) < 2:
print_usage()
return
op = Operator(argv[1])
a = int(argv[2])
b = int(argv[3])
result = op.calc(a, b)
print(result)
main(sys.argv)
Operator
に四則演算に関する知識(区分、振る舞いのインタフェース、区分と振る舞いの実装とのマッピング)が集約されました。
これにより、Operator
オブジェクトを利用する側のコードがシンプルになりました。
例えば calc()
関数はすることがなくなったため消えています。
まとめ
今回は区分に対応する振る舞いの種類が増えたケースを見てきました。
区分を中心に関連性のある振る舞いを集める(モデリングする)ことで、区分に応じた処理が直感的で率直に書けるようになったと思います。
区分に関する今回の記事はここで終わりですが、もう少しだけ続きの話をしたいと思います。
おまけ
今の状況で分かりやすく正しいと思えていたモデリングでも、ちょっとした変更で再度考え直す場面が必ず出てきます。
例えば今回の例で言えば、以下のような平方根を計算するコマンドを追加することになったとしたらどうでしょう。
$ python calc.py root 2
1.41421356
これまでのOperator
にはないインタフェースを持つので、簡単には実装できません。
改めて全体がどうあるべきか設計が問われることになります。
この変更を考慮できていなかったのは考慮不足でしょうか。
もし、どこかの段階で想定していればうまく対処できたでしょうか。
早すぎる最適化をしてしまったとのかも知れません。
しかし、すべての変更に対して予測し、備えることはできません。
モデリングは絶え間ない改善の連続です。いつでも完璧ではないことを受け入れて、最初から正解を目指すのではなく、試行錯誤をしながら磨いていくものだと思います。
ちょっとずつ改良を加え続けていると、最初は小さな影響だとしても、だんだんと形が見えてきます。そうした発見がモデリングの面白いところであり、醍醐味だと思います。
今回の記事でも、そうした面白さ、発見を少しでも味わってもらえていれば嬉しいです。