書籍「A Philosophy of Software Design」から得られた、ソフトウェアデザインの新しい視点

この記事は「Goodpatch Advent Calendar 2021」の 16 日目の記事です ✨

本日の担当は、Design Division に所属している iOS エンジニアの ナカダ です。今年は半年ほど SwiftUI と UIKit を併用した iOS App を開発に携わり、多くのことを学びました。このことについては、別の機会に共有できればと考えています。

この記事では、書籍「A Philosophy of Software Design」から得られたソフトウェアデザインの新しい視点について紹介します。「A Philosophy of Software Design」は、ソフトウェアエンジニアに向けて書かれた書籍ですが、ソフトウェアデザインに関わるデザイナー(UI デザイナーなど)にとっても、書かれている概念やテクニックは応用できると思います。

記事の終わりに、この書籍に記述されている「デザイン原則」を掲載していますので、最後まで読んでいただけると嬉しいです。

なお、本記事での引用文は、ナカダが意訳したものです。

書籍「A Philosophy of Software Design」

A Philosophy of Software Design, 2nd Edition *1」は、ソフトウェアデザインにおいて重要な課題である「複雑さ」について、大きく 2 つ書かれています。ひとつ目は「複雑さ」の意味、不必要な複雑さの見分け方です。ふたつ目は、ソフトウェアの開発プロセスにおいて、「複雑さ」を最小限に抑えるためのテクニックが紹介されています。

Google で講演された際の動画

www.youtube.com

著者の John Ousterhout さんが、Google で講演された際の動画が YouTube で視聴できます。書籍を読む時間がないという方は、この動画の視聴をオススメします。この講演では、次のことなどが紹介されています。

  • 書籍を執筆されるきっかけとなった、ご自身で考案された大学の授業
  • 「深いモジュール」「エラーを存在しないものとして定義する」などのデザイン原則やテクニック
  • 第 3 章で記述されいてる「戦略的プログラミングと戦術的プログラミング」

「複雑さ」とは

さて、ソフトウェアデザインにおける「複雑さ」とは何なのでしょうか?

書籍では

複雑さとは、ソフトウェアシステムの構造に関連するもので、システムの理解や修正を困難にするものです。(第 2 章)

と記されています。

複雑さの例としては、

  • あるコードがどのように動作するのかを理解するのが難しい場合
  • 小さな改善点を実装するのに多大な労力を要する場合
  • 改善のためにシステムのどの部分を変更しなければならないのかが明確でない場合

などが挙げられています。

端的に表現すると「自分では簡単そうに見えても、他の人が複雑だと思えば、それは複雑」となります。

複雑さは、書く人よりも読む人の方が明らかになります。あるコードを書いたとき、自分では簡単そうに見えても、他の人が複雑だと思えば、それは複雑なのです。(中略)開発者の仕事は、自分が使いやすいコードを作ることだけではなく、他の人が使いやすいコードを作ることです。(第 2 章)

また、複雑さは「変化の増幅」「認知的な負荷」「未知の未知」の症状としてあらわれ、開発作業の遂行を困難にします。

  • 変化の増幅:一見すると単純な変更でも、さまざまな場所でコードの修正が必要になること
  • 認知的負荷:開発者がタスクを完了するために、どれだけ多くの知識を必要としているのか。認知的負荷が高いほど、開発者は必要な情報を学ぶために多くの時間を費やす
  • 未知の未知:知りたいことがあるのに、それが何なのか、問題があるのかどうかさえ、調べる方法がないということ

「複雑さ」の原因

「複雑さ」の原因は、「依存性」と「不明瞭さ」の 2 つに起因すると説明されています。

「依存性」と「不明瞭さ」が組み合わさると、複雑さの 3 つの症状が現れます。依存性は、変化を増幅させ、高い認知的負荷をもたらします。また、不明瞭さは、未知の未知を生み出し、認知的負荷を高める要因となります。(第 2 章)

もし「依存性」と「不明瞭さ」を最小化するデザイン手法をみつけることができれば、ソフトウェアの複雑さを軽減できそうです。

「不明瞭さ」の原因については、ドキュメントの不備を指摘されている一方で、改善方法としては、システムデザインをシンプルにすることが、最良の方法と述べらています。

多くの場合、不明瞭さの原因はドキュメントの不備にあります。第 13 章ではこの問題を取り上げます。しかし、不明瞭さはデザイン上の問題でもあります。システムがきれいでわかりやすいデザインであれば、ドキュメントの必要性は低くなります。膨大な量のドキュメントが必要になるということは、デザインが適切でないことを示す危険信号です。あいまいさを減らすための最良の方法は、システムデザインを単純化することです。(第 2 章)

ソフトウェアデザインの新しい視点

「複雑さ」とは何か、現れる症状、原因がわかってきました。では「複雑さ」を最小限に抑えるために、何ができるようでしょうか?

書籍では、本記事の末尾に掲載した「デザイン原則」を軸に、多くのテクニックが紹介されています。本記事では、そのなかで、私が新しい視点を得られた概念やテクニックを抜粋して紹介します。

紹介するのは次の 3 点です。

  • 深いモジュール(Deep Module)
  • 2 回デザインする(Design it Twice)
  • コメントをデザインプロセスの一部として活用する

深いモジュール(Deep Module)

この書籍での「モジュール」の定義は次のとおりです。

本書では、インタフェースと実装をもつコードの単位をモジュールと呼んでいます。オブジェクト指向プログラミング言語の各クラスはモジュールです。クラス内のメソッドや、オブジェクト指向ではない言語の関数もモジュールと考えることができます。(第 4 章)

モジュラーデザインの目的は、モジュール間の依存性を最小限に抑えることです。各モジュールを「インタフェース」と「実装」のふたつの部分に分けて考えます。一般的に、インタフェースはモジュールが何をするのかを説明しますが、どのようにするのかは説明しません。実装は、インタフェースが約束したことを実行するコードで構成されています。

最良のモジュールとは、強力な機能を持ちながら、シンプルなインタフェースをもつものです。そのようなモジュールを「深い」という言葉で表現しています。(第 4 章)

書籍では、モジュールの深さの概念を、矩形の面積で視覚化されています。矩形の面積は、各モジュールが実装する機能に比例します。矩形の上辺はインタフェースを表し、横の長さでインタフェースの複雑さを表現しています。

f:id:mitsuru_nakada:20211220220818p:plain

もっとも優れたモジュールは、シンプルなインタフェースの背後に多くの機能を隠し持っています。深いモジュールは、その内部の複雑さのごく一部しかユーザーに見えないため、優れた抽象が可能です。(第 4 章)

一方で、「浅いモジュール(Shallow Module)」は、次のように説明されています。

浅いモジュールとは、提供する機能に比べてインタフェースが複雑なものです。浅いモジュールは、複雑さとの戦いではあまり役に立ちません。なぜなら、提供される利益は、そのインタフェースを学び、使用するためのコストによって否定されるからです。小さなモジュールは浅くなりがちです。(第 4 章)

これまで、私が実践してきたことは「大きなクラスを小さなクラスに分割する」「行数の多いメソッドを複数のメソッドに分割する」といった実装でした。書籍では、このような実装を、次のように指摘しています。

この方法では、浅いクラスやメソッドが大量にできてしまい、システム全体の複雑さが増してしまいます。(第 4 章)

また、著者は、クラスを細分化をやりすぎてしまうことを「Classitis」と表現しています。日本語に訳すと「クラス炎(炎症)」でしょうか。

「クラスは小さくあるべき」というアプローチの極端な例として、「クラスはよいものだから、クラスは多い方がよい」という誤った考えから生じる「classitis」という症候群があります。(中略)classitis の結果、個々のクラスは単純なものになるかもしれませんが、システム全体の複雑さは増してしまいます。(中略)これらのインタフェースが積み重なると、システムレベルで非常に複雑になります。(第 4 章)

モジュール開発者は、自身でなく、モジュールのユーザーが「複雑さ」を感じさせないようデザインする必要があります。

ほとんどのモジュールは、開発者よりもユーザーの方が多いので、ユーザーよりも開発者が苦しむ方がよいでしょう。モジュール開発者としては、たとえ自分の仕事が増えることになっても、自分のモジュールのユーザーの生活ができるだけ楽になるように努力すべきです。 (中略) モジュールはシンプルな実装よりもシンプルなインタフェースをもつことの方が重要であるということになります。(第 8 章)

では、どのようなモジュールが「深いモジュール(インタフェース)」なのでしょうか?

著者は、美しいインタフェースの例として、UNIX I/O の関数を例に挙げています。

UNIX I/O

インタフェースは、一般的なケースを可能な限りシンプルにするようにデザインされるべきです(第 4 章)

int open(const char* path, int flags, mode_t permissions);
ssize_t read(int fd, void* buffer, size_t count);
ssize_t write(int fd, const void* buffer, size_t count);
off_t seek(int fd, off_t offset, int referencePosition);
int close(int fd);

I/O のための基本的な関数は 5 つしかなく、シグネチャもシンプルです。

UNIX I/O インタフェースの実装には、数十万行ものコードが必要で、ディスクスペースの管理からファイルのキャッシュ、デバイスドライバーの膨大な量のコード、その他の多くの低レベルの実装など、多くの複雑な問題に対処しています。

これらの問題は、UNIX ファイルシステムの実装によって処理され、これらの関数を呼び出すプログラマーには見えません。

UNIX の I/O インタフェースの実装は長年にわたって急激に進化してきましたが、5 つの基本的なシステムコールは変わっていません。(第 4 章)

モジュールの分割法と結合法

「深いモジュール」が理想ではあるものの、小さなクラスや、小さなメソッドに分割した方がよい場合もあります。「Classitis」にならないよう適切に分割するには、どうすればよいのでしょうか?

メソッドをデザインする際、もっとも重要な目標はクリーンな抽象を提供することです。各メソッドはひとつのことを完全に行うべきです。メソッドはシンプルなインタフェースを持ち、ユーザーが正しく使用するために多くの情報を頭に入れる必要がないようにすべきです。メソッドは深くあるべきで、そのインタフェースは、その実装よりもはるかに単純でなければなりません。これらの特性をすべて備えたメソッドであれば、それが長いかどうかはおそらく問題ではありません。(第 9 章)

下図は、(a)を分割する方法が、大きく 3 つあることを示しています。

f:id:mitsuru_nakada:20211220220948p:plain

最良の方法は (b) のように、サブタスクを別のメソッドに分解することです。新しい親メソッドのインタフェースは、元のメソッド (a) と同じです。この分割方法は、元のメソッドの残りの部分からきれいに分離できるサブタスクがある場合に有効です。

2 つ目の方法は(c)のように 2 つの別々のメソッドに分割することですが、浅いメソッドにならないように気をつける必要があります。

このように分割すると、(d)のように、いくつかの浅いメソッドができてしまう危険性があります。呼び出し側がそれぞれのメソッドを呼び出し、その間で状態をやりとりしなければならないのであれば、分割はよいアイデアではありません。(c)のような分割を検討する場合は、呼び出し側の作業が簡略化されるかどうかを基準に判断してください。

一方で、(d) を (c) (b) (a) のように結合することで、浅いモジュールを深いモジュールに置き換え、システムをシンプルにできる場合もありそうです。

2 回デザインする(Design it Twice)

最初に思いついた設計(デザイン)やアイデアに固執して、その後、デザインの改善が求められ、リファクタリングすることがあります。ただし、リファクタリングする時間を捻出できるケースは少なく、当初のデザインが残り続けることが多いのも現実です。

著者は「最初に考えたことが、最良のデザインになるとは限らない」ことを自覚した上で、デザイン上の主要な決定事項については、複数の選択肢を検討する「2 回デザイン」のアプローチをすすめています。「2 回デザイン」するといっても、それほど時間をかける必要はなく、クラスや小さなモジュールの場合は、代替案を検討するのに、多くても 1〜2 時間ぐらいが目安です。

「2 回デザイン」の流れは次のとおりです:

  1. あるクラスが他の部分に見せるインタフェースを定義する
  2. 最初に思いついたアイデアを選ぶのではなく、いくつかの可能性を検討する
    • 妥当なアプローチがひとつしかないと確信している場合でも、(どんなに悪いと思っても)とりあえず 2 つ目のデザインを考える
  3. 代替案のデザインを大まかに検討した後、それぞれの長所と短所をリストアップする
  4. 最適なデザインを見つける
    • 代替案のひとつかもしれないし、複数の代替案を組みわせたものかもしれない
    • どの代替案も魅力的でない場合は、他の案を考える。その結果、もともとの選択肢のどれよりも優れた新しいデザインにできるかもしれない

デザインを検討する際、プラス α で次を考慮することもすすめています。

  • ある選択肢が他の選択肢よりもシンプルなインタフェースを持っているか?
  • あるインタフェースは、他のインタフェースよりも汎用性が高いか?
  • あるインタフェースは、他のインタフェースよりも効率的な実装が可能か?

複数のデザインを検討し、それぞれの強みや弱点を考え、それらを対比させることが、デザインスキルの向上につながります。

2 回デザインのアプローチは、デザインの質を高めるだけでなく、デザイナーのスキルアップにもつながります。複数のアプローチを考案して比較する過程で、デザインの良し悪しを決める要素を知ることができます。そうすることで、悪いデザインを排除し、本当によいデザインを追求できるようになるのです。(第 11 章)

コメントをデザインプロセスの一部として活用する

第 12 章では「コメントを抽象化の基本」ととらえ「コメントの重要性」が説明され、第 13 章では「よいコメントの書き方」が説明されています。

コメントを書くというプロセスが正しく行われれば、実際にシステムのデザインを改善することになります。逆に、優れたソフトウェアデザインであっても、文書化が不十分であれば、その価値は大きく損なわれます。(第 12 章)

第 12 章では「よいコメントを書くことは難しくない」「コメントを書くことは、実は楽しいこと」とも述べられており、コメントに対して前向きになれます。

よいコメントはソフトウェアの全体的な品質に大きな違いをもたらすことができること、よいコメントを書くことは難しくないこと、そして(信じられないかもしれませんが)コメントを書くことは、実は楽しいことなのだということを、これらの章で皆さんに納得していただきたいと思います。(第 12 章)

第 15 章では、コメントの記入を開発プロセスの最後に先送りするのではなく、最初の段階でコメントを書くこと(コメントファースト)をすすめています。

コメントを最初に書くことで、ドキュメント作成がデザインプロセスの一部になります。これにより、よりよいドキュメントが作成されるだけでなく、よりよいデザインが作成され、ドキュメント作成のプロセスがより楽しくなります。(第 15 章)

著者は、次のように実装を進めているそうです。

  1. 新しいクラスの場合は、まずクラスのインタフェースのコメントを書く
  2. インタフェースのコメントと、もっとも重要なパブリックメソッドのシグネチャを書く(メソッド本体は空にしておく)
  3. これらのコメントを、基本的な構造がほぼ出来たと思えるまで繰り返し書く
  4. この時点で、クラス内のもっとも重要なインスタンス変数の宣言とコメントを書く
  5. 最後に、必要に応じて実装コメントを追加しながら、メソッド本体を埋めていく

さらに実装を進め、新たなメソッドやインスタンス変数が見つかった場合は、同時にコメントを記入します。

メソッド本体を書いているうちに、たいていは追加のメソッドやインスタンス変数の必要性を発見します。新しいメソッドには、メソッド本体の前にインタフェースのコメントを書き、インスタンス変数には、変数宣言を書くのと同時にコメントを記入します。(第 15 章)

なお、コメントを書くのが難しいという箇所は、デザインに問題があることを示します。

コメントは、複雑さを示す炭鉱のカナリアの役割を果たします。メソッドや変数に長いコメントが必要な場合、それはよい抽象ができていないという赤信号です。 (中略) メソッドや変数を記述するコメントは、シンプルかつ完全でなければなりません。もしそのようなコメントを書くのが難しいと感じたら、それは記述しているもののデザインに問題がある可能性を示しています。(第 15 章)

「コメントファースト」は、コメント駆動開発とも言える手法で、慣れるまでは時間がかかりそうですが、身につけることができたなら、コメントの質、デザインの質を向上につながり、トータルでは開発速度を速くできる可能性があります。

この書籍の活用方法

著者はコードレビューと一緒に使うことをすすめています。

この本を使うもっともよい方法は、コードレビューと一緒に使うことです。他の人のコードを読むときは、それがここで説明したコンセプトに準拠しているかどうか、そしてそれがコードの複雑さとどう関係しているかを考えてみてください。自分のコードよりも他人のコードの方が、デザイン上の問題点をみつけやすいのです。ここで説明した危険信号を使って問題点を特定し、改善策を提案できます。(第 1 章)

書籍には「デザイン原則」に加え「危険信号(Red Flag)」も記述されています。本記事では「危険信号」について触れていませんが、書籍では「浅いモジュール(Shallow Module)」や「結合したメソッド(Conjoined Methods)」などが紹介されています。

「危険信号」を認識できるようになることが、デザインスキルを向上させる近道でもあります。

デザインスキルを向上させる最善の方法のひとつは、危険信号(コードが必要以上に複雑になっている可能性を示す兆候)を認識できるようになることです。(第 1 章)

コードレビューでは、複雑だと思った場合、遠慮せずフィードバックすることも大事です。

複雑さは、書く人よりも読む人の方が明らかになります。あるコードを書いたとき、自分では簡単そうに見えても、他の人が複雑だと思えば、それは複雑なのです。(第 2 章)

まとめ

「複雑さ」とは「システムの理解や修正を困難にするもの」であり、「変化の増幅」「認知的な負荷」「未知の未知」の症状としてあらわれ、開発作業の遂行を困難にします。「複雑さ」は「依存性」と「不明瞭さ」に起因することがわかりました。

そして「複雑さ」を最小限に抑えるためのデザイン手法として、次の 3 つを紹介しました。

  • 深いモジュール(Deep Module)
  • 2 回デザインする(Design it Twice)
  • コメントをデザインプロセスの一部として活用する

「深いモジュール(Deep Module)」「2 回デザインする(Design it Twice)」は、エンジニアだけでなく、UI デザインのプロセスでも活用できる考え方だと思います。

書籍では、上記のほか「戦略的プログラミング」「情報の隠蔽」「複雑さを下へ引き下げる」「エラーを存在しないものとして定義する」「一貫性」「よい名前を選ぶ」「コードを明白にする」などの手法が記述されています。

書籍「A Philosophy of Software Design」が、年末年始の読書の候補になると嬉しいです。忙しい方は、著者の John Ousterhout さんが Google で講演された際の動画 をチェックしてみてください!


Goodpatch ではデザイン好きなエンジニアの仲間を募集しています。 少しでもご興味を持たれた方は、ぜひ一度カジュアルにお話ししましょう!


デザイン原則(意訳)

書籍「A Philosophy of Software Design」の巻末の「デザイン原則(Summary of Design Principles)」を意訳したものです。

  1. 複雑さは漸進的である
  2. コードを動かすだけでは不十分
  3. システムデザインを改善するために、小さな投資を継続的に行う
  4. モジュールは深くあるべき
  5. インタフェースは、もっとも一般的な使い方を可能な限りシンプルにするようにデザインする
  6. モジュールは、シンプルな実装よりもシンプルなインタフェースであることの方が重要
  7. 汎用モジュールはより深く
  8. 汎用的なコードと特殊なコードを分ける
  9. レイヤごとに異なる抽象化を行う
  10. 複雑さを下に引き下げる
  11. エラーを存在しないものとして定義する
  12. 2 回デザインする
  13. コメントは、コードからはわからないことを説明する必要がある
  14. ソフトウェアは、書きやすさではなく、読みやすさを重視してデザインする
  15. ソフトウェア開発のインクリメントは、機能ではなく抽象であるべき
  16. 重要なこととそうでないことを分けて、重要なことを強調する

*1:e34.fmで知りました