良いコードとは
まついのぶゆき
良いコードとは
• (エンタープライズにおける)良いコードとは、「読みやすくて
理解しやすく、修正しやすいコード」のことである
• メモリ使用量やCPU使用量、I/O転送量が低いコードのことではない
• 少しでも高速に動作するコードのことではない
• ゲームや特殊な環境で動作するソフトウェアなどでは、こういうコードが「良
い」コードの場合もある
• トリッキーな手段を駆使してなるべく短くかかれたコードのことではない
• 競技プログラミングなどでは、こういうコードが「良い」コードの場合もある
なぜ「良い」コードを書くべきなのか
• エンタープライズでは、チームで開発することが多い
• 一人ですべて開発するのだとしても、3か月前の自分は他人
• エンタープライズでのコードは、「書かれる」よりも「読まれる」
ことが圧倒的に多い
• エンタープライズのコードは機能拡張が常に発生するため、拡張に
関連「しそうな」コードは全て読まなければならない
• 障害の発生時には、障害周辺のコードを素早く読まなければならない
• そのため「今動作する」ことが重要なのではなく、「読まれて
理解しやすい」コードを書くことが重要となる
「良い」コードの基本方針
• 読みやすい書き方を心がける
• ロジックをシンプルに保つ
• コードを再構成(リファクタリング)する
• テストできるように書く
読みやすい書き方
• 適切なネーミングを行う
• その変数や関数がやりたいことを端的に表現する明確な単語を選ぶ
• getなどの関数名や、resultといった変数名は、中身が何なのかわからない
• 明確な単語に情報を付加すると良い
• ファイルサイズを格納する変数には、uploaded_file_mbにするとか
• tmpやbufのような、汎用の名前は避ける
• ただし一画面で収まるスコープに限定される変数名の場合、使っても良い
• ループ変数などもスコープが限定されるため、i や j で良い
• 明確な名前が選べなかったり、非常に長い名前を付けたくなる場合は、
適切なモジュール分割ができていない
• canvas_max_pxって付けたくなる場合、max_pxをインスタンス変数に持つ
Canvasクラスが存在するのでは?
読みやすい書き方
• 適切なコーディングスタイルとネーミングスタイルに従う
• 言語に対してデファクトとなっているスタイルをなるべく利用し、
できる限りルールを発明しない
• 例えばPythonなら
• PEP8
• http://legacy.python.org/dev/peps/pep-0008/
• Google Python Style Guide
• http://google-styleguide.googlecode.com/svn/trunk/pyguide.html
• インデントや空行も意味があるので、スタイルに従う
• Pythonのようにインデントが制御構造に直接関係しなくても、意識して使う
読みやすい書き方
• 見た目の一貫性を重視する
• 例えば
• 同じ変数を引数にする関数が複数あるならば、同じ順序にする
• 同じ意味を持つ変数を複数のモジュールで定義するならば、同じ名前にする
• 読む人が「あれっ?以前でてきたアレでもこうだっけ?」とならないように
読みやすい書き方
• なるべく自分で書かない
• 言語が持っている機能やデファクトで利用されるライブラリの機能を
最大限活用する
• ソート処理や探索処理などは、セキュアで高速な実装が言語処理系から
たいてい提供されている
• 文字列処理等の誰でも欲しい機能は、Javaならばapache commons
Rubyならばactive supportなどのライブラリがたいてい提供している
• 言語仕様だけでなく、言語が提供する関数仕様やメジャーなライブラリ
仕様は一読しておくと良い
読みやすい書き方
• 適切なコメントをつける
• 「プロジェクトルールだから」と、意味のないコメントは付けない
• そのクラスやメソッドを実装しようと思った意図や設計思想をコメントする
• そもそもコレは何をするためのものなのか?
• なぜこのロジックを選択したのか?代替手段はあったのか?
• 自分で微妙と思っている箇所もコメントする
• 例えば
• このロジックは動作するけれど、データ量に対して計算量がO(n^2)になる
• このメソッドは破壊的なので、一度呼び出すと内部データは変更されてしまう
• コードを見たらわかることはコメントしない
• 処理の手続きをコメントしなければ理解できないコードは、「悪い」コード
ロジックをシンプルに保つ
• 条件分岐
• 単純で重要な条件を先に評価する
• ド・モルガンの法則やカルノー図を使うことで、条件を簡約化できないか
考える
• ド・モルガンの法則
• not (A or B) == (not A) and (not B)
• not (A and B) == (not A) or (not B)
ロジックをシンプルに保つ
• カルノー図の効用(by @kawasima)
• 例えば以下のような条件が提示された場合
ロジックをシンプルに保つ
• カルノー図の効用(by @kawasima)
• 普通に実装すればこうなるけれど
ロジックをシンプルに保つ
• カルノー図の効用(by @kawasima)
• カルノー図を書いてみるとこうなるので
ロジックをシンプルに保つ
• カルノー図の効用(by @kawasima)
• もっとシンプルな条件で実装できる
ロジックをシンプルに保つ
• 関数内からはなるべく早く返す
• 判断した条件をずっと覚えておくよりは、異常時にはさっさと関数から
抜け出してしまった方が読みやすい
def construct_msg(num_pageview):
msg = ""
if (isValid(num_pageview)):
foo()
…
bar()
…
msg = buz()
else:
msg = "invalid"
return msg
def construct_msg(num_pageview):
if (not isValid(num_pageview)):
return "invalid"
foo()
…
bar()
…
return buz()
ロジックをシンプルに保つ
• トリッキーなコードは書かない
• 言語仕様上許されているからと言って、トリッキーなコードを書いて
「オレスゲー」しない
xx = 1
yy = 2
def f(x,y):
return x + y + xx + yy
globals().update({"xx":1,"yy":2,"f":lambda x,y:x+y+xx+yy})
=
ロジックをシンプルに保つ
• トリッキーなコードは書かない
• 言語仕様上許されているからと言って、トリッキーなコードを書いて
「オレスゲー」しない
def check(x):
if x%2 == 0:
return even()
else:
return odd()
def check(x): return odd() if x%2 else even()
=
def check(x): return (x%2 and [odd()] or [even()])[0]
=
def check(x): return [even, odd][x%2]()
=
ロジックをシンプルに保つ
• 性能向上やメモリ削減のための特殊な実装は、最後の最後
• パレートの法則にあるように、性能劣化やメモリ消費を引き起こすコード
は、全体のうちのホンの一部
• 最初は読みやすいコードを心がける
• システム全体の性能を計測し、システムの性能劣化やメモリ消費に影響の
大きな箇所からピンポイントに修正する
• 本当に性能が必要なのだとしたら、ピンポイントにCで書き換えても良い
• というか、SQLチューニングが最も効果的だったりする
• ただし不用意なネストループやO(n2)の探索アルゴリズムなど、
わかりきった遅いコードは最初から避けるべき
※パレートの法則
「全体の数値の大部分は、全体を構成する一部の要素が生み出す」
「8:2の法則」
コードを再構成(リファクタリング)する
• 一画面に収まらない処理はモジュール分割できないか考える
• コードが実現したいこと(コメントとして一行で書ける目的)に直接関係
しない処理は、別のモジュールに切り出す
• ネーミングが適切に行われていれば、切りだされたモジュールのコード
を読まずともモジュールの処理内容をざっくり理解できるので、元の
コードが理解しやすくなる
• では、どのような方針で分割すべきだろう?
コードを再構成(リファクタリング)する
• the Open-Closed Principle(OCP)
• 「ソフトウェアの構成要素は、拡張に対して開いていて、修正に対して
閉じていなければならない」
• OCPが意識されているモジュールは、修正しても他のモジュールに
影響を与えない
=オブジェクト指向設計の原則
• API仕様の重要性
• 外部に公開しているAPIの仕様(公開されているメソッドのINPUTに対して何
がOUTPUTされるか、その際に発生する副作用は何か)が変わらないので
あれば、中のコードが書き換わっていても誰も気にしない
コードを再構成(リファクタリング)する
• 関数の副作用とは
• (数学的な意味の)関数=入力を出力に変換するモノ
• 入力を出力に変換する以外に、関数外部の環境へ影響を及ぼす行為
=副作用
• ファイルやネットワークの入出力、画面の入出力、データベースの入出力
などは全て副作用
• 関数外部の変数への入出力も副作用
• 副作用がない関数は、いついかなる状況で呼び出しても必ず同じ結果
• テストしやすく堅牢なモジュールになる
• 関数型プログラミング言語は、基本的に副作用の無い関数でシステム
を組み立て、副作用のある行為を限定することで堅牢なシステムを作る
• ただし「副作用」が全くないシステムには意味が無い
コードを再構成(リファクタリング)する
• 関数型プログラミングのエッセンスを取り入れる
• 変数のスコープはなるべく小さくする
• グローバル変数は敵
• 値を代入するのは一回だけ(同じ変数の使い回しや書き換えはしない)
• ループ変数など小さな関数内部に限定された変数は書き換えても良い
• 暗黙的に依存する変数を書き換えるような関数を作ると、思いもよらない箇所
で変数が書き換わるややこしいバグを生む可能性がある
• list.append()なども、変数の中身を書き換えていることに注意
• 例えばリスト内包表記を用いれば、元のリストはそのままで新しいリストが生成される
>>> x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> y = [i * 2 for i in x if i%2 == 0]
>>> y
[4, 8, 12, 16, 20]
>>> x
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
コードを再構成(リファクタリング)する
• 関数型プログラミングのエッセンスを取り入れる
• 関数に与える引数は、関数内で書き換えない
• JavaやPythonの関数の引数は参照渡しなので、仕様上は引数オブジェクト
の中身を書き換えることができるが、基本的には書き換えてはいけない
• 関数からの出力は、すべて戻り値として返す
• 例外発生時は、戻り値ではなく例外をraiseする
• 結局・・・
• 「外部入出力など副作用を前提とした部分」と「クラスの内部データに
依存した処理を行う部分」と「副作用が全く無い関数」を明確に分割し、
そのコードが影響を及ぼす範囲を局所化することが重要
コードを再構成(リファクタリング)する
• the Single Responsibility Principle(SRP)
• クラスの役割はただ一つだけ
• クラスのコメントに、「このクラスはXXをする役割を担う」と一文で表現できな
ければならない
• クラスの実装を変更する理由は、その「XXの役割」に拡張や修正があっ
たときだけのはず
• 「XXの役割の修正」以外の理由でプログラムを修正する際に、なぜか「XXの
役割」のクラスに修正が入るようであれば、クラスの分割が間違っている
コードを再構成(リファクタリング)する
• 適切なクラスを見出すために、デザインパターンを活用する
• イイカンジにクラスを分割するためのベストプラクティス集
• 最も伝統的なもの:GoF(Gang of Four)デザインパターン
• GoF以外にも、様々なデザインパターンが提唱されている
• ただし「パターンを使いたい」がためにパターンを使っては本末転倒
コードを再構成(リファクタリング)する
• コードの適切さを評価するために、メトリクスを活用する
• 凝集度と結合度
• モジュールのOCPやSRPを計測する尺度
• 凝集度は高く、結合度は低いのが望ましい
• コードメトリクスツールで数値化することができる
• 細かい数値にはあまり意味がなく、ざっくりとした傾向を見るために用いる
• McCabeの循環的複雑度
• コードの複雑さ(ループや分岐の度合い)を計測する尺度
• 一般的には、10以下が良いと言われている
• 30を超えると構造的に失敗したモジュールと言われており、50を超えるとテスト不
可能で、75を超えると微細な修正であってもバグが混入するらしい
テストできるように書く
• 副作用のない関数は、容易にテストできる
• なるべくシンプルに、網羅的に
• ただし闇雲に様々な値でテストを書くのではなく、なぜその入力で
テストをするのか、明確な理由を考える
• 限界値
• 期待している値の最大値を+1超えた値、最小値を-1した値等
• 特殊値
• 数値ならば0
• 文字(utf-8)ならば、ビットパターンが1byteになる文字(US-ASCII文字)、2byteに
なる文字(ギリシャ文字等)、3byteになる文字(常用漢字や丸数字等)、4byte以上
になる文字(JIS X 0213の第3・4水準漢字)など
• 文字(Shift-JISやCP932)ならば、「表」や「ー」など2byte目が0x5c(バックスラッ
シュ)になる文字や、CP932からUTF-8に変換することによって化ける文字(~)等
テストできるように書く
• 副作用のある処理
• スタブやドライバを駆使してテストを書く
• テストフレームワークが提供する機能をうまく活用する
• 適切にモジュール分割されテストできるように書かれた
プログラムを継続的にテストし続けることで、チームで
開発していても安心して機能拡張をすることができる

良いコードとは