はじめに
この記事の対象者は 「関心の分離」 という考え方をあまりよく知らない方です。
すでに知っている方は読まなくても大丈夫だと思います。
ドメイン知識をクラスに落とし込むのってとても難しいですよね。
うまく落とし込めたと思ったら1つのクラスの責務にしては大きすぎるものが出来上がったり、はたまたドメイン知識が様々なクラスに散らばってしまったりと。。。
これを解決するには単一責任の原則やカプセル化、ドメイン駆動設計、関心の分離などがあります。
そこで、今回は「関心の分離」とは何か?について簡単にまとめてみました。
まず初めに、関心の分離がうまくできていない例(悪い例)を初めに見ていただき、その例の悪い部分を理解してもいます。
次に関心の分離という考え方を知っていただきます。
最後に関心の分離を意識して悪い例をリファクタリングして、関心の分離の旨みを知ってもらおうと思います。
※サンプルコードはrubyで書いてあります。
関心の分離がうまくできていない例(悪い例)
サンプルコード
毎月自宅に世界各国の美味しいお菓子が届くようなサブスクがあるとします。
これを販売している会社は、以下のタイミングでお客さんにメールを送る仕組みを作っていました。
- サブスク継続時の決済が完了したタイミング:決済完了メール
- 自宅にお菓子を配送したタイミング:配送完了メール
- お客さんがサブスクを解約したタイミング:解約メール
また、メールはヘッダー・案内文・フッターの3つの項目に分かれており、これらに入る情報はメールの種類ごとに異なるとします。
(現実にはメールの種類によってヘッダーやフッターが変わることはあまり無いかもしれませんが、関心の分離を説明しやすくするためにこのような仕様にしています。)
これを関心の分離がうまくできていないコードで表現してみました。
class Email
def charge_header
# 決済完了時のヘッダー
end
def delivery_header
# 配送完了時のヘッダー
end
def cancel_header
# 解約時のヘッダー
end
def charge_guide
# 決済完了時の案内文
end
def delivery_guide
# 配送完了時の案内文
end
def cancel_guide
# 解約時の案内文
end
def charge_footer
# 決済完了時のフッター
end
def delivery_footer
# 配送完了時のフッター
end
def cancel_footer
# 解約時のフッター
end
private
def _something_common_item
# 何かしらの共通項目
end
end
ご覧の通りメールクラスには決済完了時・配送完了時・解約時の3つの知識が入り混じっている状態です。
イメージ図は以下のようになります。(一部枠線を重ねているのは共通項目があることを表現しています)
このコードはどうしてダメなの?
現状は読めないことはないし、むしろ メールに関する情報が集約されているので良いコードなんじゃない? と思う方もいらっしゃるかもしれません。
(「メールに関する情報が集約されているので良いコードなんじゃない?」が太字になっていますが、これは関心の分離のコツのセクションで意識して欲しいポイントなのでそうしています。)
なぜこのコードがダメなのかについて、仕様変更の観点から見ていきましょう。
仕様変更1:メールの種類追加
あるときトラブルがあり、商品をお客さんに届けることができませんでした。
それゆえ返金対応をとることとなり、返金完了メールを送る必要がありました。
さて、あなたならこのサンプルコードをどのように修正しますか?
現状のコードと揃えるならばおそらく返金完了時のヘッダー・返金完了時の案内分・返金完了時のフッターという3つメソッドを新たに追加するでしょう。
このようにメールの種類追加がどんどん行われていってしまい、それに関するロジックもどんどん記述されてしまいます。そしていずれは 「神クラス」 と呼ばれるものになるでしょう。
機能の追加をすればするほど依存関係が複雑になり、変更容易性 が低下していってしまうのです。
もちろん数千行単位の神クラスになってしまうと 可読性 もかなり低下してしまっているでしょう。
仕様変更2:一部メールの文章修正
先ほどのような仕様変更を幾度も経て、すっかり神クラス的なコードになっているとしましょう。
そんなときに一部メールのヘッダーや案内文などの修正依頼がきました。
ですが依頼がきた時にはすでに可読性と変更容易性が低下してしまっている状態で、どこを修正すればバグを生み出さなくて済むのかをすぐには把握できなくなっています。
こんな簡単な依頼なのに、修正するのに何時間もかかってしまうことがあり得るかもしれません。
関心の分離という考え方
このコードはどうしてダメなの?というセクションで「メールに関する情報が集約されているので良いコードなんじゃない?」という文を太字で書いていました。
これが関心の分離を語る上でのキーポイントだと私は考えています。
情報が集約されている状態
まずもって、情報が集約されている状態は良い状態であると言えます。
ただし、「正しく」集約されている場合に限ります。
正しく集約されている状態って?
ここでいう「正しく集約されている状態」とは、関心事によって情報が分離されており、それが1つのクラスに集約されている状態を指します。
今回の悪いコードの例では、メールクラスという1つのクラスに 決済完了時・配送完了時・解約時 という3つの関心事がありました。
確かに「メール」という括りでは情報を分離できていますが、関心事による分離はできていません。
それゆえ仕様変更を重ねていくにつれて可読性と変更容易性がどんどん低下していくのです。
どうすれば関心事で分離できるの?
テクニックの一つとして、そのクラスの説明をしてみるというものがあります。
今回の悪いコードを説明してみます。
「このクラスは決済完了時と配送完了時、そして解約時のメールを組み立てるものです」
どうでしょうか?3つの関心事が見えてきませんでしたか?
もちろんこんな簡単に関心事を分離できることは普段の業務ではなかなか無いでしょう。
ですが、一見うまく情報を集約できてそうなクラスを、関心の分離という観点で見直して深く考えることでより良い設計にできると思います。
関心の分離を意識して悪い例をリファクタしてみる
では最後に関心の分離を意識して、悪い例をリファクタしてみましょう。
module Charge
class Email
def charge_header
# 決済完了時のヘッダー
end
def charge_guide
# 決済完了時の案内文
end
def charge_footer
# 決済完了時のフッター
end
end
end
module Delivery
class Email
def delivery_header
# 配送完了時のヘッダー
end
def delivery_guide
# 配送完了時の案内文
end
def delivery_footer
# 配送完了時のフッター
end
end
end
module Cancel
class Email
def cancel_header
# 解約時のヘッダー
end
def cancel_guide
# 解約時の案内文
end
def cancel_footer
# 解約時のフッター
end
end
end
同じEmailというクラスがありますが、それぞれを関心事ごとにmoduleを切っています。
この状態で悪い例と同じ仕様変更をしてみましょう。
仕様変更1:メールの種類追加
メールの種類が追加されても既存のEmailクラスに影響を与えることはありません。
なぜなら新たにmoduleを切り、そこに既存とは別のEmailクラスを作成するからです。
こうすることで既存クラスが膨らんで神クラスになることはないでしょう。
仕様変更2:一部メールの文章修正
先ほどのような仕様変更を幾度も経て、moduleごとにEmailクラスが数多く存在している状態でこのような文章修正依頼があったとしましょう。
この状態であればバグへの恐怖はほとんどありません。
なぜなら修正したいメールの種類がわかっているのであれば、それに対応するmodule内のEmailクラスを修正すればいいだけの話だからです。
こうすることで他のクラスへの影響はなく、安全にバグを生み出す事なく仕様変更できると思います。
関心の分離を意識すると可読性と変更容易性が上がる
リファクタ前とリファクタ後で同じ仕様変更をしましたが、リファクタ後の方が仕様変更をしやすいと感じられたかと思います。
関心の分離を意識する事で自分だけでなく全ての人が理解しやすく、そして安全に素早く修正できるようになります。
ぜひ皆さんもこの考え方を意識して設計してみましょう!