Clojureで堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計

これは何か

和田卓人さん(🦁)の「PHP7 で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計」を Clojure で再現してみた記録です。

こんなところに、こんなコードがありあました

(ns bug-repository.core
  (:require [clojure.java.jdbc :as jdbc]))

(defn find-all [spec params]
  (let [stmt "SELECT bug_id, summary, date_reported FROM bugs WHERE assigned_to = ? AND status = ?"]
    (jdbc/query spec [stmt (:assigned-to params) (:status params)])))

成功するとこんな感じのデータを返してくれます

({:bug_id 1, :summary foobar, :date_reported 2019-08-30})

さて、処理失敗の原因になる可能性が潜んでいるのはどの行?

・データベース接続確立失敗
・接続情報(spec)が nil、または `host` や `password` などのキー名が変更された
・テーブル名が誰かに変更された
・params が nil
・params のキー名や数の不一致
・params 値が文字列に変換不能
・途中でデータベース接続エラー

第一部:防御的プログラミング

関数を定義する際に :pre :post 条件をつけてチェックする

(defn find-all [spec params]
  {:pre [(comment "引数チェック")]
   :post [(comment "戻り値チェック")]}

第二部:攻撃的プログラミング

・アサーションが失敗した時無理せずエラーを吐く(空気を読む必要はない)
・Fail-fast

第三部:契約プログラミング

Clojure は core.spec を使って契約を表現できるので一般的は型よりも高度な運用ができる。たとえば JDBC に渡すための接続情報は JDBC 自身の spec を使って確認できる。そのほかローカルドメインの params はローカルで spec を定義できる。(clojure.java.jdbc.spec みたいに別ネームスペースとして定義すると使いやすい)

(ns bug-repository.core
  (:require [clojure.spec.alpha :as spec]
            [clojure.java.jdbc :as jdbc]
            [clojure.java.jdbc.spec :as jdbc-spec]))

(spec/def ::assigned-to string?)
(spec/def ::status #{"ok" "in-progress"})
(spec/def ::params (spec/keys :req-un [::assigned-to ::status]))

(defn find-all [spec params]
  {:pre [(and (spec/valid? ::jdbc-spec/db-spec spec)
              (spec/valid? ::params params))]
   :post [(seq? %)]}
  (let [stmt "SELECT bug_id, summary, date_reported FROM bugs WHERE assigned_to = ? AND status = ?"]
    (jdbc/query spec [stmt (:assigned-to params) (:status params)])))

まとめ

・core.spec を使って契約を表現しよう
・:pre :post などを使って契約のアサーションをしよう
・Fail-fast
・テストを書く際には core.spec を使ってテストデータを生成しよう
・あと一時的な接続エラーはConnectionPoolなどを使ってコネクション管理をしよう

この記事が気に入ったらサポートをしてみませんか?