操作より状態・性質に着目する
この記事は、「KNOWLEDGE WORK Blog Sprint」第1日目の記事になります。
mayah です。株式会社ナレッジワークでは創業時より CTO をしています。開発ではプロダクト全体のアーキテクトをやっていることが多いです。普段からメンバーたちが書く Design Document やコードのレビューをする機会も多く、コードレビューだけでも月 200〜300 PR ぐらいは見ています。
さて、美しく堅牢なコードを書く上で、一握りのソフトウェアエンジニアは自然に習慣としていることで、多数のソフトウェアエンジニアは全く考えていない(もしかしたら一度も考えたことすらない)ように見えていることが1つあり、本日はその話をしたいと思います。
それは、操作よりも状態・性質に注目してコードを書くという習慣です。
ここでいう操作とは、インプットからアウトプットを計算したり、なんらかのオブジェクトなり環境 (RDB など) なりの状態を変更することをいいます。
イメージしにくければ、操作とは関数定義であると捉えてもらってもかまいません。関数を書くとき、「関数の中身を考えること」は「操作」に注目しており、「関数呼び出し前後に成り立っていることを期待する事前条件・事後条件を考えること」は「状態・性質」に注目していると捉えてもらえるとよいでしょう。
ユーザー登録を例に考える
簡単な例として、ユーザー登録の機能を考えます。ユーザーは次の RDB 上のテーブル users
に格納されているものとします。
CREATE TABLE users(
id UUID PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
deactivated BOOL NOT NULL
);
ユーザーは、id、ユーザー間でユニークなメールアドレス、また無効化フラグを持っています。簡単のためにこの話に無関係な列の定義は省略しています。無効化フラグの利用の是非については議論があるのは承知ですが、この話においては本質ではないので話が簡単になる形式での定義としました。
さて、あなたはユーザーを追加するという機能を作ることになりました。自分ならどのように実装するか、これまでであればどのように実装してきたかを考えてから残りを読んでください。
………
………
………
操作から考える
ある程度 RDB 操作になれていれば、「users
テーブルにデータを INSERT するものを作ればいいんだな」という考えが真っ先に浮かんだかもしれません。
はい、それはまさに「操作から考えている」ということです。
そう考えた方は多分、次に、無効化フラグが立っているユーザーと同じ email
が来たときはどうするのが仕様なんだ? ということを考えたと思います。そして、無効化フラグを FALSE
にして元のユーザーを同じ id で復活させるのか? 新しく id
を作って既存ユーザーの email
をランダム化したりユーザーを物理削除したりするのか? 登録は失敗するのか? などということも考えるでしょう。また、既存の有効なユーザーに同じ email
がすでに登録されていた場合はどうなるんだ? ということも考えるでしょう。
このように、まず一番ありそうなところから考えて、残りをオマケのように後から考えていく思考過程を通ることが多いです。
この考え方をとる人は、操作の前提となっている状態をその場しのぎで考えてしまう傾向があり、状態の変更について抜け・漏れを生じさせやすくなります。
さらに悪いのは、操作の後に取り得る状態のことを考えてないことです。この話の例だと適切な例が作りづらいのですが、ちょっとエキセントリックな例だと「無効化されたユーザーと同じ email
が登録されていた場合は、ユーザーが登録されないものとする」を仕様とし、ユーザーを登録したのにユーザーを登録していないという状態を作ってしまうようなことをしたりします。
このユーザー登録の例であれば、さすがにエキセントリックなのでそんなことする人はいないと思うかもしれません。でも、周辺の仕様を考えるのがめんどくさいからという理由で、 操作(コード)が簡単になるように 仕様を作った経験、ないですか? なんらかの操作の結果に「ただし書き」をつけたことはないですか? つまり、「状態を A に変更する。ただし、ユーザーが〜〜の場合には状態を変更しない」みたいに「ただし書き」をつけて状態を複雑にしたこと、ありませんか?
これらの仕様が求められている場合もあります。避けられないこともあります。しかし、避けられる「ただし書き」は可能な限り避けていますか? このような小さな「ただし書き」が積み重なると、コードを読む側は覚えておかなければいけないことが非常に多くなり脳のリソースを消費します。操作の記述で思考を通さずに楽をすればするほど、状態は複雑になります。多数の状態を気にしながらコードを書くことは人間には向いていません。
状態から考える
考える順番を変えてほしいのです。つまり、どう実装するかを考える前に操作の前後に取り得る状態をきちんと考え、特に操作後に取り得る状態を可能な限りシンプルにし、その考えを巡らせた後にはじめてどのような実装にするかを考える、という順番にしてほしいのです。
今回の例であれば、操作の前にまず次のことから考えます。
- 操作前の状態は次の3つである
- 該当するメールアドレスのユーザーは存在しない
- 該当するメールアドレスのユーザーが存在し、有効である
- 該当するメールアドレスのユーザーが存在し、無効である
- 操作後の状態は、実行が成功すれば次の1つである
- 該当するメールアドレスのユーザーが存在し、有効である
操作が成功すれば確実に有効なユーザーが作成されていることを保証し、それ以外のことが起こればすべてエラーである、という状態を保証します。
この機能では、状態は上のように定義したときに、操作をどう定義すべきか悩ましいポイントが1つ存在します。無効なユーザーのメールアドレスが渡されたらどうするか、です。これはプロダクトの仕様なのでプロダクトの仕様を決めてから操作の定義に戻ってきます。場合によっては、オプションを渡して複数の動作モードを指定できるようにする必要があるかもしれません。例えば通常はエラーとするが、あるオプションを渡すと「ユーザーを復活させる」ように動作を変更するようなオプションを作る必要があるかもしれません。そのようなオプションをいくつ作っても、「操作後の状態は、実行が成功すれば該当するメールアドレスのユーザーが存在し、有効である」という性質を崩してはいけません。
状態を制御するということは、セキュア・バイ・デザイン 安全なソフトウェア設計 などでも述べられているように、考え方としては一般的です。しかし、状態の制御を操作の制御より優先度高く考えようとしている人はそれほど多くないように見えています。コードレビューをしていると、考え方の順序がコードに如実に表れているのが見て取れます。操作から考えていると思われる人は、当初考え損ねた状態に陥ったときの埋め合わせのコード (特に if 文) を書き散らかし、コードが不必要に複雑になっているからです。
状態から考えた方がいいのは関数定義に限らない
この話は関数定義のような小さなものだけでなく、プロダクトの仕様定義のようなより大きな話にも敷衍することができます。仕様を考えるときに、機能の詳細(ある意味で「操作」)を先に考えていませんか? ユーザーが負に感じていること(ある意味で「事前条件」)、作ろうとする機能でなしとげたいこと(ある意味で「事後条件」)を先に考えていますか?
操作より状態を先に定義しようという話を受けて、そのためのやり方を先に考えていませんか? 「きちんと状態を定義できているか確認するにはどうしたらいいんだろう、TDD とか使えばいいのかな?」みたいに how から考えていませんか? そのやり方はこの文脈では「操作」から考えているのに等しいです。どういう状態になったら勝ちか? を先に考えてから、その状態に向かうためのやり方を考えるという順序で考えましょう。
さらに敷衍しながら少し違う角度からの言葉で述べると、この話は本来(高校や大学ぐらいの)学生のときに言われてきたであろう、問題構造を理解するという話の焼き直しといえます。問題が与えられたときに、いきなり回答に飛びつくのではなくまず問題の構造を考えよう、というような教えを受けませんでしたか。この話も、実装時にまず操作即ち回答に飛びつくのではなくて、操作によって満たしたい状態や性質即ち問題を理解するところから考えましょうと言っているだけに過ぎません。
まとめ
操作から考える癖をやめましょう。まず操作前後で成り立ってほしい状態や性質を考え、それからどのような操作にすればその状態や性質を満たすことができるのか、を考えます。そのような順序で考えることで美しく堅牢なコードになりやすい下地が整うことと思います。
KNOWLEDGE WORK Blog Sprint、明日9/2の執筆者はバックエンドエンジニアの 38tter です。お楽しみに!
Discussion