2016年11月3日にPHPカンファレンス2016が開催されました。本稿では、ゲストスピーカーである和田卓人さんによる講演「PHP7で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計」についてレポートします。
PHP7では例外や表明の機能が大幅に見直され、強化されました。この講演では、例外処理を設計する際の基本的な考え方や、表明(assertion)の使い方、そして表明と例外を使い分け、堅牢なコードに導くための設計手法「契約による設計(Design by Contract)」の考え方を説明しました。
導入
はじめに、和田さん自身が監訳に関わった『SQLアンチパターン』に掲載されているコードを、よりひどくさせた次のコードを示しました。このコード自体は検索を行うものなのですが、いくつか問題があると言います。どこに問題があるかを会場に問いかけました。
和田さんは、関数の中身はたった6行にも関わらず、そのうち4行にバグが起こる可能性があり、そのバグの状況としては次の8つが考えられると指摘しました。
- データーベース接続確立失敗(誰かがマイグレートを実行している等の考慮がない)
usr
、passwd
等キー名が変更された
- テーブル名やカラム名が誰かに変更された
$params
がnull
$params
のキー名や数の不一致(値がおかしい可能性)
$params
の値が文字列に変換不能(暗黙のキャストに失敗する可能性)
- Bugクラスが未定義(Bugクラスに配列をマッピングしているが、そもそも配列がない場合の考慮がない)
- 途中でデーターベース接続エラー
この中で一番手強いと感じるバグとして、「$params
のキー名や数の不一致」を挙げました。理由としては、裏側でWarningが出ているのにも関わらず、特にエラーで落ちたりせず、表面上の検索結果としては1件も引っかからなかった場合と同じように見えるからだそうです。つまり、正常系と見分けがつかないのが問題だと言います。この場合、和田さんは「不具合の発見が遅れるので、傷が深くなる可能性がある」と指摘しました。
そして、ソフトウェア開発の進み具合とともに「不具合の発見が遅れれば遅れるほど傷は深くなる」ことを図示しました。和田さんは「とにかくバグを早く見つけることがもっともコストを下げる方法であり、開発の後半になればなるほど関係箇所が増えて面倒になる」と述べました。
さらに、ソフトウェア開発者のマイケル・ジャクソンさんの言葉を引用し、「賢明なソフトウェア技術者になるための第一歩は動くプログラムを書くことと正しいプログラムを適切に作成することの違いを認識すること」であると話しました。また、氷山の断面図のスライドを示して、「堅牢なコードは、50%以上がエラーハンドリング」と言及し、「大事なのは氷山の下のほうであり、実際に動くコードではない」と述べました。
以上がこの講演の導入部です。バグの早期発見のためにどのようなプログラミングをおこなえば良いのか、和田さんは「予防的プログラミング」「攻撃的プログラミング」「契約プログラミング」に分けて説明していきました。
予防的プログラミング
先に示した一連のコードに内包されるバグは、データーベースの接続確立失敗はインフラの問題を表し、usr
やpasswd
のキーが変更されるのはバグであるし、テーブル名やカラム名が誰かに変更されるのはコミュニケーションの問題な気もする、とコメントした後に、まずは次の3つの心配事について考えてみると話しました。
$params
がnull
$params
のキー名や数の不一致がある
$params
の値が文字列に変換できない
そこで取り上げるのが予防的プログラミングです。これは、対処よりも予防のほうが低コストなのだから、間違ってからどうにかしようとするのではなく、そもそも間違わないようにしようというプログラミングの方法だと言います。
予防的プログラミングに近いものとして防御的プログラミングがあります。これは端的に言えば「かもしれない運転」であり、「決めつけを行わない」プログラミングの方法です。「誰も信じない。自分は自分で信じる」というのは、大事なプラクティスですが、誤解も多くあると指摘しました。典型的な誤解として、次の例を挙げました。
- 徹底的に値を確認する
- おかしな値が来たけど、なんとかゴニョゴニョいいようにする
- パラメーターの中身をコメントで書きまくる
和田さんは「これは問題を解決していることにはならない」と言います。「防御的プログラミングとは悪いコードに絆創膏をあてることではない」とし、防御的プログラミングはいろいろな人が様々な解釈をしているが、和田さんが好きな解釈としては「問題発生を事前に防ごうというコーディングスタイルのことである」と述べました。
そして、次のような良いプラクティスの積み重ねこそが、防御的プログラミングであるとも話しました。
- 可読性の高いコードと適切な命名規則
- すべての関数の戻り値をチェック
- デザインパターンの採用
型の制限
ここで『プログラマが知るべき97のこと』から「正しい使い方を簡単に、誤った使い方を困難に」を引用し、その中で「誤った使い方をすることが困難」に注目するとして、型を取り上げました。
例えば、型をひたすらチェックするくらいであれば、タイプヒンティングを利用しようと言います。その際、見知らぬ人ともうまくやるには「『出来てはならぬことを禁じる』のではなく、はじめから『出来ていいことだけを出来るようにする』と考える』(『プログラマが知るべき97のこと』)という言葉を引用していました。
防御的プログラミングの例として、そう変更する前と後のコードを示し、「入り口をガバガバにしていたのでいろいろチェックが必要だったが、そもそも受ける型を作っておけばここまでソースは減る」と述べました。やり方としては入ってくる値の種類を制限しようという話だとし、列挙型(Enum)を紹介しました。
「問題領域の知識を活用して固有の型を作ることで、取り得る組み合わせを大幅に減らせる」(『プログラマが知るべき97のこと』)ことを、和田さんはコードを指し示して説明しました。ステータスの文字列はOPEM
, NEW
, FIXED
のたった3種類であること、そもそもString
やint
を使う必要がないことを示しました。
これで列挙型ができるため、次のようなコードで利用できるとのことです。
以上により、$params
に関する心配事に対応できるとしました。
多すぎる責務に起因する問題
次に、知りすぎ、多すぎる責務に起因する次の処理失敗の問題を取り上げました。
- データーベース接続確立失敗
usr
, passwd
等キー名が変更された
この部分のコードは検索を行うコードです。データーベースに接続しに行くことは、責務ではないとし、やらなくても良い責務を減らす必要があると指摘しました。「知りすぎない。やりすぎない」が大切とのことです。
そしてPDOを作るところまでは外で行い、コンストラクタで受け取るようにコードを変更することで、グローバル変数の抹殺にも成功しました。
予防的プログラミングの話のまとめとしては「予防にまさる防御なし」と締めくくりました。また、大量のコードがかなり小さくなった(44行もあったコードが15行になった)ことを示しつつ、「防御的プログラミングは、考えなければならない条件を少なくすることが重要」と述べました。
攻撃的プログラミング
攻撃的プログラミングの「攻撃的」とは、「なにかおかしいことが起きたら、例外を発生させたりエラーを発生させて即時落とす。速やかに停止させる。無理やり動作させても、どうせその先で落ちるならさっさと停止させる。これで傷が浅くなる。障害を抱えて中途半端に動いているプログラムよりも死んだプログラムのほうがいい」と紹介しました。
「そんなにかんたんにシステムを落としていいのか?」という懸念については「アーキテクチャとプログラミングスタイルは別である」だとし、正当性と堅牢性を次のように説明しました。
- 正当性とは、不正確なことを出すくらいなら死ぬこと
- 堅牢性とは、ほどほどに死なないこと
和田さんは「個々のクラスは正当性を重視し、穏当なエラー画面等に変えるのはフレームワークとか外側のレイヤーですべきである」と述べ、システム全体としては堅牢性を重視し、個別としては正当性を重視する考え方が望ましいと話しました。
表明プログラミング
正当性の話として、ここで「Bugクラスが未定義」という心配事を取り上げました。この問題は、表明プログラミングで解決できると指摘しました。「起こるはずがない」と思っているのであれば、表明しようということだと言います。
PHP7ではassert
関数を使って書けます。特にPHP7では、文字列を渡す形ではなく、評価式を渡すことができる完全なものになったと言います。思い込みが思い込みの通りであることを確かめる……つまり「Bugクラスがある」と考えているなら、assert(class_exists("Bug"));
と書こうと話しました。
しかし、このようにassert
を書いて実行してもWarningしか出ないし、落ちないと言及しました。これはPHP7のassert
の標準設定でassert.Exceptionが0
となっているため、警告を出すのに留まっていると指摘しました。この設定は「今まで動いていたシステムを落とさないようにするためだと考えられる」と述べていました。ただ今回は自殺してほしいので、assertException
を1
にして、表明違反で落とすように設定します。
なお、表明のメリットは大きく分けて、「コミュニケーション」と「デバッグツール」としての観点があると言います。コミュニケーションについては、書き手がコードの読み手に情報を与えられるという話で、バグをその原因に近いところで発見しやすくなります。デバッグツールについては、テストコードを書いているのであればさほど重要ではなく、コミュニケーションメリットのほうが大きいと説明しました。
さらに本来のエラー処理に表明を使うのではなく、あくまでも暗黙の前提を明らかにするために使うべきであると注意しました。和田さんは「エラーの発生が予測できる時は、エラー処理のコードを使いましょう」と述べました。
「表明を書き過ぎたら、コードがどんどん遅くなるのではないか?」という疑問については、PHP7のassert
は単なる関数ではなく言語構造なので、実行段階ではオフにできると紹介しました。
- zend_assertions:1(コードが生成される)
- zend_assertions:0(コードを生成するが、実行時には読み飛ばす)
- zend_assertions:-1(コードが生成されない)
zend_assertions
を-1
に設定すればノーコストになるとのことです。ただし、オフにされることを意識してコードを書く必要があると注意しました。例えばassert(end($users));
のようなコードを書くと、このコードの前後で内部ポインタの位置がずれてしまうため、このような表明を書いてはいけないと指摘しました。
以上により、「Bugクラスが未定義」という心配事に対応できました。
エラーハンドリング
「途中でデーターベース接続エラー」という心配事については、「エラーは無視しても何も良いことはない。不安定なコードにもなる。だから戻り値を使うようにして、エラーハンドリングをしたほうが良い」(『プログラマが知るべき97のこと』)と説明しました。
戻り値を使うとfalse
のチェックができ、エラーを処理できますが、コードは増えます。例として9個も多重ネストをしているエラーハンドリングのコードを示しました(インデントの形状から波動拳コードとして紹介しました)。この深いネストのコードは特定の条件をチェックしてfalse
ならばすぐ落とすといったコードを書くことで、発生を防止できると紹介しました。
しかし、たくさんチェックしているコードは堅牢ですが、肥大化しがちで無視されやすいのが問題だと話しました。
例えば、PDOでエラーになった時の処理は、わりと読み飛ばされて無視されやすく、テスト中は見つからずに問題が発覚した時に調べることになると言います。和田さんは「気がつきにくさは問題を深くさせる」と述べました。
この対策としては、PDOを例外モードにする(PDO::ERRMODE_EXCEPTION
)ことです。例外を発生させた結果として、false
の代わりにPDOException
が発生するようになって無視できず、わかりやすいコードになると説明しました。
責務を移譲(今回の例では、メソッドの返り値から例外へと責務を移譲)したら、その暗黙の前提をassert
に書くようにします。和田さんは、「assert
ならば、無視できない方法でエラー状態を知らせることができる。エラーを握りつぶすことは可能だが、そういうコードは見つけやすい。握りつぶしには根拠が必要であり、理由もなく握りつぶすのは書き手の姿勢に問題があるとすぐにわかる。超攻撃的スタイル」と述べました。
契約プログラミング
最後の心配事は「テーブル名やカラム名が誰かに変更された」です。和田さんはこの問題に対応するために、誰の責務かをはっきりさせる「契約による設計」を取り上げました。
契約による設計は、『オブジェクト指向入門 第2版 原則・コンセプト』に解説されています。ソフトウェアモジュールの権利と責任を文章化(そして承諾)し、プログラムの正しさを保証するための完結かつパワフルな技法とのことです。
正しさの公式は、数学的に{P}A{Q}
のように表現されます。事前条件P
が成り立つ時にプログラムA
を実行し、その実行後には必ず事後条件Q
が成り立つならば、プログラムA
は正しいという表現だそうです。
和田さんは「コードを呼び出す人と、呼び出される人との間での取り決めがあれば約束を守る」ことであると説明しました。そして、次のフローチャートを示し、「常に失敗するかどうかで、メソッドが動かなかった時にバグなのか、例外的状況なのかを切り分けるようになる」と指摘しました。
契約による設計を行った時、実行時の表明違反はそのソフトウェアにバグがある証拠であり、「事前条件違反は呼び出し側にバグがあり、事後条件違反は供給者側にバグがある」と言及。和田さんは「表明によって、呼び出し側か供給側のバグなのか見分けることができる」と述べました。
さらに、PHP7の例外のツリーを紹介しました。catch Exception
を書くだけではfalse
を返すに等しいので、ツリーの適切な例外を返すようにすべきだと言います。Error
とLogicException
はバグで、この2つは供給者が必ず直す必要があります。RuntimeException
(例外)は時折起きるため、供給者が直す必要があるかもしれないし、ないかもしれないとしました。
例えばPDOException
が発生した場合、さらに分解してあげると誰のバグなのかわかるようになるとも指摘しました。LoginException
ならば自分のバグであり、assert
を出すようにしておけば自分のバグであると気がつけるようになると話しました。和田さんは「これで事後条件を守ることができる」と述べました。
まとめ
最後に、和田さんは次のようにまとめて、講演を締めくくりました。
- 予防的プログラミング「予防にまさる防御なし。ガバガバにするな」
- 攻撃的プログラミング「Fail fast。障害を抱えて中途半端に動いているプログラムよりも死んだプログラムのほうがダメージは少ない。バグは死んだところに近いところにあったほうがいい」
- 契約プログラミング「バグと例外を区別し、さらに誰の責任かも見分けられるようにする。俺のバグとお前のバグ」