はじめに
これまでもいくつも設計に関する記事を書いてきました。
今回は設計が大事なのはわかったけど何から始めればいいの?という方向けに、設計への入門としてのアプローチをまとめてみました。
初学者のときに知っておきたかったことを中心にまとめているので、設計に興味がある方はぜひ参考にしてみてください。
ここでの設計とは、ソフトウェアの設計、特にコードを書くうえでのソフトウェア構造の設計を指します。
良いソフトウェアとは
まずは設計を学ぶ上での目標を明確にしましょう。
設計の目標は良いソフトウェアを作ることですが、良いソフトウェアとはどのようなものでしょうか?
もう少し具体化してみましょう。
良いソフトウェアとは、以下のような特徴を持つものだと言われます。
- 可読性
- 保守性
- 効率性
- 信頼性
- 拡張性
一つ一つ見ていきましょう。
可読性
ソフトウェアの可読性は、コードがどれだけ理解しやすいかを示す指標です。
他の開発者が容易にコードを理解し、修正や改善がしやすい状態にあるコードは可読性が高いと言えます。
可読性が高いコードは、仕様の誤解によるバグの発生を抑制し、将来的な機能拡充のコストを低減することができます。
コンピュータ・プログラミングにおける可読性(readability)とは、プログラムのソースコードを人間が読んだときの、その目的や処理の流れの理解しやすさを指している。
引用元: 可読性 - Wikipedia
ソフトウェアに関するプラクティスのうち、可読性に関わるものは非常に多く存在します。一般にコーディング規約と呼ばれるものはほとんどが可読性向上を目的としています。
また、ソフトウェアエンジニアにとってのバイブルとも言えるリーダブルコード - O’Reilly Japanも可読性向上のための書籍です。
それくらいソフトウェアエンジニアにとって可読性は重要な要素です。
他にも意識するポイントがいくつもあるので、可読性を高めることが目的なんだなと理解しながら、内容を確認していきましょう。
命名の重要性
まずは変数やクラス、メソッドなどの名前に適切な名前をつけることから始めましょう。
ソフトウェアというのは基本的に変数の名前やメソッドの名前は短くても動作しますが、多くのエンジニアは適切に処理を表す名前をつけたほうが良いと考えており、プログラミングにおいて最も重要な要素の一つだと言えるでしょう。
命名においてのポイント
- 名前が表す内容が明確であること
- できるだけ省略せずに名前をつける
- コードを多少早く書くことより、コードを読む人が間違わないことのほうがコストが低く、現在はIDEによる補完機能もある
- 使われる箇所が広ければ広いほど誤解を招く可能性が高い
- 逆に言えば、限られたスコープでしか使わない変数は慣例的に短い命名をすることもある(for文のカウンタ変数など)
- 品詞を意識する
- 変数名やクラス名には名詞、メソッドには動詞を使うことでより理解しやすくなる
- 抽象的な単語や情報量のない単語を利用しない
- 一時的を表す変数名に
tmp
を使うのは避ける 〇〇info
や〇〇data
など、抽象度の高い、情報量のない単語を使わない(プログラミングに使う変数は全部info
やdata
です。)
- 一時的を表す変数名に
- 対になる変数には名前に意味を持たせる
start
とend
など、関連する変数には関連性を持たせる- ソフトウェアの世界には驚き最小の原則 - Wikipediaと呼ばれる原則がある
- できる限り読んだ人が存在を予測できるように作るべきだという原則。
美しさ
美しさとは、コードの見た目の美しさを指します。やや抽象的ですが、コードのインデントや改行、コメントの使い方などが美しさに関わります。
コーディング規約でもこれらのルールを中心に記述されていることが多いです。
美しさに関しては、人それぞれ感じ方が異なる部分もありますが、基本的に意識するべきポイントは下記のようなものがあります。
- コードの改行位置やインデントを統一する
- 関連する処理をまとめてブロックにする
美しさに関する内容はLinterやフォーマッターを導入することで自動化しやすいものなので、できるだけ早めにこれらを整備しておくと良いでしょう。
コメント
コメントはコードの可読性を向上させるための重要な要素です。
よくHowを説明するコードよりもWhyを説明するコメントを書くべきと言われます。
コメントの内容についてはよく議論のポイントになることもありますが、基本的には以下のような内容をコメントに書くことが多いです。
- コードを読めばわかることを書かない
- 人間はコードよりもコメントに目が止まりやすく、コメントには読ませたい情報を書くようにする
- 変数に対して説明的なコメントを書きたくなった場合、変数の命名が悪いことが多いので、命名を見直す
- 自分の考えを書く
- 実装したときに他に考えた選択肢と現在のコードになった理由を書く
- コードの欠陥に対するメモとしてコメントを書く。
- たとえば、よく使われるコメントとして、
TODO
やFIXME
、HACK
などがある
- たとえば、よく使われるコメントとして、
- コードの背景を書く
- コードの背景になる情報を書く
- 読み手がコードを読んだときに驚く、気になる箇所を想像してコメントを書く
不変性
ここで、改めてコードが読めないという事象について考えてみましょう。
その読めないコード
はどこから読めなくなるのでしょうか?
私自身の経験から言えば、コードが読めなくなる原因の一つに、脳内メモリの容量不足があります。
登場する変数の多さ、変数の中身の書き換えの多さ、関数呼び出しのネスト関係、複雑なif文の条件分岐など、コードの複雑さが増すと、脳内メモリの容量が足りなくなり、コードが読めなくなることがあります。
1行1行のコードは読めても、その内容を覚えながら処理を追うことが難しくなります。
この現象に関連するプラクティスとして、不変性があります。
不変性とは、文字通り変更されないことを指します。
変数の値をできるだけ変更せず、代入したい場合には適した名前をつけた別の変数を用意することで、変数の命名が中身の記憶を助けてくれます。
また、オブジェクトにおいてもfinal
等の修飾子を使って変更を禁止することで、オブジェクトの状態が変わらないことを保証することができる言語が多くあります。
できる限り不変性を保つことで、コードを読むにあたって記憶することが減り、コードの見通しが良くなります。
保守性
保守性とは、プログラムが将来的にどれだけ簡単に変更や拡張ができるかを示す指標です。
言い換えれば、機能追加やバグ修正が容易に行えるかどうかを示します。
保守性が高いコードは、変更に対して柔軟に対応できるため、長期的な開発においてコストを抑えることができます。変更にかかる時間が短縮できるということは、市場や技術の変化にも迅速に対応できるようになり、これはソフトウェアとしての競争力に直結します。
保守性を高めるための方法の一つに、コードのモジュール化があります。
モジュール化とは、コードを機能ごとに分割し、それぞれの機能を独立したモジュールとして扱うことです。
モジュール間の依存関係をなくし、変更の影響範囲を限定することで、保守性を高めることができます。
また、モジュール化は再利用性を高め、テストが書きやすくなるというメリットもあります。
関連する原理、原則として最も有名なのは、SOLID原則でしょう。
SOLID原則は、オブジェクト指向プログラミングにおける5つの原則をまとめたものです。
- 単一責任の原則(Single Responsibility Principle)
- オープン/クローズドの原則(Open/Closed Principle)
- リスコフの置換原則(Liskov Substitution Principle)
- インターフェース分離の原則(Interface Segregation Principle)
- 依存性逆転の原則(Dependency Inversion Principle)
詳しい解説はWEB上に多くありますので、興味のある方は調べてみてください。
他にもモジュール化と関連のある概念として、高凝集・低結合があります。
これは、情報の隠蔽によってモジュール間の結合を弱くし、モジュール内の凝集度を高めることに繋がります。オブジェクト志向プログラミングにおけるカプセル化の考え方に近いです。
ほかにも、有名な原理原則は数多くあるので、ここで紹介しておきます。
- DRY(Don’t Repeat Yourself)
- 同じコードを繰り返し書かない
- 繰り返し箇所が多いと、修正漏れや工数の増加につながる
- ただし、関心事が異なる場合にDRYを適用してしまうとかえって可読性が下がることもあるので注意
- KISS(Keep It Simple, Stupid)
- 可能な限りシンプルに保つ
- 過度に複雑な設計を避け、初学者でも理解できるレベルを目指すことで可読性・保守性を高める。
- YAGNI(You Aren’t Gonna Need It)
- 未来の機能を先取りして実装しない
- エンジニアの予測が外れた場合のコストを考慮し、必要になった時に実装する
- ただし、将来の変更への拡張性を考慮して設計する必要はあるため、過度なYAGNIも問題
- Boy Scout Rule(ボーイスカウトのルール)
- 「来たときよりも美しく」
- コードに手を加える際には少しでもリファクタリングしてコードをきれいにする
- Law of Demeter(デメテルの法則)
- 「友達の友達に直接話しかけない」
- オブジェクトは、密接に関係するオブジェクトだけを知るようにし、深いチェーンを辿らないようにすることで結合度を下げる。
- SLAP(Single Level of Abstraction Principle)
- 「関数やメソッドの中で扱う抽象度を1つに統一する」
- 低レベルな処理と高レベルな処理を混在させず、階層的に構造を分けることで可読性を高める。
効率性
効率性は、ソフトウェアがパフォーマンスとリソースの効率的な利用を行っているかを示す指標です。
効率的なソフトウェアは、ユーザーにとって快適な利用環境を提供し、コストを抑えることができます。
効率性を高めるためには、アルゴリズムやデータ構造の選択が重要です。
また、リソースやパフォーマンスにおけるボトルネックを特定し、最適化を行うことも効率性向上につながります。
マルチスレッドや非同期処理を活用することで、並列処理を行い、処理時間を短縮することも効率性向上の手段の一つです。
信頼性
信頼性は、ソフトウェアが正確に動作し、安定しているかを示す指標です。
主にエラー処理の文脈で語られることが多いですが、信頼性を高めるためには、エラー処理だけでなく、テストやデバッグ、ログの出力なども重要です。
ソフトウェアは、予期しない障害に対処できるように設計される必要があります。
いいエラー処理とは、問題が発生した際にソフトウェアが安全に停止するか、または問題を報告して適切な対処を行うことを保証します。
また、これまで紹介してきた可読性や保守性、効率性などによってそもそもバグを減らすことも信頼性向上につながります。
モジュール化によるテストの充実や自動テストなどのテクニックも信頼性向上に役立ちます。
拡張性
拡張性は、ソフトウェアが将来的な変更や拡張に対応できるかを示す指標です。
要素としては次のようなものがあります。
抽象化
保守性の項目でも触れたモジュール化に関連する概念として、抽象化があります。
抽象化とは、具体的な実装の詳細を隠蔽し、抽象度の高いインターフェースのみを外部に公開することです。
抽象化されたシステムは、具体的にどのように実装されているかを知らなくても、インターフェースを通じて利用することができます。
例えば、DBへのアクセスや外部APIの利用など、具体的な実装が変わる可能性がある部分において、抽象化を行うことでDBやAPIの変更が発生した場合でもアプリケーションの他の部分には影響を与えないようにすることができます。
高凝集・低結合
モジュール化の項目でも触れた高凝集・低結合も拡張性向上につながります。
高凝集とは、モジュール内部の要素(関数や変数など)が一貫した目的や役割を持つ状態を指します。
もっと簡単に言うと、「1つのモジュールが関連性の高い機能だけをまとめて持っている」ことです。
高凝集なモジュールには以下のようなメリットがあります。
- 理解しやすい
- モジュールの役割の単位でまとまっているため、どこで何をやっているか把握しやすくなります。
- 保守・修正がしやすい
- 変更や追加が必要なとき、モジュール内部に収まっていることが多いので影響範囲を把握しやすいです。
- 再利用性が高い
- しっかり役割が定義されているモジュールは、他の箇所はもちろん、他のプロジェクトでも同じ役割で流用できる可能性が高まります。
反対に、低凝集の場合のデメリットとしては、何かを変更した場合に意図していない箇所に影響が及ぶ可能性が高まるという点が挙げられます。
低結合とは、モジュール間の依存度が低い状態を指します。
具体的には、「あるモジュールを修正しても他のモジュールに大きな変更を強制しない」ような状態が理想です。
低結合なモジュールには以下のようなメリットがあります。
- 変更の影響範囲が狭い
- 拡張しやすい
- テストしやすい
まとめ
今回は良いソフトウェアとはという観点から、設計において重要な要素を紹介しました。
ソフトウェアとしての良さを追求していくと、先人たちの残した原理原則やプラクティスが見えてきます。
流行しているアーキテクチャもこれらの原理原則に基づいて設計されていることが多く、まずは原理原則を理解することでアーキテクチャの理解、取捨選択ができるようになります。
可読性や保守性への理解が進むと、「動く」ソフトウェアと「良い」ソフトウェアの間には大きな差があることがわかってきます。
コードを書くことのコストと、将来的に読むコスト、変更するコストのバランスを考えながら、設計を進めていくことが大切です。