実務で設計任せてもらえることも多くなってきたので、良い設計ってなんだろうなぁといつも思いながら(騙し騙し)設計を行なっています。
そんな中、「良いコード/悪いコードで学ぶ設計入門」という本を知り、最近読み進めてます。
https://www.amazon.co.jp/dp/4297127830
まだまだ読み進めている途中なのですが、個人的に目から鱗のパターンを知ったので学習がてらアウトプットしようかと思いました。
参考元は書籍 : 「6.3.1 ポリシーパターンで条件を集約」より。
if文でのルール判定で個人的に感じる辛み
ある処理を行えるかどうかルールを規定して、許可する・しない処理を書くことは多いかと思います。
が、個人的な経験上、if文が増えると以下のような問題が発生するなぁといつも思ってました。
- if文がネストする
- if文が書かれている行数が長くなる
ここはその場の対応でどんどんとルールが足されていくが故の仕方なさ。早期returnしてくれてる場合もあるのですが、ルールがどんどんと重なっていってコードを読む時の辛さがあります。
- if文の中で後続処理に必要な値の設定をする
稀にこれがされているパターンもある。たぶん経緯としては判定に必要な情報を変数持ち出したから、ついでに後続処理でも使っちゃえパターン。気持ちは分かるけど変数の生存期間が長くなってこれまたコードを読むときに辛い。
そして大抵こういうロジックってテストしやすいようになってないので、修正する前も後もお祈りしながら修正することが多いです。
ポリシーパターン
書籍ではそこらへん丁寧に条件分岐をどうよくして行くかが記載されているのですが、ルールを部品化するという視点でポリシーパターンを紹介してました。
丁寧な内容は書籍を見ていただくとして、まとめるとこんな手順になります。
- ルールを規定するinterfaceを用意
- 戻り値はtrue or false。ルールを満たしているかを判定することがこのinterfaceの目的
- 1ルールにつき1クラス、intrafaceを実装したクラスを作成(ルールクラス)
- ルールを全て満たしているかを判定するポリシークラスを作成
- ポリシークラスはルールクラスを持っている
- メソッドを呼び出すと順次ルールクラスを呼び出してルール違反していないか判定する
コードベースで超単純化するとこんな感じ。
// NOTE: Historyクラスは実際に判定したい内容を含んでいるクラスのイメージ
/* ルールを規定するinterface */
interface Rule{
boolean ok(final History history);
}
/* ルールクラスの作成 */
class ARule implements Rule {
public boolean ok(final History history) { return 何かしらの処理; }
}
class BRule implements Rule {
public boolean ok(final History history) { return 何かしらの処理; }
}
/* ポリシークラスの作成 */
class Policy{
private final Set<Rule> rules;
Policy(){
rules = new HashSet();
}
void add(final Rule rule){
rules.add(rule)
}
/* 判定処理 */
boolean complyWithAll(final History history){
for(Rule each : rules){
if(!each.ok(history)) return false;
}
return true;
}
}
/* 他の無関係なルールが突っ込まれないようにPolicyクラスラッピングしとくとなおよし */
class WrapPolicy{
private final Policy policy;
WrapPolicy(){
policy = new Policy();
policy.add(new ARule());
policy.add(new BRule());
}
/* これを呼び出してルール判定を行う */
boolean complyWithAll(final History history){
return policy.complyWithAll(history);
}
}
ルールをクラス化するというのが個人的な感動ポイント!
- 実情としてif文でルール何個も繋がっていくことが多いから、こうやって判定をスマートにまとめられるのは良いなぁと思いました。
- Spring Bootやってた時、Validationの仕組みがあったのを思い出しました。
- アノテーションをつけてやる感じだったけど、内部的にはこういうことしているんでしょうかね?
- Validationの仕組みだとエラーメッセージとかついてきたので、判定の時にbooleanじゃなくて何かしらのDTOに差し替えてあげると似たようなことが実現できそうかなと。
Pythonでやるとどうなるか?
ここまでだと単なるまとめなのでもう少し私の実情に寄せて考えてみます。
現在、実務だと現在Pythonを使っていますので↑のJavaのコードをPythonに書き換えてみます。
Pythonの言語仕様としてインターフェースという概念がありません。
が、Classを使ってやれば似たようなことが可能なのでそれっぽく書いてみました(動作確認はしてないです)。
import abc
from typing import Final
# NOTE: Historyクラスは実際に判定したい内容を含んでいるクラスのイメージ
# ルールを規定するinterface
class Rule(metaclass=abc.ABCMeta):
@abdc.abstractmethod
def ok(history : Final[History]) -> bool:
raise NoImplementedError()
# ルールクラスの作成
class ARule(Rule):
def ok(self,history : Final[History]) -> bool:
何かしらの処理
class BRule(Rule):
def ok(self,history : Final[History]) -> bool:
何かしらの処理
# ポリシークラスの作成
class Policy:
def __init__(self):
self.__rules : set[] = {}
def add(self,rule : Final[Rule] -> None:
self.__rules.add(rule)
# 判定処理
def complyWithAll(self,history:Final[History]) -> bool:
for rule in rules:
if not rule.ok(history):
return False
return True
# 他の無関係なルールが突っ込まれないようにPolicyクラスラッピングしとくとなおよし
class WrapPolicy:
def __init__(self):
self.__policy = Policy()
self.__policy.add(ARule())
self.__policy.add(BRule())
# これを呼び出してルール判定を行う
def complyWithAll(self,history : Final[History]) -> bool:
return self.__policy.complyWithAll(history)
最後に
ルールをクラスにするという考えを持ち合わせていなかったので、このポリシーパターンは私の中では割と新しい視点になりました。
チーム内で認識合わせていかないといけないというところは課題としてありそうですが、ルールクラス化していくとテストも容易になるので真似していきたいです。
(書籍もおすすめなので、コーディングや設計悩んでる方いらっしゃったら目を通してみるのはアリかと)