みなさん、してますでしょうか、モデリング。 今回は業務中に遭遇した、ちょっと複雑な構造のコードに、愛と勇気を持って立ち向かうために用いたことについてお話できればと思います。
業務システムでは証跡などの関係で、誰に対してメールを送ったかなどを逐一DBに記録している場合も多いかと思います。記録用のテーブル(user_mails)とActive RecordのUserMailモデルで表現できますね。「送信方法」や「送信タイミング」はいくつも存在するので。それらの処理を記述したモジュールをUserMailモデルにMixinします。さらに、複数のメールの送信先をまとめられるようにしたいので、別テーブルで送信先を管理するようにします。
というのが、下記のActive Recordモデルです。
class UserMail < ApplicationRecord include Concerns::UserMail::Auto include Concerns::UserMail::Manual include Concerns::UserMail::Delivery include Concerns::UserMail::WithoutSheet include Concerns::UserMail::MailerJob has_many :mail_targets
このActive Recordモデルにはいくつか問題があり、
- MixinしているConcernはこのモデル内にしかincludeされない
- メールの送信経路は複数あるのにもかかわらず、一つのモデルで表現しようとしている
- コードをのせる必要がないくらいにMailTargetモデルは振る舞いを持たない、いわゆるDTO的なつくりになっている。「ある人には送信できなかった」というのを表現できない。
- 送信処理中にメールの置換処理などを行うようになっている
ざっくりいうと、新しい送信経路を増やしにくく、送信経路を増やすために発生するもろもろの要求を受け入れるだけの設計になっていないといえましょう。
この設計的な問題を解決するためには、異なる振る舞いを持つ独立したモデル(オブジェクト)を組み合わせる、どのようにしてモデル(オブジェクト)を分離するかという着眼点が重要になります。この発想や考え方を少しだけご紹介できたらと思います。
関心の分離
ソフトウェア工学においては、プログラムを関心(何をしたいのか)毎に分離された構成要素で構築することである。 https://ja.wikipedia.org/wiki/%E9%96%A2%E5%BF%83%E3%81%AE%E5%88%86%E9%9B%A2
車が動作するためには車を整備するという操作があり、それは例えば「ガソリンを入れる」とか「タイヤを交換する」といった操作だったりします。こういった操作は概念的に類似した概念なのでひとまとめにしよう!というのが関心の分離の発想です。 例えば、Cのプロトタイプ宣言は実装と、実装のためのインターフェイスを分離しようという発想です。「UI」と「ロジック」など概念的に異なるものに、線を引くことに等しいです。
制御の反転 IoC
一方、制御を反転させたプログラミングでは、再利用可能なコードの側が、個別目的に特化したコードを制御する。 https://ja.wikipedia.org/wiki/%E5%88%B6%E5%BE%A1%E3%81%AE%E5%8F%8D%E8%BB%A2
一般的な発想では、車を考えた時に、よし車体を作って、色を考えて、最高速度はこれくらいで、というような発想でコードを書いていくとこを検討しますが、制御の反転では個々の部品(オブジェクト)ごとに動作可能なものを集めてソフトウェアを実現しようという考え方です。エンジンはこれくらいのスペックで(エンジンは単体で蒸しテストできる)、タイヤはこれくらいのスペックで(もちろんタイヤだけでころころ転がる)といった風に、単体でも動作するしテスト可能な部品を先に考えようというように考えます。部品が単体で完結しているので、そのままでも動くし、部品を組み合わせても動作するという非常に重要な考え方です。
冒頭で例示したコード中においてUserMailモデルが複数の送信経路を管理していることを考えると、まず複数のモデルに分離して単体のモデルで動作するようにすることが考えられます。同時にMailTargetがただ送信先を記録しているだけであることを考えると、複数モデルに分離する過程での責務をMailTargetに移動させようという発想になりますね。
例題
新型自動車開発開発を例に考えてみましょう。
あなたはA県にある大手自動車メーカーTのエンジニアです。今度新型のエンジンを搭載した電気自動車を設計することになりました。新型自動車の燃費を実験したいので、ソフトウェア上で燃費をシミュレーションできるようにしてください。
関心の分離では、「そうか燃費を調べたいんだからこの電気自動車の「走る動作」に関係することに着目して、共通化すればいいのかな。そうすればいろいろ実験条件を作るときに再利用できそうだし。」。
冒頭のコード例では、Deliveryという実際のメール送信に関するモジュールをMixinしていましたが、このモジュールはメール送信経路によらず必ず必要となるので、関心事としては共通にしておいて、異なるオブジェクトで再利用できそうです。
class MySuperElectricCar include ElectricRunningFunctions include MaintainanceFunctions end module ElectricRunningFunctions # OilRunningFunctions def run(road) # なんか処理 end def brake(road) end end module MaintainanceFunctions def charge_electricity(val) end def clean_up end end
制御の反転では、「車」「エンジン」「タイヤ」「道路」といった「個々」をあわせてシミュレーションしよう。個々がちゃんと動作することが確認できれば実験結果の確認も安心だ。メール送信や送信経路特有の処理を合わせて、ひとつの責務を果たそうという考え方が重要です。
class MySuperCar def initialize(engine, taire, road) @engine = engine @taire = taire @road = road end def run end end class Engine end class Taire end class Road end i_name_this_car_prius = MySuperElectricCar.new(Engine.new, Taire.new, Road.new)
ポイントとしては、関心の分離的な発想では、再利用可能なように設計しないと、なんか微妙な手続きが集まったテストしづらいモジュールを量産してしまう可能性をはらんでいる所にあります。制御の反転では、モデル(オブジェクト)単位で設計することの困難さはありますが、テスト可能でエンジニア間でのコミュニケーションもとりやすいリーダブルな部品を量産できるといえるでしょう。
冒頭で示したモデルをリファクタリングするのなら、送信経路によってモデルを分離して(STI)、メール送信に関する処理は個々のモデル内でincludeするというような方針が考えられますね。また、メール送信に関する処理でMailTargetモデル内で自己完結できるものは処理を移動することも検討できます。
関心の分離 | 制御の反転 | |
---|---|---|
コードの粒度 | 主にモジュール | 主にオブジェクト |
保守性(再利用性) | △ | ◎ |
開発速度 | ◎ | △ |
開発者間のコミュニケーション | ◯ | ◎ |
テスト可能性 | △ | ◎ |
絶望の仕様変更
そして、ある日上司から、「わが社は宇宙にも進出する事になったから、『宇宙でも走れる自動車』をつくってくれ。燃料はなんでもいい。」と言われました。関心の分離の発想ではどうなりますか。制御の反転の発想ではどうなりますか。