最近取り組もうと思っていること
ビジネスロジックのコードを解析することでこのようなことができるのではないか?
- このコードを実行するために必要なDB上のデータの範囲を特定する
- そこからDBへのクエリを自動生成できるのではないか
という疑問だ。
前提とするコード
ビジネスロジックの前段階で必要なデータを落としてくる
僕はビジネスロジック内でDBにアクセスするのではなく、その前にデータを落としてきておいて、そのデータを引数に取る事にしている。
// こうではない
func getUserSalesSum(req Request, db DB) Response {
sales := db.QuerySalesByUser(req.UserID)
return sum(sales)
}// こう
func onRequest(req Request, db DB) Response {
sales := db.QuerySalesByUser(req.UserID)
return getUserSalesSum(req, sales)
}
func getUserSalesSum(req Request, sales []Sale) Response {
return sum(sales)
}
これはgetUserSalesSumがスローテストにならないようにするためだ。
また、DBへのクエリが正しく書けたかということとこのビジネスロジックが正しく書けたかということは本来別問題なのに、同時にテストしなければいけなくなる。だからビジネスロジックに入る前に必要なデータは全部落としておくべきだ。
ビジネスロジックでも同じフィルターを書く
前述のこのコードは悪いコードの例である。よく「バッドコードの例」として紹介される。
func getUserSalesSum(req Request, sales []Sale) Response {
return sum(sales)
}
暗黙の前提が存在することについてだ。
このコードでsalesという値はあるユーザの購買履歴だけを含む配列ということになっている。しかし、このコードにそのメッセージ性がない。このコードを読んだ人は普通「このsalesはすべてのsaleが入った配列だな」と感じると思う。
そこで、いくつかの手を打つ。例えば引数名や関数名を工夫することがある。また、Assertを書くというのもただしい選択の一つだろう。しかし、僕はビジネスロジックにもフィルターを書くのがいいと思う。
func getUserSalesSum(req Request, sales []Sale) Response {
sales = filterUserSales(sales, req.UserID)
return sum(sales)
}
こうしたほうがビジネスロジックの1行1行が仕様と1:1で対応するようになるのでわかりやすい。
また、前者はこの配列が特定の条件でフィルターされていることに依存していたコードだが後者は必要なデータが入っていればそれ以外のデータが入っていても正常に動くコードになる。後者のほうがキャッシュなどとの相性が良くなる。
またクエリを書くことを手抜く余地が出る。例えば国コード一覧というのがあったとする。それは高々200個くらいの値しかないはずだ。そうであれば、毎回DBに取りに行くより手元のインメモリに持って置きたいかもしれない。そういう時に特別なコードを書かなくてよくなる。
DB上のすべてのテーブルを一つのオブジェクトツリーにまとめる
このコードの問題点の一つとして使うデータの種類が増えれば引数が多くなってしまう点がある。ユーザー情報や購買情報や位置データやあれこれ……
そこで、DBのテーブルを全部一つのオブジェクトツリーに含めてしまうといいと思う。
type Table struct {
Users []User
Products []Product
Sales []Sale
}
func getUserSalesSum(req Request, table Table) Response {
sales := filterUserSales(table.Sales, req.UserID)
return sum(sales)
}
さて、「ビジネスロジックの前段階で必要なデータを落としてくる」でビジネスロジックが参照透過性を手に入れた。そして、「ビジネスロジックでも同じフィルターを書く」でテーブルが特定の条件に依存するコードから。さらに一つのオブジェクトツリーにまとめることによって、テーブル全体の関数に変わった。
HTTPリクエストハンドラとは
ここで一つの結論に到達した。
HTTPハンドラとは、HTTPリクエストとDB上のすべてのデータに対してHTTPレスポンスを返す純粋関数である。
POSTメソッドなどのこともあるので、HTTPレスポンスとDB書き込みコマンドを返す関数であるということにしておく。
自動生成
おさらいだが、このビジネスロジックは引数でDB上のすべてのデータを受け取った前提で書いてある。しかし、実際に必要なのはその一部分である。その部分は必ず入っていなければいけないが、それ以外の部分にデータが入っていても入っていなくても動作は変わらない。逆に混ざっているとまずいデータというのはありえない。
たとえば上記のこのコードの場合だ。
type Table struct {
Users []User
Products []Product
Sales []Sale
}
func getUserSalesSum(req Request, table Table) Response {
sales := filterUserSales(table.Sales, req.UserID)
return sum(sales)
}func filterUser(sales []Sale, id interface{}) []Sale {
var results []Sale
for _, s := range sales {
if s.UserID == id {
results = append(results, s)
}
}
return results
}
このコードを見れば誰でもUsersとProductsにデータが入っていても入っていなくても同じ動きをすることはわかる。
また、Salesにreq.UserIDのすべてのSalesが入っていなければならないが、UserBさんやUserCさんのデータはあってもなくても同じということもわかる。
僕の問題意識は、「人間がこのコードを読めば必要なデータと不要なデータを判定できるならコンピューターに計算させることができないか?」である。
UsersとProductsが不要であるということはASTを見ていけば結構簡単にできそうだ。しかし、req.UserIDのSalesだけが必要だ、ということはどうすれば判定できるのだろうか。
今のところ、すべての「値」に対して「その値を作るのに必要な値」を挙げていき、それをツリー上にすればある程度可能かと思ったが自信がない。
誰か教えてくれ。