yojikのlog

yojikのブログです

業務アプリには関数と構造体だけあればいい件について

たけぞうさんやひがさんの古いエントリをよんでいまさら考えてみました。

関数と構造体しか無い、いわゆる構造化設計*1だと、上位の関数が下位の関数に処理を委譲して・・・みたいな感じで、機能のツリー構造が出来上がる。このような構造では必然的に、上位レベルの関数が下位レベルの関数(のインタフェース)に依存することになると思う。上位の関数が、下位の関数の面倒見る*2から。(追記: 厳密には関数を持つモジュールのツリー構造が出来あがる)
ならば下位の関数のインタフェースをカッチリ決めれ! といわれそうですが、なんだかんだで下位の関数こそ変更されやすいのが業務アプリの現実なはず。結果、頻繁に発生する下位レベルの変更が上位にまで伝播していくわけだから、システム全体としては変更に弱くなってしまう。
ちなみに、これはDIみたいな技術では解決できない。だって問題はインタフェースだから、実装部分だけプラガブルにしても意味無い。

以下に簡単な例を示してみる*3。イメージとしては、画面で特定の条件を入力すると、一致したアドレスにメールを送りつけるスパム販促ソフトです。

  • なんか画面で特定の条件(構造体Input)を組み立ててsendMail関数を呼び出す
  • sendmail関数ではそこから画面情報であるInputから検索につかう情報を取り出してgetAddress関数を呼び出す
  • さらに帰ってきたアドレスのリストをsend関数に引き渡す(ここで実際にメールが送られると仮定)
//以下謎言語でお届けします。
class Input {
  hoge1;
  hoge2;
}
function sendMail(input) {
   adresses =  getAdressList(input.hoge1, input.hoge2);
   for(i : addresses) {
     send(i);
   }
}
function getAdressList(hoge1, hoge2) {
  //アドレス検索して返す
}

ここで、依存関係的には下位にあたるgetAddressリストの引数の仕様とかが変わったら、それを呼び出しているsendMailや構造体Inputを作成しているところなど、呼び出し元にどんどん変更の影響が伝播していく。

個人的には、このような問題を解決するために「検索条件」自体をCommandオブジェクトとして、sendMailに引き渡す設計をすることが多い。さらに「検索条件」自体に検索する機能を持たせる。なぜならば、検索条件と検索ロジックは一心同体なのだから、一緒のオブジェクトにしてしまえ!ということです。そうすることによって変更の影響を閉じ込めることができる。テスタビリティも高くなってる感じだし、実にOOっぽい話だ。*4

class FindAddressCommand { 
   hoge1;
   hoge2;
   function  find() {
      //アドレス検索して返す
   }
}
function sendMail(f:FindAddressCommand) {
    addresses = f.find();
    for(i: addresses) {
       send(i);
    }
}

もちろん上記は一例で、もっと別の解法があるかもしれない。
ここで注意したいのは、僕は「Entityにロジックを持たせて問題が解決する」と思っているわけではない事。ワークステート管理(状態マシーンな)ロジックとか、単体に閉じたバリデーション以外は、Entityに持たせるべきじゃない。*5
そもそも、そんな私立オブ小な人はもうあんまりいないでしょう。OOマニアックな人が多いと思われる弊社でもそんな人いません。
でも、ここに出てきたFindAddressCommandは、EntityオブジェクトじゃないけどDomainオブジェクトだと思う。「検索条件」はEntityじゃないけど重要な概念で、こういうのが出てくるのがOO分析やったりする醍醐味だから。結構見落としがちだけど、このような(Entityじゃない)Domainオブジェクトが活躍して、システムの構造が安定するのはOKじゃないかなーと思う。
実際にはOO分析マジメにやる時間も無かったりするので、結局関数と構造体モデルでいくことが多いでしょう。仕様等が変更される範囲が見切れてて、瞬殺できそうな案件ならばそれでもよいと思う。

まとめ
  • 構造化設計には重大な弱点がある
  • OOはそれを補うかもね
  • でも、DomainModelはEntityにロジック持たせるということだけじゃない。むしろレアケース
  • Entity以外にDomainオブジェクトってあるし、そういうのがロジック持つパターンの方が多いかも
  • Commandパターン大好き*6
  • 一方Lisperはクロージャを使った

*1:追記:いわゆる、なんちゃってSA/SD

*2:引数を渡して結果を受け取らなくてはいけない

*3:恣意的な例なので細かいところは本当にごめんなさい。わざとウソ言語で書いてます

*4:実は「検索条件」組み立てるところも影響あるのだけど、これは必然だよね

*5:その意味では伝票の状態遷移ロジックとかはEntityに持たせてもいいかも

*6:引数/返り値と処理が一体化した、基本のやつ限定