2018年12月13日

UNIX 哲学に学ぶ 高品質なソフトウェア設計

Outline
 なぜ UNUX 哲学 [1] なのか.まずはこの題意について説明したい.UNIX [2] の誕生には,先行して開発されていた Multics [3] の「新奇なアイデアを貪欲に取り込んだ [3]」結果,「巨大で複雑なものに [2]」なり,「現実解 [3]」となり得なかった歴史がある.これを踏まえ,UNIX では,まず小規模に動作するシステムを作り,巨大で複雑な問題に対しても,巨大な単一プログラムで解決することを避け,小規模なソフトウェアへと分割し,組み合わせることで,解決した.このような UNIX 開発における経験則を,UNIX 哲学と呼び,我々の求める高品質なソフトウェア設計の礎となり得る思想である.品質の高いソフトウェア開発には,ある種の哲学,つまりは設計思想が重要となる.本投稿では,UNIX 哲学を交えつつ,ソフトウェア設計に必要な設計思想について,おおよそ一般的と思われることを説明する.

※ この記事は,「みゅーもり Advent Calendar 2018 (通称:みゅんカレ)」13 日目の記事です.十分に検証されたものではなく,私の理解を大雑把にまとめたものです.したがって,事実と異なる可能性があります.また,本文中の擬似コードは C/C++ または Python です.
Prologue
 品質の高いソフトウェアとは,どのように定義されるだろうか.よく言われるのは「可読性」である.しかし,真に重要な点は,「ソフトウェアは常に変更を必要とされる」という点である.そして実際,変更が容易なソフトウェアは,可読性も高い.簡単に意味が取れて,必要な変更も少ないからだ.本投稿では,変更が容易なソフトウェア,即ち,十分にメンテナンス可能かつ,持続的に開発可能なソフトウェアを,品質の高いソフトウェアと定義する ※1

 アントニー・ホーア博士は「複雑すぎないソフトウェアシステムを作成する困難さについて [4]」,「ソフトウェアを設計するには,2通りの方法がある.1つは,とてもシンプルに設計して,明らかに欠陥がないようにすること.もう1つは,とても複雑に設計して明らかな欠陥がないようにすることだ.前者の方がはるかに困難である [4]」としている.しかし,たとえ困難が待ち構えていようとも,我々が目指すべき方向は自明である.歴史上多くの偉人達がしてきたように,複雑な問題を分割して ※2,ソフトウェアをシンプルに,明らかに欠陥がない状態を保つ方向だ.
 では,どのように実現するか.

※1. ソフトウェアの品質について考えるとき,1) 一人での開発速度がスケールするか,の他に,2) 開発速度が開発人数に対してスケールするか,3) 実行速度が CPU 数またはサーバ数に対してスケールするか,といった問題がある.この記事では,主に,1) を説明する.2) は触れるに留め,3) は説明しない.
※2. ルイ 11世「分割して支配せよ」.ルネ・デカルト「困難を分割せよ」.ヴィクトリア女王「分割して統治せよ」.ビル・ゲイツ「問題を切り分けろ」.(敬称略.出典不明.)
Unix philosophy
 品質の高いソフトウェアを実現するための答えを,ここでは UNIX 哲学に求める.UNIX 哲学とは,

1. 小さいものは美しい.
2. 各プログラムが一つのことをうまくやるようにせよ.
3. できる限り早く原型(プロトタイプ)を作れ.
4. 効率よりも移植しやすさを選べ.
5. 単純なテキストファイルにデータを格納せよ.
6. ソフトウェアを梃子(てこ)として利用せよ.
7. 効率と移植性を高めるためにシェルスクリプトを利用せよ.
8. 拘束的なユーザーインターフェースは作るな.
9. 全てのプログラムはフィルタとして振る舞うようにせよ.

である [1].この項目では,UNIX 哲学を噛み砕いて説明する.
1. 小さいものは美しい.[1]
 小さいものは,一つの関数に意味が完結しており,理解し易い.一つの関数は,一画面に収まりるようにし,全体が見渡せるとよい.また,小さい関数やクラスは,変更が発生した場合でも,殆ど組み替えるだけで済む.新しく何かを実装する場合でも,既存のパーツをレゴブロックのように組み合わせるだけで,ある程度形になる.
 では,どのように小さく作るか.

冗長な表現を避けた,必要十分な命名規則
例1) 日本語で例えるなら,「パソコンの設定を行う」ではなく「パソコンを設定する」と記述すべき.
解説1)「行う」という単語は何も説明しておらず,必然性がない.
例2) 日本語で例えるなら,「頭痛が痛い」ではなく「頭痛がする」あるいは「頭が痛い」と記述すべき.
解説2) 一般に同じ意味を重ねることを「重言」と言い,この例では不自然に言葉を重ねている.

初見で理解できる程度の略度は積極的に使う
例) make directory は make dir で十分.(ただし,mkdir コマンドがあまりにも有名なため,mkdir としてもよい.)

不必要なコメントは削除する.(変数名/関数名と意味が重複するコメントは不要)
例) mkdir() 関数のコメントとして「ディレクトリを作成する関数」などの自明な説明の記入は,目障りなだけである.
解説) 中には不安から削除を嫌う人々も存在する.しかし,それはソフトウェアのバージョン管理の問題であって,コメントにおいても,ソースコードにおいても,不要なものは積極的に削除し,必要であれば,古いバージョンからコピーして来ればよい.重要なのは,人間が管理できるコード量には限界があり,プログラマは,増え続けるコードに対して,体系化した管理やファイル分割,そして,不要なコードの削除と,共通化によるコード量の圧縮を駆使して,対処しなくてはならないという点である.誤解を恐れずいえば,コードは短ければ短いほど,管理は簡単で,読むのも楽になる.
- 余談 -
いつ,どのようなコメントを書くのか.基本的にコメントは邪魔なので,書かない.コメントを書かずとも理解して貰えるように書く.それでもコメントが絶対に必要なときがある.それは,
・ハイパーパラメータを記述したとき.その導出方法.
・どうでもよいがなにか値が必要だったとき.(「勝手に値を変えてもよい」と書いておく.)
・Doxygen を用いるとき.
などである.結局,何をするかはコードを読めば分かる.そのため,コメントには,なぜそのように設計したのか,設計根拠を書く.
- 閑話休題 - .


 これら3つの規則を踏まえた例を,一つ考える.

例) ディレクトリを生成する関数を作るとき,
void MakeDirectoryFromPath(const char* GeneratePath); // GeneratePath に指定したディレクトリを生成する.
に,上記規則を適用すると,
void makeDir(const char* pPath);
または,
void mkdir(const char* pPath);
となる.
解説) 基本的には,上記の規則に従っているだけである.ポイントは「自明なことを改めて書かない」ことと「自明でないことは明示する」ことである.例えば,関数名の "FromPath" は不要である.ディレクトリの生成先を path 以外で示すことはない.同様に,"GeneratePath" も冗長である.ディレクトリ生成関数に,生成するパス以外のパスを渡すことは在り得ない.最後に,コメントの「GeneratePath に指定したディレクトリを生成する」は,関数名と意味が重複しており,不要である.また,Directory は非常にスペルが長いため,一般的な略文字である dir を用いる.命名の工夫としては,ポインタ型を p と修飾している.型を修飾する例としては,他にもベクトル型に vec (vector), 行列型に mat (matrix), リストに list,複数要素を持つ変数に複数形の s,正の整数に unsigned の u など様々である.
 上記の 例) では,はじめ makeDir() と命名していた.この記法は,キャメルケース [6] と呼ばれ,先頭が小文字のため見た目が美しく,シフトを押す頻度が低いため,楽に入力できる.また,複数単語接続される場合でも,アンダーバーを用いないため文字数が抑えられる.特に指定がなければ,キャメルケースを推奨する.通常,どのフォーマットを利用するかは,コーディング規約によって定められており,他にも,
・makeDir: キャメルケース [再掲]
・MakeDir: パスカルケース
・make_dir: スネークケース
[6] がある.これらは,状況ないしコーディング規約に応じて使い分けることもある.詳しくは,[6] 等を参照されたい.
- 余談 -
 命名規則やコーディング規約について,より詳しく学習したい読者には,"Google C++ Style Guide" [原文 (英語)] [日本語訳] の一読をお勧めする.一度に全てを取り入れることは難しいかもしれないが,大いに参考となるはずだ.まずは一つずつ真似できる所から取り入れればよい.(ただし,命名規則の責任を "Google C++ Style Guide" に求めるのは間違いである.コードの責任は自ら負わなくてはならない.そして "Google C++ Style Guide" であっても稀に書き換わることがある.正解がある訳ではなく,日々移り変わっている.)
 ところで,命名規則が崩壊することは無いだろうか.大抵の場合は,開発を進める間に,1) 標準ライブラリの関数と名前が衝突したり,2) 何らかの不整合が生じるものである.前者 1) について,短い命名規則は,基本的に名前の衝突を引き起こす.これは,別のオブジェクトの同じ操作に対して,同じ命令でアクセスできる点において,むしろ好ましい.ただし,名前の衝突は解消する必要があり,関数の多重定義か,名前空間の分割によって解決される.名前空間を利用する場合であれば,C++ なら namespace ms{ void myFunc(); } // ms: mySpace として ms::myFunc(); と呼び出せばよく,Python なら import fileName as ms として ms.myFunc() と呼び出すのが常である.後者 2) について,小手先の対応は,将来的な関数の使用範囲拡大に伴い,負債の影響範囲を拡大させる.そのため,都度,不整合を起こさない命名規則に修正する必要がある.また,直ちに影響が広がらない箇所についても,命名規則が異なると可読性が低下することや,誤った命名規則を元に開発されるリスクを考えれば,修正するのが妥当な判断である.
 人間は,適切で短い名前を付けることで,物事に対する認識コストを大きく下げることができる.これは,プログラミングにおいて非常に重要である.適切,というのは当然であるが,短い必要はあるだろうか.なぜ長い名前を嫌うのか.これは,人間が関数名や変数名を認識するとき,短い文字列の方が早く認識でき,かつ,コーディングに際しても,少ない打鍵数で入力できるからである.加えて,短い名称は記憶しやすく,何度も名前を確認せずとも入力できるからである.
 読みやすさはともかく,打鍵の話となると,生産性の話であるが,早く実装できれば,品質に回すだけの余力が生まれる.結局は同じことである.私が説明するのも何なので,下記のブログを読んでみると良い.
 作りたいものを作るには結局大量のコードを書かないといけないことについて
- 閑話休題 - .


不必要な実装は削除する
 コメントの削除に同じ.不要なコードが残っていると,後で見たときに,何が必要が分からない.そのため,Git 等のバージョン管理ソフトなどでバックアップを取った後,削除する.必要であれば,バックアップから復元するが,古いコードが必要となるのは稀である.
 下記に,より具体的なエピソードを記した記事を紹介する.
 コードを削除したら喜ぶべき.知らない人がみたら意味不明なコードが残っていませんか? - sigbus.info - Rui Ueyama

インデントを浅く保つ
 インデントの浅いコードは,ロジックが小さく美しい.
- for 文が 1 つ増えるごとに,別の関数に分割する.
 例) [哲学 9. のベクトル版の擬似コードを参照]
- if 文は continue を用いてインデントを保つ.
 例) パスが存在した場合に,画像の読み込みと保存を行う擬似コードを,例外処理に付き合って
vecImgPath = ['0.png', '1.png', '2.png', '3.png']
for imgPath in vecImgPath:
    if fileExist(imgPath)==True:
        img = path2img(imgPath)
        img2file('result_'+imgPath, img)
のように記述していてはいけない.このような実装は,例外処理の度にインデントを不覚する.そこで,continue を活用し,
vecImgPath = ['0.png', '1.png', '2.png', '3.png']
for imgPath in vecImgPath:
    if fileExist(imgPath)==False: continue
    img = path2img(imgPath)
    img2file('result_'+imgPath, img)
のようにインデントを抑制する.

関数に余計な機能を追加しない
 哲学 2. で説明する.

出力を小さく保つ
 「余計な出力をすべきではない.他の開発者にとってただ邪魔なだけである.[1] 沈黙のルール

実体を高々 (最大でも) 一つに保つ
 「実体が 2 つ以上あるものは管理できない」.(ただし,機能の異なる関数や変数を無理に一つにまとめることも,またよくない.)
2. 各プログラムが一つのことをうまくやるようにせよ.[1]
 プログラムを実装するとき,欲張ってはいけない.アプリケーションレベルの話をするなら,エディタでブラウジングできる必要があるのか,ということだ.ブラウジングにはブラウザを使えば良い.(ただし,残念ながら,私は Emacs ユーザである).関数レベルの話をすれば,以下の設計は一つの関数の守備範囲が過大なため,大抵破綻する.大きなプログラムは管理には,大きいなコストを必要とするからだ.このコストは,複雑性の増加のため,複数の小さな関数を管理するコストより大きい.その上,大きな関数は,必要な機能のみを使い回すことができない欠点を,さらなる機能追加で補おうと,更に肥大化する.

例) 画像を処理する擬似コード.(画像を読み込み,4x4 でビニングしたのち,RGB -> HSV 色空間へ変換し,赤色のみ取得し,RGB 色空間へ再変換した後,ファイルへ保存している).
import imageAnalyzer as ia

def cnv_imgRGB(imgRGB, isBinning4x4, isOnlyThroughRed):
    if isBinning4x4==True:
        imgRGB = ia.img2binning4x4(imgRGB)
    if isOnlyThroughRed==True:
        imgRGB = ia.img2binning4x4(imgRGB)
        imgHSV = ia.img_RGB2HSV(imgRGB)
        imgHSV = ia.img_throughRed(imgHSV)
        imgRGB = ia.img_HSV2RGB(imgHSV)
    return imgRGB

imgRGB = ia.path2imgRGB('./001.png')
imgRGB = cnv_img(imgRGB, True, True)
ia.imgRGB2file('./001_processed.png', imgRGB)
例) こうした場合は,哲学9. で示すように,フィルタとして実装するのが常である.[哲学 9. に示す擬似コードを参照]
3. できる限り早く原型(プロトタイプ)を作れ.[1]
 ソフトウェアは,設計に失敗すると必ずソースコードが発散する (行数の増加速度が早すぎて,機能追加がままならなくなる).そのため,予め技術的に失敗しそうな点を試したり,ソフトウェアの骨格部分を先に設計し,問題点を洗い出しておくことは,重要である.

4. 効率よりも移植しやすさを選べ.[1]
 この理由は,
・「ソフトウェア開発に終わりはない.あるのはリリースだけだ.[7][8]
・「ソフトウェア技術者たちが,ユーザーが現在必要としている機能と将来必要になるであろう機能とをすべて把握していたとすれば,ソフトウェアはいらないだろう.すべてのプログラムは,最初からROMに書かれていればいい[7][8]
・「ソフトウェアのエンジニアという職業には,継続的な改訂作業がつきものだ.[7][8]
に要約される.
 ソフトウェアは,兎に角あらゆる理由で変更が必要とされ続ける.それは,新しいハードウェアへの対応かもしれないし,バグの修正や新機能の追加かもしれない.あるいは,ライブラリの一機能だけ別に移植したいのかもしれない.そして,効率と移植しやすさはトレードオフの関係にある.もし,特定の OS やハードウェアに特化すれば,その分だけ固有のコードが生成され,移植や変更が不可能となる.(ただし,現実的には UNIX か Windows か,x86 かそれ以外かはに限定することが殆どだろう.過度な移植しやすさの追求もまたコストである.それでも,OS やコンパイラ,インタプリタ,その他ライブラリ等,場合によっては,ハードウェアのアップデートには,対応しなくては動かないし,動かせなくなる.)
 これは,経済/メモリ/実行 効率 についても同様である.プログラマの時間は貴重である [1] 経済のルール.大抵の場合,移植性や開発効率は,経済効率やメモリ効率に優先する."buy more memory" と揶揄されるのは,このためである.特に,実行効率について考える場合は,注意が必要である.多くの高速化は,コードの再利用性を低下させ,また,一歩間違えれば,プログラム全体として全く高速化されない.先人の言葉を借りるなら「プログラムがどこで時間を消費することになるか知ることはできない.ボトルネックは驚くべき箇所で起こるものである.したがって,どこがボトルネックなのかをはっきりさせるまでは,推測を行ったり,スピードハックをしてはならない.[1] ルール1」,「計測すべし.計測するまでは速度のための調整をしてはならない.コードの一部が残りを圧倒しないのであれば,なおさらである.[1] ルール2」である.
 基本的に高速化はしない.ただし,実行に時間の掛かるコードは,それだけでデバッグが困難となる.この場合,テストで小さなサイズについて計算したり,デバッグが必要なコードだけを切り出して変更とテストをした後,組み込む.それでも追いつかない場合は,高速化を検討することになる.まず 1) データ構造が参照の局所性を破壊していないか (ポインタ接続やリスト構造を避け,メモリアドレスが連続するように設計されているか),や, 2) アルゴリズムの妥当性, 3) スクリプト言語なら for 文等の遅い処理を避けた実装をしているか, などを確認した後, 4) マルチスレッド/マルチプロセス, 5) 命令セットアーキテクチャ,の利用を検討する.ただし,1) や 2) は初期設計の段階で検討して置かなければ,書き直しが多すぎるため,始めから考えておくものである.したがって,3) 4) 5) について検討することになる.
5. 単純なテキストファイルにデータを格納せよ.[1]
 これは 哲学 4. で説明した.設計を複雑にするのは愚かである.
解説) データをバイナリとして保存することは,効率がよいかもしれない.しかし,バイナリフォーマットは,テキストファイルのようにエディタで読むことはできず,また,予めデータフォーマットを綿密に設計する必要がある.また,新たに加わるプログラマにとっても,フォーマットの理解は大きな負担となる.特に必要のない場合に,そこまでの効率を求める必要はない.
 即ち,本当に必要となるまで,設計を難しくしてはいけない.そして,本当に必要であっても,同じ機能を簡単に実現できる方が,より望ましい.
6. ソフトウェアを梃子(てこ)として利用せよ.[1]
 ソフトウェア開発を推進する上で,最も強力な武器は,ソフトウェアである.その武器は,自動テストかもしれないし,デバッガかもしれない.あるいは,wiki システムかもしれないし,Git あるいは エディタの自動補完機能かもしれない.いずれにしても,ソフトウェア開発を推進する上で,もっとも強力な武器は,ソフトウェアそのものである.そして,その中でも自動化とは,まさに梃子そのものである.自動化できるものは,全て自動化されたい.(ただし,自動化は目的ではない.)
7. 効率と移植性を高めるためにシェルスクリプトを利用せよ.[1]
 残念ながら,私は,シェルスクリプトを活用していないが,C 言語よりも簡単な梃子で,移植性が高いのであれば,言うまでも無いだろう.これは 哲学 4. と 哲学 6. に準ずる.最近では,シェルスクリプトの代わりに Python を利用する人々もいるかもしれない.
8. 拘束的なユーザーインターフェースは作るな.[1]
 原意は「過渡の対話的インターフェースを避ける [7][9]」であるが,ここでは,単にインターフェースについて述べる.
 人間が直接操作するプログラム以外は,人間ではではなく,他のソフトウェアが使いやすいプログラムを書くべきである.また「インターフェースデザインにおいては,常に驚きが最小限であるようにせよ.[1] 最小限の驚きのルール」と指摘されているように,他の 関数,クラスないし他のプログラム へ自然と接続できるように実装する.これは,結果的に,より多くの目的で,より汎用的に利用できる設計となる.関数やクラスにおけるインターフェースは,引数の項目や順番,型,そして演算子である.可能な限り統一されたい.
9. 全てのプログラムはフィルタとして振る舞うようにせよ.[1]
 原意は「複数の小さなコマンドをフィルタとして設計すると,パイプによる接続と組み合わせで,多くの問題を解決できる」ようになるということ.
 これは,コマンドに限らず関数やクラスについてもフィルタとして振る舞うように実装することで,プログラム構造をフラットに保ち,入れ子型に複雑化することを防げる.(簡単に考えるなら,for 文が 1 つ増える度に,関数へ分割すると思えばよい.)

 例) 画像を処理する擬似コード.(画像を読み込み,4x4 でビニングしたのち,RGB -> HSV 色空間へ変換し,赤色のみ取得し,RGB 色空間へ再変換した後,ファイルへ保存している).
import imageAnalyzer as an
imgRGB = an.path2imgRGB('./001.png')
imgRGB = an.img2binning4x4(imgRGB)
imgHSV = an.img_RGB2HSV(imgRGB)
imgHSV = an.img_throughRed(imgHSV)
imgRGB = an.img_HSV2RGB(imgHSV)
an.imgRGB2file('./001_processed.png', imgRGB)

 例) 複数画像を処理する擬似コード (ベクトル版).(基本的に,処理をベクトルとして記述すると for 文が不要となり,実装の難易度は大幅に低下する.ベクトル版は,スカラー版の処理を利用して実装するのが常である).
 ./imageAnalyzer.py
...
def vecImg2binning4x4(vecImgRGB): return [img2binning4x4(imgRGB) for imgRGB in vecImgRGB]
def vecImg_RGB2HSV(vecImgRGB): return [img_RGB2HSV(imgRGB) for imgRGB in vecImgRGB]
...
 ./main.py
import imageAnalyzer as an
vecImgRGB = an.path2vecImgRGB('./*.png')
vecImgRGB = an.vecImg2binning4x4(vecImgRGB)
vecImgHSV = an.vecImg_RGB2HSV(vecImgRGB)
vecImgHSV = an.vecImg_throughRed(vecImgHSV)
vecImgRGB = an.vecImg_HSV2RGB(vecImgHSV)
an.imgRGB2file('./*_processed.png', vecImgRGB)
 このように,フィルタとして記述された関数は,処理の追加や削除が非常に簡単である.例えば,ビニング処理が不要なら取り除けばよいし,ビニング範囲を広げたければ vecImg2binning8x8() と交換すればよい.このような,自明で小さな関数は,いくら増やしても,コードが発散する原因となり得ない.逆に,コードの複雑度は,レイヤー(関数の「入れ子」構造)を重ねるごとに増加し,レイヤー間の設計(≒関数の上下関係)を誤ると,容易に発散する.コードを書くときは,なるべく「入れ子」とならないよう平坦に書くことが「コツ」である.もし関数が「入れ子」となりそうな時は,可能な限り,関数の外へと分割する.これは,結果としてインデントが浅いコードとなる.レイヤーを薄くし,代わりに,同じレイヤーに属する関数を増やす戦略は,コードを分離し,見通しを良くする.

- 余談 -
 ループ変数について考える../imageAnalyzer.py では,型を明示するため imgRGB と記述しているが,RGB 以外の型を用いないのであれば,単に img でよい.特に理由がなければ,文字列は s,整数なら i, j, k でも構わない.むしろ,極端に短いことは,ループ変数であることが分り易いとする意見もある (原本が手元に無いが,おそらく出典は [リーダブルコード]).ところで,何故ループ変数に i, j, k を使うかは i j k 変数 で [検索] してもらえれば「index や integer からの i かつ アルファベット順で,古くから使われており,一般的だから」だと分かる.しかし,私は好きではない.フォントの問題ではあるが,i と j は形状が似通っており,何度も見間違えたからだ.だから,2 変数や 3 変数のときは p, q, r を使う.ただしこれも,1 変数で p だけ使うのはやはり不自然だし,何より大抵のコーディング規約では,暗黙の内に i, j, k を使うことになっている.規約には従うべきだろう.
- 閑話休題 - .
How can I write a clear program ?
 この投稿は,ソフトウェア開発における哲学を,2018 年現在の実状に合わせて平易に書き下すことを目的としている.ここでは,UNIX 哲学ではない開発思想について,上記の話との重複を許しつつ説明する.
ソフトウェアを一から設計する場合
はじめから綺麗に実装する.(始めから分割して実装する)
 ソフトウェアは 2 度実装しない.大抵の場合はする時間がない.したがって,1 度目である程度綺麗に書く必要がある.大抵の場合は,関数に分割してから書くと,必然的に綺麗になる.(動かない関数を書いてみて,骨格となるインターフェースを纏め上げてから細かい実装をする).

全て書き直す.(2 度目の実装する)
 矛盾するようだが,1 度で綺麗に書けず,技術的負債が蓄積すると,書き直さざるを得なくなる.この場合は,1 度目のプロトタイプにおいて発生した問題を,解決するように書く.幾つかの方法があるが,接続用のラッパを用いて,動く状態を保ちつつ,少しずつ負債を返済し,最終的に全て書き直す方法と,一からフルスクラッチで書き直す方法がある.全容が分かっていれば,フルスクラッチで骨格を作りなおしてから,肉付けする.全容が分からなければ,兎に角動く状態を保ちつつ,修正する.(当然,間を取ることもある.)
 この負担は非常に大きいため,大きな負債となる前に普段から設計を変更して軌道を修正し,全書き直しとならないように,書くのが普通である.

例)
 書き直す,というのは,勇気がいるかもしれない.しかし,設計に誤りがあった場合,コードは発散して収集がつかなくなる.そうすると,遅かれ早かれ書き直すか,プロジェクトが解散する.ここで,Rui Ueyama 氏 (lld という次世代の高速リンカのオリジナル作者) による面白いエッセイがあるので,下記に紹介する.
「悪い方が良い」原則と僕の体験談 - note - Rui Ueyama
また,同著者の,動く状態を保ちつつ,変更を加え続けることで,最終的に大規模なプログラムを作成する手法として,
Cコンパイラ制作の夏期集中コースが思っていた以上にうまくいった話 - note - Rui Ueyama
も非常に興味深い.
既存のソフトウェアを改変する場合
 私は個人プロジェクトで小さなプログラムを一から作ることが多いので,実際に大規模なプロジェクトで開発しているプログラマの意見を参考する.例えば,Rui Ueyama 氏は,自身の note で,
 ・「できる限りコードを正常動作する状態に保つこと」
 ・「できるかぎり小さな変更を積み重ねていくこと」
 ・「互換レイヤ(というと大げさですがただ古いものと新しいものをブリッジするクラスとか関数)を適当に作って,
   全体を一気に変更しなくても,一つ一つ変更していけるように」すること.
 ・「きちんとコンパイルできるところでコミットしておくこと」
などと説明している [10].これは,大きなプログラムだけではなく,小さなプログラムを一から開発する場合にも,有効だと考えられる.また,もし改変するプロジェクトが OSS 開発であれば,オープンソース活動がフルタイムの仕事になる仕組みの話 - note - Rui Ueyama も大変興味深い.私から説明することは,これ以上ないので,詳細は Ref. [10] を参照されたい.
 (なお,この場で言及するのも,やるせないが,動作する状態を積み上げつつ初期設計の軋轢を緩和しながら開発するのが,プログラミングの基本であり,古典的な意味でのウォーターフォール・モデルは破綻している.プロトタイプを作成しなかったり,エンジニアを信用せず if 文レベルまで詳細設計してみたり,適度な方向性の再検討も無いのは危険である.)

- 余談 -
 思い出されるのは,青い銀行のシステム統合プロジェクトだろう.合併により複数のベンダーのシステムが継ぎ接ぎだらけで動いていたため,システムの刷新/統合計画が持ち上がった.あまりにも巨大なシステムなので,移行には,兎に角動く状態を維持しつつ,少しずつ,共通部分を増やしていき,最終的に統合されるような戦略が必要だろう.最近ではようやく,完成したとか,していないとか,囁かれているが,ともかく,止められないシステムを,フルスクラッチで全て書き直したシステムへスイッチするには,大きな労力が必要となる.
- 閑話休題 -.
複雑性を隠蔽する
 UNIX 的には「階層的に考えよ [1]」という.例えば,C++ や,あるいは,他の言語で,TCP/IP を用いてネットワークへアクセスするとき,輻輳制御,フロー制御,順序制御や再送制御,エラー訂正について考え,実装することは,まず無いだろう (ただし,組み込みエンジニア等を除く).大抵は OS の socket に処理をさせる.通信回りは階層的な設計となっており,OSI 参照モデルで説明される.データ処理のそれぞれが階層 (フィルタといってもよいかもしれない) に分けられており,各処理の複雑性が,各々の層に封じ込められている.そして,理想的には,各層ごとに個別に機能を刷新して入れ替えればよく,他の機能について考慮せずとも開発できる.
 例 1) オブジェクト指向における話.C++ でメモリを管理するときに,オブジェクトのディストラクタがスコープ (端的には括弧で囲まれた区間) の終わりに呼び出されることを利用して,コンストラクタでメモリを確保し,ディストラクタで開放することがよく行われる.C 言語の問題として取り上げられるメモリ管理上の問題は,多くの場合,スコープによるオブジェクトの管理によって解決される.(詳細は RAII - Wikipedia を参照).スクリプト言語においては,この複雑性を,多くの場合,ガーベッジコレクションへ押し付ける.
 例 2) ラッパ関数における話.ラッパ (wrapper) とは,日本語で包装紙を意味し,ソフトウェアにおいては,ある関数や操作を,ユーザが利用しやすいインターフェースとして隠蔽する役割がある.例えば,C++ から Python でグラフをプロットしたいとき,一度 csv に吐き出してから,Python スクリプトを呼び出し,ファイルを読み込ませて,グラフをプロットする.これは,とても面倒である.こうした場合に,言語間の データ共有・インターフェースの違い を隠蔽するために,ラッパを用いる.有名どころでは,matplotlib-cpp のようなラッパがある.マイナーどころでは,私自身が sstd::c2py() といったクラスを書いたことがある.

 戦略としては,複雑な物を,簡単に見せる.各層ごとに自明とする.注意すべきは,隠蔽をしていても,本来の対象は複雑なので,インターフェース設計が難しい点である.また,ある層から別の層を見ると,その層は,完全なブラックボックスとなる.別の層にバグが混入していた場合,プログラマが対象の層を熟知していない限り,修正は困難となる.したがって,各層には,ある程度の完全性,即ち,バグが無いことが求められる.また,無理に別の層でバグに対処しようとすると,コードに軋轢が生じる.そのため,可能な限り,原因となったコードそのものを修正する.どの層の責任であるかを明確にし,越権行為を許してはならない.しかし,知らない層のバグを修正することは困難であるため,実際には場当たり的な対応となることもある.
試行錯誤を必要とするプログラム
 解析コードや,実装可能か試すためのコードは,基本的に品質よりも試行錯誤が優先される.規模が小さい内は,ただ書き散らかせば問題ないが,コードが大きくなると厄介である.この場合は,試行錯誤部分と共通部分(ライブラリ)に分離し,コードが汚染する範囲を制限すべきである.汚いものや,厄介なものは,一箇所に固めて管理する.(これは実際にライブラリとして書いてみるのがよい.ライブラリ側へも多くの変更が発生し続けるので,何度もインストールするよりは,ポータブルな設計が好ましい.)
一貫性のあるソフトウェアの開発と維持
 結局我々は,ソフトウェアを自明な状態に保ちたいだけである.自明であれば説明せずとも分かる.これは,一貫性のあるソフトウェアとも言える.一貫性のあるコードは,ルールさえ分かれば後は自明である.一貫性を維持するには,ある程度のコストが掛かる.短く正確な名前を付け,命名規則が崩壊した場合は,全て再定義する.自明で驚きがなく,接続のよいインターフェースを,考えてから実装する.接続の互換性が崩壊した場合は,全て再実装する.技術的負債の,影響範囲が広がる前に,可能な限り早く改修する.追加の実装をする際に,負債を発生させないよう,先に,対象となるコードを分割し整理する.銀の弾丸は無い.したがって,一貫性は,このように維持する.

 負債の改修については,
5. 新しいコードを書くまえにバグを修正するか? - ジョエル・テスト - Joel on Software [日本語訳]
を参考にするとよい.
Summary
 ここまで説明した UNIX 哲学を,正確でなくとも,ある程度意味を理解出来ていれば,自然とまともなコードが書けるようになる.(「私のコードがまともかどうか」には議論の余地があるが...).まとめとしては,冒頭付近で示した UNIX 哲学そのままである.しかしながら,本投稿では,「UNIX 哲学についての私の理解」を説明する形となっているため,これについてまとめると,次のようになる.

 自明なコードを書くコツ
・一貫性を確保する.
・短く実装する.
・短く正確に命名する.
・先にインターフェースを設計する.(関数に分割してから実装する.)
・一関数に一機能.複雑な関数単一機能関数は,単一機能の関数を組み合わせて実装する.
・効率よりも移植性.
・自明なコメントを書かない.
・処理をまとめる.
  関数化までしない場合でも,異なる処理は改行で分割し,一続きの処理は改行ぜずに固める.
・複雑性を隠蔽する.

 自明なコードを維持するコツ
・不要なコメントは削除する
・不要な実装は削除する.
・命名規則が崩壊した場合は,全て再定義する.
・インターフェースが崩壊した場合は,全て再実装する.
・技術的負債は,放置すると影響範囲が広がるため,可能な限り早く改修する.
 (バグも同様.後回しにすると,バグを再発見するためのコストがかさむ.ただし,納期とは相談する.)
・追加の実装をする前に,対象となるコードを分割・整理する.(そもそも,技術的負債を発生させない.)
・解析コードなど,大規模なコードは,アプリケーション部分とライブラリ部分に分離する.
Epilogue
 ソフトウェア・エンジニア諸君には退屈な記事であったと思う.あるいは,冗長であるとか,間違いであると感じたかもしれない.この記事は,これからソフトウェアの品質について考えようという学生や駆け出しエンジニアが,何かしらの糸口を掴めればと執筆したつもりである.その使命を達成できたかは定かではないが,最後に,日の有名な「プログラマの三大美徳 [5]」について話をしたい.これは「怠惰 (Laziness) [5]」「短気 (Impatience) [5]」「傲慢 (Hubris) [5]」と言われ「プログラマに必要とされる効率や再利用性の重視・処理速度の追求・品質にかける自尊心 [5]」を示す.これらを達成するためには,実際のところ,莫大な時間と労力が必要になる.しかしそれは,結局のところ「当たり前のことを当たり前にする [11]」のは結構大変だということに他ならない.そして,少なくとも天才でないのなら※1,ソフトウェア・エンジニアリングに泥臭い以上の「もっとカッコいい仕事 [11]」はない.全てを薙ぎ倒してくれる「銀の弾丸 ※2」はないのである.広く噂話を拾い集めれば,仕事としてプログラムを書き捨てる,あるいは様々な事情により他に選択肢を持たない人々は意外と多いのかもしれない.複雑に組み上げられたソフトウェアを正しく動作させるためには,多くの労力を必要とする.それは,単に仕事だからと頑張るには,少し大変だと思う.しかし,だからこそソフトウェアを組み上げるには,自尊心だったり,ある種の情熱が必要となる.高い品質を維持するオープンソースのソフトウェアが多く存在しているのは,これらの思想を正しく理解し,資本によって時間を定められず,その情熱によって必要な労力を注ぐことができるからであろう.私はこれまで,あるいは現在でも「品質にかける自尊心」といった曖昧な表現を理解できていないが,事実「自尊心」の無いソフトウェアは悲惨である.ソフトウェア開発には,その正しさ,一貫性を追求する姿勢が欲しい ※3

※1.「お前は天才ではない.普通のことを普通に行うことを心がけろ [出典不明]」より.
※2. 読まれない名著 [13]「人月の神話(にんげつのしんわ)[12]」の一節「銀の弾などない」より.
※3. 少しコードが大きくなると,自分で書いたコードすら思い出せない.このとき,コードが秩序立っていれば,簡単に推測できるので,覚えておく必要もない.しかし,そこに矛盾したコードがあると,やんごとなき事情によるのか,単なるミスなのかすら,判断できない.(特に「やんごとない事情」の場合は,コメントが必須となる.)
Acknowledgments.
  毎年アドベントカレンダーは,ネタがなかったり,時間がなかったり,ともかく,指を咥えて何もやらない日々であった.しかし,今年は,稚拙でかなり冗長な内容ではあるが,記事を書き参加する側になることができた.カレンダーの企画者である みゅーもり様,誘って頂いた もらとりあむお271様と,記事の題目を選定するためのアンケートに答えて下さった TL の皆様,言葉を借りた引用元の著者の皆様に,感謝申し上げたい. ※ どうやら,このアンケートには,無効票が多数投じられているとか,いないとか.(つまり,哲学に興味を持った方は,このアンケートが示す結果より,ずっと少ない,らしいです.興味を持っていなくても,お気になさらずに...)
References
 [1] UNIX哲学 - Wikipedia - 2018年11月24日閲覧
 [2] UNIX - Wikipedia - 2018年11月24日閲覧
 [3] Multics - Wikipedia - 2018年11月24日閲覧
 [4] アントニー・ホーア - Wikipedia - 2018年11月24日閲覧
 [5] プログラマ#プログラマの三大美徳 - Wikipedia - 2018年11月30日閲覧
 [6] 変数名の命名規則 ケースの使い分け - Quitta - 2018年11月30日閲覧
 [7] UNIXという考え方 - その設計思想と哲学
 [8] 『UNIXという考え方』読書メモ - Qiita - 2018年12月01日閲覧
 [9] UNIXという考え方 The UNIX philosophy - gist.github.com - koudaiii - 2018年12月02日閲覧
 [10] ソースコードって実際のところどういうふうに書いていますか? - note - Rui Ueyama - 2018年12月01日閲覧
 [11] 未来のいつか/hyoshiokの日記 - 9月末で60歳定年退職しました - 2018年12月02日閲覧
 [12] 人月の神話 - Wikipedia - 2018年12月02日閲覧
 [13] 読まれない名著「人月の神話」を本気で読み込んでみた(まとめ) - 2018年12月02日閲覧


Appendix
 ここでは,少し大きな話や,本題から外れる話をする.
どのように設計するか
 ソフトウェアの基本設計は,複数人で行うと破綻する.互いに影響を及ぼし合っているため,仕事を分割することができない.そのため,最近では多人数ではなく,少人数の優秀なエンジニアを集めて開発する.そして,各機能を独立して設計できるところまで基本設計を落とし込む.ここまでしてようやく,開発速度が開発人数に対してスケールする.このような,他の機能に影響されず,追加するだけで拡張できる設計は優れている.ただし,ソフトウェアの開発人数が増えると,コミュニケーションによるオーバーヘッドの増加と,バグ混入率の増加は自明である.そのため,凡庸な多人数のチームより,優れた少数のチームがより大きな成果を挙げるのは自明である.そして,プロジェクトの難易度に対して凡庸に満たないと,おそらく破綻する.私が説明しても仕方がないので,Ruby 開発者の Yukihiro Matsumoto 氏のツイートを引用すれば, である.

- 余談 -
人間がシステムに合わせる
 これは,マネジメントの問題であるが,顧客の要望に合わせたシステムを構築することは,ソフトウェアを複雑にするだけである.多くの問題は,適切なシステムに業務フローを合わせることで解決されるが,管理者 (ないし組織) によっては,システムに全ての問題を押し付るべく,現状の業務フローにシステムを合わせようとする.このとき,この取り組みは上手く行かない.
- 閑話休題 - .
法則
 感覚的に理解している事象には,名前が付けられていることがある.目に止まった著名な法則を,ここにメモする.

ブルックスの法則
 遅れているソフトウェアプロジェクトへの要員追加はさらに遅らせるだけだ ※1

コンウェイの法則
 ソフトウェアのどの部分であれ,それを作った組織の構造を反映する ※2
解説)「ソフトウェアを開発するチームを5つに分けて進めた場合,ソフトウェアの構造も大きく5つに分かれた形で開発され ※2」る.

マーフィーの法則
 「不都合を生じる可能性があるものは、いずれ必ず不都合を生じる ※2

パーキンソンの法則
 「仕事の量は,完成のために与えられた時間をすべて満たすまで膨張する ※1
解説) この記事の執筆に,恐るべき量の時間と労力が吸い取られているのは,この法則に従っているからに他ならない.締め切りと仕事量は自分で決めなければならない.残業がいつまでも無くならないのはこのためだ.なお,「残業しないで定時帰り」を23ヶ月間続けた記録【人生を取り戻す】では,残業をしない方法を「とにかく,帰る.それが唯一無二のルール」としている.我が指導教官が締め切りを絶対に譲ってくれなかったのも,同じことだ.決められた期限の中でなんとかしなくてはいけない.

※1. 人名を冠したソフトウェア開発の19の法則 - 2018年12月02日閲覧
※2. ソフトウェア開発技術者が知っておくべき5つの法則 - 2018年12月02日閲覧
自由でポータブルな環境
 生憎と手元に原本がないが,記憶によれば,※1. に「職場が変わり,マシンが変わるごとに,今まで出来ていた解析処理ができなくなり,困っている研究者を何度か見た」というような一節がある.家業は違えど,ソフトウェア・エンジニアも一緒である.OS やハードウェアは進化し続けるし,そうでなくともこの御時世,ずっと同じ会社に勤めていられるとも限らず,まして,ソフトウェア業界は良くも悪くも流動性が高い.たとえ何処にいても,同じように仕事ができる方が望ましいだろう.そういった意味では,どこでも動く移植性に優れたコードは素晴らしいし,それがライセンス的にも自由なソフトウェアであればなお素晴らしい.加えて,自分自身も,市場価値に乗ったどこでも通用するポータブルなスキルを身に着けられれば,いうまでもない.

※1. 松山洋・谷本陽一 2005 『 UNIX/Windows を使った実践気候データ解析―気候学・気象学・海洋学などの報告書・論文を書く人が知っておきたい 3 つのポイント』,古今書院.
リンク集
 さて,実はここまで書くのに既に 1 週間と 4 日程費やしている.本文が終わったと思ったら,次は付録だって?絶対に記事を分割すべきであったし,ここまでの労力を注いだにも関わらず,正直上手く作文できている自信はない.自信はないけれども,一度スイッチが入れば,良し悪しは別として,兎に角,作業はできるようだ.ここに示すのは,そういった話.
 何もできない時に,スイッチを入れるには,どうするか.
 射撃しつつ前進 - Joel on Software [日本語訳]

 一つ恥ずかしい話をしよう.オブジェクト指向,つまりクラスの書き方を覚えた頃だ.私は,あるコードを書いていて,それがクラスで表現できそうだと思った.まだ,関数が不揃いで,修正の余地はあったが,私はクラス化を進める内に解決できるだろうと考えた.大した量ではなかったが,実際に実装してみると,思いの外時間がかかった.そして,新しい機能を追加しようとすると,更に多くの時間を必要とした.設計が間違っていたことは明らかだった.結局,私は,コードを幾つかの関数へと分割し,その関数群を,なんの変哲もないただの名前空間へと押し込んだ.
 基本的にトリッキーなことはやらないことだ.トリッキーなコードは,書くのも修正するのも時間がかかる.凝った難しい実装よりも,簡単に実装できるなら,その方がいいよね?というお話.(もちろん必要なら書くけれど.)
 ダクトテーププログラマ - Joel on Software [日本語訳]
 もうひとつ関連する話題として,オブジェクト指向がはやり過ぎて何でもかんでもオブジェクト指向で書こうとしていた時代 (があったらしい) に書かれたと思われる記事 (オブジェクト指向が不要と言っているのではなく,必要十分でよいという話).
 正しくオブジェクト指向できているどうかという意味のない議論

 どうしてなのかは分からないが,Joel on Software は有名だ.他の記事も面白そうなので,リンクしておこう.(全文読んだことは無いけれど.)
 Joelonsoftware.com - wiki - Japanese [検索]
 Joel on Software 日本語訳 まとめ - komiyakの通り道 - hatena
頻繁に使用する名称
 名前は重要である.略語を知らないなら調べるし,不安でも調べるし,合致する英単語が分からなくても,やはり調べる.中途半端な名前を付けてはいけないし,名前が衝突したら,衝突を避け得る新しい名前を考えなくてはいけない.一貫性が崩れる前に.通常は,思いつく名前や,記法を 3 から 8 通り,または,全通り書き出してみて,一番理解がよく,間違いの少ない文字列を選ぶ.参考に,私が頻繁に利用する語句を,表1. および 表2. に示す.

表 1. 頻繁に使用する名称と略語.プログラミングで頻繁に使用する名称とその省略形.(アルファベット順).
省略形 / 記法 原型 意味
- all 全部
arg argray 引数
arr array 配列
bin binary バイナリ
buf buffer バッファ
X_byY / XByY by Y によって X する
c / char charactor 1 文字
cmp compare 比較する
cnv convert 変換する
dbg debug デバッグ
- dummy ダミー (どうしても必要な場合は利用)
dir directory ディレクトリ
flag / flagX / flag_x flag X フラグ
gen generate 生成する
- list リスト
img image 画像
isX - Is X Y. : X は Y であるか?
(bool で真偽値を返す)
len length 長さ
lhs - left hands side: 左辺値
m / mat matrix 行列
num number
op / ope operator オペレータ.演算子
p - ポインタの p
- path パス
ret / ret_x / retX - return value: 戻り値
rhs - right hands side: 右辺値
- result 結果
rm remove 取り除く.(rm コマンドより)
- s - 複数形の s
s / str string 文字列
- split 分割する
tf / TF / xTF - true or false (bool 値フラグとして扱う)
tmp temporary 一時的な
v / vec vector ベクトル
x_withY / xWithY - Y と一緒に X する
x_withoutY / xWithoutY - Y を除いて X する
xExist - X exist. : X は存在する
(bool で真偽値を返す)
2 (英字続きの場合は数字)
_to_ (数字続きの場合は英字)
to 変換の向きを示す
4 (英字続きの場合は数字)
_for_ (数字続きの場合は英字)
for 要因を示す


表 2. ペアで使用する単語.(アルファベット順).
意味
begin end 始まり / 終わり
first last 最初 / 最後
next previous 次の / 以前の
start stop 開始 / 停止
src (source) dst (destination) 始点 / 終点
in out 入力 / 出力
row col 行 (rows なら行数) / 列 (cols なら行数)



Impression (所感)
  どこの世界もそうであるが,ソフトウェア・エンジニアリングの世界も当然例外ではなく,自分の知らないことは山のようにあり,自分より遥かにできる人は巨万といる.そんな中で,場合によってはセンシティブとなり得る内容を取り扱うことに,不安は尽きない.今回,本ブログで,技術的でなく,思想的な話をする切っ掛けとなったのは,(情けないことに) アドベントカレンダーへ投稿する手頃なネタを持ち合わせていなかったからに他ならない.UNIX 哲学について言えば,既に web 上に数多くの記事が掲載され,本ブログで改めて取り上げる特筆性があるかと問われれば,明確に NO である.しかしながら,ここ最近,コードの構造化が十分にされず,スパゲッティと化したコードを整理する機会い恵まれ,このまま放置しておく訳にもいかなかったのは事実である.思えば,私自身も,学部生のころ,教官にご指導頂いた経験があり,ソフトウェアの品質について考えを巡らせるようになったのは,それ以降である.当時は,私が「とんでもない」コードを書いて教官を困らせていた訳であるが,今度は私が頭を悩ます側へ回ったに過ぎない.曰く「動くソフトウェアは書けるね」だそうで,当然この言葉の裏には「単に動くだけではなく,十分にメンテナンス可能で,持続的に開発を続けられる品質のソフトウェアでなければならない」という真意が隠されている.これは,本投稿のはじめに話した「品質の高いソフトウエアとは,どのように定義されるだろうか」に通ずる訳である.

0 件のコメント:

コメントを投稿