SKYROCKETING WORK!

日常のエントロピーを上げてくぞ🚀

変化が激しく脆いドメインで技術的負債を増やさない設計

はじめに

ソフトウェア設計についてのポエム。
それも、ドメインが短期的なサイクルで大きく変化をしてしまうような領域での。
僕が携わる領域、つまりメディアサイトでのSEO施策を反映するシステムでの経験を念頭にしている。

結論から言えば、ビルド・アンド・スクラップを可能にしようという話です。

「変化の激しく脆いドメイン」とは?

問題の解決策のコアとなる部分が頻繁に変わるドメインのこと。
特にプロダクトのライフサイクルよりも早く変わるドメインのことを指したい。

僕が携わっている「Webメディア」というのも、この変化の激しく脆いドメインに当たると思う。
Webメディアにおける開発では、SEOやUXについて考慮することは避けることはできない。
しかし、SEOやUXは頻繁に変わるGoogleのアルゴリズムやユーザの嗜好によって正解が変わっていく
一年前に正解だったものが、次の年にはただの技術的負債になっている可能性すらあるからだ。

補足: ドメイン is 何?

少なくとも僕が表す文脈で言えば「問題領域」と訳されるもの。
以下の記事で「problem domain」と表されているものを指している。

オブジェクト指向分析 (OOA; object-oriented analysis) は、システム化の対象となる領域 (問題領域; problem domain) を対象とし、分析の対象となる問題領域に存在するさまざまな情報の概念モデル (conceptual model) を作ることを目標とする工程である。
オブジェクト指向分析設計 - Wikipedia』より

ドメインや責務の賞味期限を考えて切り分ける

設計をするとき、私たちはドメインに横たわる概念を責務として表現している。 *1
「決済」というドメインの中には、「買い物の合計額を計算する」「クレジットカード会社との取引を仲介してくれるサービスへ通信をする」などといった責務があるという具合に。

そうしたドメインや責務には賞味期限がある。
賞味期限が異なるモノを一緒にしてしまうと、本来は賞味期限が長かったモノまで賞味期限が 短くなってしまう。

変わりやすさを基準に概念に境界を引いていく

環境や状況というのは常に変化をするもので、それにともなってある時に問題だったものが問題ではなくなり、その解決策も解決策ではなくなってしまう。
時間だけでなく場所を変えるだけでも、問題が変化してしまうことさえある。
ましてや、解決策が技術的負債となって襲いかかってるくることも...。

例えば、和暦だ。
和暦は日本ローカルな紀年法で世界では使われていない。
日本で展開しているサービスで生年月日を表現する際に、その表現の解決策として和暦を使っていたとする。
そのサービスが運良く成功して海外展開をしようという時、その概念に依存して構築されたシステムは技術的負債になるかもしれない。

このような変化によって発生する技術的負債に対処するためには、概念の変わりやすさを基準にして設計を考えるといい。
ここで「変わりやすさ」を基準に、変わりやすいものを脆い、変わりにくいものを堅いとする。
ある概念を表現しようとするとき、概念を掘り下げて境界を引き脆い責務と堅い責務を切り分けましょうという話。

そうすることで、堅い責務を表現したモジュールを脆い責務の変化の影響から守れる。*2
つまり、脆い責務によって発生するシステム改修の影響を最小限にとどめることができる。

先にあげた例で説明すると「和暦は時間の表現方法の一種類に過ぎないので、時間を表す責務と切り分けて考えると良い」という話。

時間を表す責務は堅く、時間の記法である和暦の責務は脆い概念にあたる。
特に和暦は天皇の交代という短めのサイクルで変化が起こることがわかる脆い概念なので。

コードに落として考えるとしたら、時間class時間表現classを別々にすると良い、という感じだろうか。
とはいえ、時間表現は様々なバリエーションがあるコト自体がわかりきっているため、interfaceabstract class、あるいはダックタイピングの利用を検討すべきだと思う。
時間表現interfaceでアプリケーション内における時間表現の概念を定義して、それを実装する和暦表現classで計算して出力するという具合だ。

脆い概念を堅い概念に依存させる

そして、切り分けた2つの責務を組み合わせて元の概念の表現しようとするときは、脆い責務を堅い責務に依存させたほうがいい。
コード上の変更の影響は結合によって発生していて、その影響は依存関係によって各モジュールへと伝播していくので。

これも具体的に説明する。

時間という概念を表す時間class時間表現の1つである和暦表現classがいるとしよう。

Rubyの擬似コードで表すならば、以下のように設計できる。

# 時間の記法を表現する責務である
time = 時間.now
# 時間classはその代表値として西暦を使っているとする
puts time.year.to_s
#=> "2019年"

j_time = 和暦表現.new(time)
# 西暦から和暦に変換して出力する
puts j_time.year.to_s
#=> "平成31年"

# Rubyならば以下のようなメソッドを生やして型変換っぽくメソッドを追加するのもあり...かも?
# 時間#to_ja
#
# class Time
#   ...
#   def to_ja
#     和暦表現.new(self)
#   end
# end

汎用的な時間表現を西暦の時間classにもたせて、和暦表現が必要なタイミングのみに変換をするように設計する。
こうすることで、和暦表現classに新しい年号を追加するというイベントが発生しても、変更の影響範囲は和暦表現classとその利用箇所にだけとどまる。
時間classを利用している箇所には、変更の影響が及ばないはずだ。

時間class自身も、便宜上の理由で西暦を使っていて、時間表現でないと言ったら嘘になる。
とはいえ、時間を表現するためには何かで時間を代表させる必要があり、そこで西暦を選んだのは国や時間が変わっても使われているだろうという見込みが高いからだ。
和暦に依存する場合に比べて変更が発生する回数少ない堅い設計になるはずだ。

賞味期限の早いものは「捨てやすく」しよう

賞味期限が切れたモジュールは、開発の邪魔になりがちである。
いわゆる技術的負債となっていることが多い。

  • 無意味だけれど、なぜか動き続けているロジック
  • 一部の機能はほしいけれど、そこまで大きい必要がない過剰に高機能なclass
  • メディア側では見えていないのになぜか残っているコード
  • grepにたくさん引っかかる謎の動いていないコード
  • ...etc

こうした技術的負債は、回り回っていつか自分の足を撃ち抜くような事故に至る可能性がある。
また、こうした技術的負債によって、プロダクトのコード量が無駄に増え、変更の影響範囲が拡大し開発工数を増やすこともある。
そして、その存在自体が割れ窓理論的にコードの治安も悪くしがちなため、早急にご退場を願ったほうがよい。
割れ窓理論 - Wikipedia

とはいえ、そうした技術的負債も過去では金脈だった可能性もあり、様々な施策で利用されて巨大なモジュールと化し、簡単には捨てられない可能性がある。

技術的負債を捨てやすい設計にするためには、不要になる確率の高いモジュール(以下、脆いモジュール)の依存関係を最小限に抑える設計をすることだ。

脆いモジュールとは、変化の激しい領域で設計されたものであることが多い。
それは上で説明したような脆い概念の領域だ。

例えば、社内システムで職場のルールを実装する場合がここに当たるだろうか。
おそらく、ある年に作られたルールが、次の年には撤回されている可能性すらあるはずだ。

そして、脆いモジュールの依存関係を最小限に抑えるとは、「依存性逆転の原則」に従うことである。
依存性逆転の原則 - Wikipedia

というのも具体的な概念ほど脆い傾向があるため。
なので、具体的な概念に依存する必要がある場合は、それが変化することを前提にするとよい。
つまり、具体を直接使うのではなく、その抽象に依存するように設計するということ。

総務に備品の購入の申請をする社内システムがあるとしよう。
入力フォームで必要事項を記入し申請ボタンを押すと、備品の購入ルールに基づいたチェックが走ると考えてほしい。

そこで、ルールチェックclassと、個々のルールを表現した申請ごとの上限額ルールclass金額の参照元の入力ルールclassなどがあると想像してほしい。
このときに、備品購入ルールチェッカーclassは直にそれらのルールclassを実行するのではなく、それらのルールの抽象を抜き出してをinterfaceabstract classあるいはダックタイピングとして定義をして、それに依存するということだ。

Rubyの擬似コードで表すならば、以下のように設計できるだろうか。

# すべてのルールclassは、
# 抽象を抜き出すと「フォームの内容が正しいかどうかを返すという責務を負っている」とし、
# その結果を#valid?で返すというダックタイプと考えた。

class 備品購入ルールチェッカー
  def initialize(form)
    # RULESは下記のルールclassを配列にしたモノ
    # ルールclassを配列で取得できれば定数だろうがYAMLだろうがJSONだろうがなんでも良い
    @rules = RULES.map { |rule| rule.new(form) }
  end

  def valid?
    # すべてのルールの`#valid?`がtrueであれば、trueを返す
    @rules.all?(&:valid?)
  end
end

class 申請ごとの上限額ルール
  ...
  def valid?
    # 申請内の上限額が超えていないかをチェックする
    ...
  end
end

class 金額の参照元の入力ルール
  ...
  def valid?
    # 金額の参照元が正しく入力されているかをチェックする
    ...
  end
end
...

こうすることで、ある年にルールが増えたり消えたりしても、ルールclassを追加したり削除をしたりするだけで済む。
ルールclassを利用する側である備品購入ルールチェッカーclassのロジックを変更する必要はない。*3

最後に

この僕の考えは以下の書籍がベースになっている。
非常にオススメなので、本屋などでお手にとっていただければと思う。

*1:「そもそも責務ってなんだよ」という方にオススメの記事。 オブジェクト指向設計帖 巻之二

*2:モジュールという表現は、各プログラミング言語におけるclassやpackageなどのひとかたまりのコードを表している。

*3:ルールclassの配列の取得に定数を利用している場合は定数を修正する必要はあるけれど