これは何?
新人研修用に作った資料です。
ソフトウェアの設計について
良いソフトウェア設計 (design) を考えることは良いコードを書くことよりも遥かに重要です。悪いソフトウェアデザインの下で記述されたコードは、たとえそれがどんなに美しく効率的なものであっても、良いデザインの下で記述された並のコード以上にソフトウェアの価値に寄与することは無いかもしれません。しかし、そもそも良いデザインとはどのような設計を指すのでしょうか。それを論じる人物によって説明の仕方は様々です。本稿では、2名の優れたエンジニアの共通する考え方を並べ、理解を深めることを試みます。サンプルコードは C++ で記述します。
分離したデザイン
元EA所属プログラマであったロバート・ナイストロム氏が著書1の中で説明している「良いデザイン」とは以下のようなものです。
保守性の高い設計
- 良いデザインとは、何かを変更したときにまるでプログラム全体がその変更を予想して作られていたかのように思えるデザインです。
- 「部分的な変更があっても全体に影響が及ばないようなコードを書くようにするとよい」ということなのです
このことからナイストロム氏は保守性の高いデザインを重視していることがわかります。保守性とは概ね以下のことを意味する言葉です2。
- 不具合の発見・修正のしやすさ
- 仕様変更や機能追加の行いやすさ
- ソースコードの読みやすさ
また、以下のようにも述べています。
- よいソフトウェアアーキテクチャの本領が発揮されるのは、コードを読んで理解するときだと思っています
- 読み込む量を減らす方策を見つけることに意義があります
これは先に述べた保守性の内、コードの読みやすさ、すなわち可読性に関する主張であるとわかります。
補足: 可読性について
可読性はチームとしての生産性に関わる重要な要素であり、会社の売上や利益、従業員の業務時間に密接に関係します3。可読性について理解を深めることは大変重要です。ソフトウェアの設計後、プロトタイプの実装に取り掛かる前に適切なガイドライン4を参照することを強くおすすめします。可読性を向上させるテクニックを盛り込むことで、コードのレビューコストを下げることが可能なためです。
分離の効能
その上、以下のように結論づけています。
- 2つのプログラムが「分離できていない(結合している)」というのは、一方を理解しないともう一方も理解できない状態だと私は考えています
- ソフトウェアアーキテクチャを考える主要な目的は、この「次に進む前に頭に入れておかなければならない知識を最小にすること」です
- 「分離」は「一方のプログラムを変更してももう一方は変更する必要がない状態」とも定義できます
ナイストロム氏は**「プログラムをうまく分離し、保守性と可読性を高めること」**を良い設計の条件の1つとして重視しています。
疎結合なデザイン
元ピクサー所属リードエンジニアであったマーティン・レディ氏は、自身の著書5の中で優れたAPIデザインの条件を述べています。
- ソフトウェアで取り扱う領域(ドメイン)の主要な対象(オブジェクト)を定め、それらの相互関係と階層構造を要件に沿って表現できている
- ユーザに対して必要以上のインタフェースを公開せず、内部実装や内部データを適切に隠蔽できている
- 必要最小限のインタフェースのみを提供できている
- ドキュメントを見なくてもシンボル名やシグネチャ等の限定的な情報から使い方を理解することができる
- ユーザが間違った使い方をできない又はしづらいデザインが施されている
- 一貫性のあるデザインが施されている
- 相互独立性が保たれている (副作用が無いAPIを提供できている)
- リソースの解放漏れが無い (例えば、スマートポインタの適切な利用ができているか等)
- 複数プラットフォームをサポートする統一的なインタフェースを実現できている
- 疎結合である (結合度が低く、凝集度の高いデザインが施されている)
本記事では最後の「疎結合」について言及します。
疎結合
ソフトウェア設計に関する基礎的な概念である「カップリング(結合度)」と「コヒージョン(凝集度)」をレディ氏は以下のように定義しています。
- カップリング(結合度): コンポーネント間の相互連結性の強度。コンポーネント間の相互依存度合い
- コヒージョン(凝集度): 1つのコンポーネント無いの様々な機能あいだの密着性・関係性の強度
優れたデザインとは、低カップリング・高コヒージョンであるとレディ氏は説明しています。低カップリングとはすなわち疎結合な状態を指します。可能な限り疎結合を実現することで、複数コンポーネントをそれぞれ独立に使用し、理解し、保守することが可能になります。
結合度を減らすための手法
名前だけの結合
依存先クラスのサイズ情報が不要かつメンバ参照が不要ならば、前方宣言で解決することで結合度を削減することができます。例えば C++ では以下のようなコードを書きます。
class AudioPlayer; // 前方宣言
// #include AudioPlayer.h> クラス定義本体は不要
class AudioPlayerHolder
{
public:
void Set(const AudioPlayer *Player);
const AudioPlayer* Get() const;
private:
AudioPlayer* mAudioPlayer;
};
AuidoPlayerHoler クラスには AudioPlayer ポインタを保持し、閲覧用に返す Getter を定義しています。AudioPlayerHolder の実装側でAudioPlayerのメンバ関数やデータにアクセスすることはありません。他クラスに AudioPlayer の変更を許可したくない場合、AudioPlayerHolder に包んで渡すといった用途が考えられます。
関数とクラス間の結合度の削減
音声波形を表現する SoundWave クラスを考えるとします。このクラス定義を C++ コードにすると以下のように表現できます。
class SoundWave
{
public:
// 波形のサンプリングレート数を返す
float GetSamplingRate() const
{
return mSamplingRate;
};
// 波形の総サンプル数を返す
float GetNumSamples() const {
return mNumOfSamples;
};
// 総サンプル数をミリ秒単位に変換して標準出力に出す
void PrintDurationMsec() {
std::cout << mSamplingRate/mNumOfSamples * 1000.0f << std::endl;
};
private:
float mSamplingRate;
float mNumOfSamples;
};
メンバ関数 PrintDurationMsec() に注目してください。この関数は SoundWave の内部詳細を直接参照しています。将来、SoundWave の内部詳細が変更されると、GetSamplingRate()、GetNumSamples() と共に、この PrintDurationMsec() 関数の定義が破壊される可能性があります。この場合は予め設計を見直し、将来修正が必要になり得るコード量は最小限に抑えて保守性を高めるべきでしょう。実際、下のように書き換えることで PrintDurationMsec() 関数と SoundWave クラス間の結合度を削減することができます6。
class SoundWave
{
public:
// 波形のサンプリングレート数を返す
float GetSamplingRate() const
{
return mSamplingRate;
};
// 波形の総サンプル数を返す
float GetNumSamples() const {
return mNumOfSamples;
};
private:
float mSamplingRate;
float mNumOfSamples;
};
// メンバ関数ではなく自由関数にすることで SoundWave の内部詳細と切り離せた
void PrintDurationMsec(float SamplingRate, float NumOfSamples) {
std::cout << SamplingRate/NumOfSamples * 1000.0f << std::endl;
};
PrintDurationMsec() 関数は SoundWave のメンバ関数ではなくなりましたが、これによって SoundWave の内部詳細から切り離すことができました。 SoundWave が public インタフェースとして公開しているメンバ関数から必要な情報を取得して渡すことで、目的の結果を得ることが可能です。
考察
ナイストロム氏の考えは、一般的な技術用語よりもむしろ平易な言葉で説明されています。それも恐らくは彼自身の設計の意味に対する解釈を反映しているように見えます。特にプログラムの「分離」の効能を、コードの読み手の理解のしやすさ、すなわち「読解コスト」の低さに見出している点は、彼の著書の読者にとっては経験的に理解しやすいのではないかと考えます。ソフトウェア設計に関する理論は高度に抽象化された概念の理解を要求するため、普段の具体的な業務において意識的に活用するには相当の訓練が必要でしょう。ですがナイストロム氏のように、将来コードを読む者の理解を助ける設計を意識することで、設計者はより自然にソフトウェアのデザインを考えることができるかもしれません。
一方のレディ氏の著書5は一般に知られたソフトウェア開発ノウハウを整理したカタログとして大変優れています。結合度や凝集度といった用語も一般的に用いられているものです。ナイストロム氏の「分離」デザインを第一歩とし、レディ氏の紹介した一般の技術用語で理解を補強することで、設計に関する主観的な理解を客観的理解へと押し広げる助けになると考えます。
参考文献
-
"Game Programming Patterns ソフトウェア開発の問題解決メニュー", Robert Nystrom, 株式会社インプレス, p.10-12 (2015). ↩
-
"保守性", IT用語辞典 e-words, 株式会社インセプト, http://e-words.jp/w/%E4%BF%9D%E5%AE%88%E6%80%A7.html (2020). ↩
-
"なぜ読みやすいコードが必要なのか - コードの可読性を高める手法をサンプルで学ぶ", 米村歩, エンジニアHub, https://employment.en-japan.com/engineerhub/entry/2019/09/24/103000 (2019). ↩
-
"リーダブルコード: より良いコードを書くためのシンプルで実践的なテクニック", Dustin Boswell, Trevor Foucher, 株式会社オライリー・ジャパン (2012). ↩
-
"C++のためのAPIデザイン", Martin Reddy, ソフトバンク クリエイティブ株式会社, p.23-69 (2012). ↩ ↩2
-
"Effective C++ 第3版", Scott Douglas Meyers, 丸善出版, p.100-104 (2014). ↩