Skip to Content

プリンシプルオブプログラミングを読んだ

プリンシプルオブプログラミングは多くのプログラミングの本から良いコードを書くためのエッセンスを抽出して、体系的に説明してくれている本で、実際に読んでみて、その辺の思想を一通り知りたい場合にはとても良い本だと感じた。

こういう系の知識は実際の現場で活用することが目的だが、それには問題に直面したときに素早く知識を引き出せるようにしておくことが重要だと思う。

そのために、今回は自分なりに本書の内容を整理してみた。整理するにあたっては、本書の「アーキテクチャ非機能要件」という項が、ソフトウェアがどうあるべきかという点をまとめていて個人的に分かりやすいと感じたのと、自分としてはこのような要件(目的)別に項目をまとめるのが最も理解が良いので、これを中心にまとめた。

アーキテクチャ非機能要件

非機能要件は機能面以外の全般についての要件のことで、開発や保守、運用、コンピュータリソースの効率活用などに大きな影響を及ぼす。

具体的に本書では以下にまとめられている

  • 変更容易性
  • 相互運用性
  • 効率性
  • 信頼性
  • テスト容易性
  • 再利用性

変更容易性

ソフトウェアが、どれだけ容易に改善できるか、という能力のこと。基本的に殆どのソフトウェアは最初のリリース後も要件の追加等で変更されていくので、とても重要な能力。

本書ではアーキテクチャ的な視点の性質だけをこの項目で取り上げているが、「コードの読みやすさ」という性質も変更容易性に多大な影響を与えると思うので、少し拡大解釈してまとめる。

変更容易性に関する原則

KISS(Keep It Simple, Stupid)

コードを書くときの最優先事項を単純性・簡潔性に置くこと。

YAGNI(You Aren’t Going to Need It)

コードは常に必要最小限のものを書くこと。なぜなら、将来を予測して余分に拡張性の高いコードなどを書いても、大抵の場合予測は外れて、それが不要になるから。

DRY(Don’t Repeat Yourself)

コードの重複をなくすこと。重複が多いとコードが読みづらく、修正もしづらい。モジュール化とも。

ただし、ここには議論があって、やみくもに似たようなコードをモジュールとしてまとめると問題が生じる場合がある。個人的に、モジュールとしてまとめられたときに困るのは、自由度が制限されることだと思う。例えば、似たようなコードの箇所をまとめてモジュール化した後に、達成したいことはほとんど同じだけれどそのモジュールでは対応できないコードが生じるケースというのはとてもたくさんあると思う。その際に、モジュールを少し拡張するだけで解決できればよいが、モジュールの設計的に対応が困難なケースもそこそこあるとは思っていて、その場合モジュールにかなり強引な実装を入れるかその部分だけフルスクラッチで実装するか、という感じになり、それは変更容易性という観点で見ると寧ろモジュール化しない場合より劣っているように思える。

もちろんケースバイケースだと思うが、モジュール化するべきか、しないべきか、については慎重に考えたいと感じる。

PIE(Program Intently and Expressively)

コードの意図が明確になるようにコードを書け、ということ。

ここでは、色々な効率よりも「読む効率」を優先しろ、ということが述べられている。

特に次の2点はそのとおりだと思った

  • 実行効率(プログラム実行時のパフォーマンス)より読む効率を優先する。なぜなら、読みやすければ後でパフォーマンスを上げるのは簡単なのと、本当にパフォーマンス向上が必要かどうかは実際に調査するまで分からないからである
  • コメントがなくても理解できるコードが理想だが、コードだけでは「なぜそれをしているか」というWhyは表現できないから、Whyは積極的にコメントで補うと良い

SLAP(Single Level of Abstraction Principle)

抽象レベルは同じまとまり内において統一するということ。

例で挙げられていた、コードを「書籍」のように書く、というのが分かりやすかった。書籍には確かに同じ章であれば抽象レベルがたいてい揃っているし、その抽象レベルが章・段落などで階層的になっているので読みやすい。

OCP(Open-Closed Principle)

コードは「拡張に対して開いていて」「修正に対して閉じている」べき、という原則。

前者は、コードは拡張しやすく在るべき、ということで、後者は、拡張しても外側のコードに影響を与えるべきではない、ということ。

モジュールの使用者である「クライアント」とモジュールの提供者である「サーバ」の間に「クライアントインターフェース」を設けるのが標準的な手法で、そうすることでサーバの変更にクライアントが振り回されなくなる。

バリエーション防護壁という言葉もあり、これはコードの不安定箇所はインターフェースで区切ってあげることでその箇所から外側へ影響が及ばないようにするということだが、この「防護壁」という考え方がしっくりきた。

Naming is important

プログラミングにおいて、命名は慎重に行うべきということ。

例えば、コードを読むとき、関数名を見ただけで処理内容がわからなければ、それを理解するために関数の内部まで深く読まなければいけなくなり、結果的に読む量が増大してしまう。

命名が読む効率に多大な影響をあたえることを理解する。

メカニズムとポリシーの分離

ポリシーとはそのソフトウェアの前提に依存する部分で、ビジネスロジックやユーザインターフェースが当たる。メカニズムとはそのソフトウェアの前提に依存しない部分で、描画処理のラスタ操作アルゴリズムなど、エンジン的な役割を果たす部分がそれに当たる。

頻繁に変化するポリシーとほとんど変化することのないメカニズムを分離することで、メカニズムを壊さずにポリシーを変更することができるし、メカニズムの再利用性にもつながる。

表現性の原則

データとロジックのどちらかを複雑にしなければいけないとしたら、データを複雑にするほうが読みやすい。

透明性・沈黙の原則

UNIX思想。デバッグしやすいようにデバッグメッセージは適切に表示する。一方で、デバッグメッセージで重要なメッセージが埋もれてしまってはいけないので、オプションでオン・オフできるようにして、普段は重要なメッセージだけを表示させる。

モジュール化に関する思想

変更容易性を達成するための主な手法がモジュール化であるが、どのようにモジュール化するかに関する思想や技法はたくさんあったので、ここにまとめてみた。

SRP(the Single Responsibility Principle)

単一責任の原則。モジュールを変更する理由は1つより多く存在してはならないという原則。

モジュール化の際は変更頻度・変更理由でグルーピングすることを意識する。ロジックとデータの一体化という思想もあるが、これも同じ理由。

関心の分離

関心(ソフトウェアの機能や目的)を分離してモジュール化することで、変更を容易にする。

最も代表的なパターンはMVCパターンで、MVCは「ビジネスロジック、ユーザへの表示、入力処理」で関心を分離し、変更容易性を向上している。

充足性、完全性、プリミティブ性

あるモジュールがコレクションを表現しているとき、それは以下を満たすべき

  • addだけでなくremoveを提供するべき(充足性)
  • 要素数を取得できるべき(完全性)
  • addに関してadd10は必要なく、addだけを実装すべき(純粋性)

モジュールの凝集度

モジュール内の要素が互いにどれだけ関係があるかを基準としてモジュールの強度を評価するもの。

評価が高いものだと、特定のデータ構造を扱う複数の機能を1つにまとめたモジュール(情報的強度)や、すべての命令が1つの機能を実行するために関連しあっているモジュール(機能的強度)がある。

モジュールの結合度

モジュール間が互いに疎な関係になっているかを基準としてモジュールの強度を評価するもの。

評価が高いものだと、データ構造体を受け渡しするモジュール(スタンプ結合)や、スカラ型のデータ要素だけをパラメータとして受け渡しするモジュール(データ結合)がある。

スタンプ結合はデータ構造体の一部しか使われない可能性がある分データ構造より評価は低いが、ケースバイケースでスタンプ結合を採用するのも良いっぽい。少なくとも、ある程度渡すパラメータが多ければ、読みやすさという点ではスタンプ結合は読みやすい気がする。

その他

  • ポリシーと実装の分離
  • インターフェースと実装の分離

相互運用性

ソフトウェアが、他のソフトウェアとやり取りできる能力のこと。

基本的には標準規格を選択することで相互運用性を上げることができる。

相互運用性に関する原則

シェルスクリプト活用

UNIX哲学の1つ。シェルスクリプトをソフトウェア同士を結びつけるためのグルー言語として用いる。

逆に言えば、ソフトウェアを複雑に設計するよりも、ソフトウェア自身はシンプルに設計しておいて、シェルスクリプトやコマンドを用いて他のソフトウェアと連携する手段を用意する方が筋が良いのかもしれない

対話インターフェース回避

UNIX哲学の1つ。ユーザの入力を求めるようなインターフェースは避ける。

なぜなら、ソフトウェア同士の対話ができなくなるから。確かに、yesコマンドを使わせるよりは、オプションで起動時に指定できる方が良さそう。

フィルタ化

UNIX哲学の1つ。ソフトウェアをフィルタとして設計すること。

フィルタとは、入力ストリームをデータとして受取、加工を施し、それを出力ストリームとして送り出すこと。標準入出力を使うと連携させやすい。

効率性

ソフトウェアが、実行に伴うリソース使用において、適切な性能を引き出す能力のこと。時間効率性(スループット、レスポンスタイム、ターンアラウンドタイムなど)と資源効率性(CPU、メモリ、ストレージ、ネットワーク使用量)がある。

リソースを無駄に使いすぎない一方で、適切なタイミングではリソースをふんだんに使えるように設計する

ただし、前述したが、効率性と可読性はトレードオフになっていることが多いので、多少の効率性を上げるために可読性を大きく損なうくらいであれば、可読性を優先する。一方で、効率性を計測し、ユーザにとってその箇所がボトルネックになっているということがはっきり分かれば、効率性を優先してもよさそう。

信頼性

ソフトウェアが、例外的な場面、予期しない方法や不正な方法で使用された場面においても、機能を維持する能力のこと。フォールトトレランス(障害時にも正常な動作を保ち続ける能力)とロバストネス(不正な使用方法や入力ミスからソフトウェアを保護する能力)がある。

フォールトトレランスはサービスの停止が許されないようなシステムにおいて求められる。一方で、ロバストネスはサービス継続よりもデータ保全が重要なシステムにおいて求められる。

信頼性に関する原則

修復の原則

UNIX思想。障害時には強くエラー通知して処理を停止する。ロバストネスを保証するもの。

安全原理

コードレビューの原理。書くべきか迷う例外処理(一見生じるとは思えない例外だが、生じた場合のリスクが大きい)があった場合は、安全サイドに倒してコードを書く。

防御的プログラミング

関数に不正なデータが渡されたときに、被害を受けないよう「防御的に」実装すること

ダーティルーム(人間の入力など)とクリーンルーム(内部モジュール)がバリケード(検証モジュール)で区切られるように意識する

ダーティルームでのエラーはアサーションで対応し、クリーンルームのエラーはコードのせいなのでエラー処理で対応する

契約による設計

呼び出し側は「関数を使う際の取り決め」を守り、呼び出される側は「事後条件を満たすこと」を守るということ。

事前条件はコメントでわかるようにし、守られなかった場合はアサーションを出す。事後条件を守れそうにない場合もアサーションを出す。関数側で引数を変換するなどして調整してはいけない。

テスト容易性

テスト容易性が高いとは、そのソフトウェアがテストによって漏れなく品質を検証できるような設計になっており、さらに、テストを実装する労力が少ないことを示す。

モジュール間の依存関係を排除し、疎結合にすることで、テスト容易性を上げることができる。

テストのないコードは一般的にレガシーコードと呼ばれる。

自分は、単純にテストを書くことで実装スピードが遅くなるのが嫌なので、普段テストをほとんど書かない。正直個人開発かつ売り物でなければそれでもいいかなと思っているが、仕事で複数人である程度大きな規模のシステムを開発すると、やっぱりちょっとした変更が他の箇所に影響を与えてしまうことは良くある。よって、実装コストを抑えつつ、テストによって品質を担保するために、テストをする箇所としない箇所の取捨選択をしたいと思っているが、他の方はどう取捨選択しているのだろうというのが少し気になる。

再利用性

ソフトウェアを、全体でも一部でも、別のソフトウェアの開発に再利用する能力のこと。

再利用性については、変更容易性と強い相関関係があると思う。例えば、コードが読みやすかったり、疎結合であったりすれば、そのコードを再利用するのも簡単になる。

全般

プロジェクトの進め方に関する話や用語など。

ボーイスカウトの原則

コードは以前よりも改良してからコミットすること。

自分も今やっているプロジェクトで「1プルリク1リファクタリング」的なことを自主的にやっているが、これはそれなりに効果があるので、今後もやっていきたい。

技術的負債

スケジュールを守るために一時的に技術的負債を抱えるのは仕方ないが、きちんと管理してその後に返すこと。

コメントの「TODO」は管理しているようで管理してないので、きちんとチケット化するのが良さそう。

曳光弾

曳光弾とは飛んだ軌跡がわかるように発光する弾丸のこと。優先的に検証したい部分を先行的に実装すること。骨格コード。これを実装した後にそれのフィードバックなどを取り込みながら肉付けしていく。

プロトタイプとの違いとしては、プロトタイプは「コンセプトを検証するため」であり、検証後は作成されたものをすべて捨て去って次は正しい姿で再構築する。

コンテキストスイッチ

プログラマがフロー状態に入るには15分以上の精神集中時間が必要と言われており、そういう意味でコンテキストスイッチが頻繁に行われる環境は非常に効率が悪い。

コンテキストスイッチを引き起こす最大の原因はチャットや口頭でのコミュニケーションだと思うが、仕事をするためにはコミュニケーションが必要なので悩ましい。コミュニケーションによってフロー状態をできるだけ損なわないために工夫しているチーム・人もいるようで、例えば、口頭でのコミュニケーションの前にチャットで確認するというフローを作っているチームがあったり、あるいは作業時間はカレンダーに明確にスケジュールとして登録して、作業時間中はチャットをできるだけ見ないなどしている人がいたりした。

ブルックスの法則

工数について。人と月は交換不可能。人を増やしてもスケジュールは前倒しできない

コンウェイの法則

アーキテクチャは組織に従うということ。例えば、3チームいれば、3要素に分かれたアーキテクチャになる

よって、アーキテクチャをまずは考え、それに合わせて組織を分けるようにする

ジョシュアツリーの法則

物事や概念の名前を知ることではじめてそれを認知できるようになること。

チームの共通言語、ユビキタス言語を定義し、使用することが大事。

車輪の再発明

すでにあるものを作ること。

原因として、車輪を知らなかったり、プログラマが自分で車輪を作りたいというエゴを出したために生じることが多い。

防ぐ手段として、コードを書く前に、同じ機能のライブラリがないか必ずチェックすることが大事だが、ビジネス目的(ソフトウェアのアピール要素はコントローラブルである必要があるので自前で作るべき)や学習目的でなら車輪を再発明することが必要。

ヤクの毛刈り(Yak Shaving)

ある問題解決を行おうとした際に芋づる式に別の問題が生じ、本来の問題解決になかなか至らないこと。

ロゼッタストーン

将来の保守担当者に対する簡潔な手引書。ソフトウェアの開発環境とアーキテクチャを理解するための情報を記述する。

ドッグフーディング

自身の開発したものについて自ら使用するようにすること

感想

普段からある程度意識していたり、無意識的にやっていることであっても、改めて原則としてキーワードとともに定義されると、頭のなかで強く意識できるようになったので(まさにジョシュアツリーの法則)、言語化するのは大事だなと感じた。

あとやっぱり思ったのは変更容易性を目的とした原則が多いこと。多すぎて、結局整理になってないので、なんとかしたい。