2015年4月5日日曜日

読みやすいソースコードを書く方法

読みやすいソースを書くために気をつけていることを軽くまとめてみます。ソースコード例は Python ですが、特に言語に関係なく通用する話題です。業務などでコーディングする際の参考になれば幸いです。

良い名前を付ける

最優先で気をつけるのはこれです。「プログラミングは『名前』が9割」と言われることもあるくらい、名前付けには時間を割くべきだと思います。良い名前を付ければプログラムの見通しが良くなり全体像を把握しやすくなりますし、他人(または未来の自分)が保守しやすいソースコードになります。

では良い名前とはなんでしょうか?自分なりにまとめてみました。

名前と内容を一致させる

関数名なら処理内容を端的に表す名前にすべきですし、逆に名前から想像できない処理はしてはいけません。

私は実際に、副作用を持った operator +() の実装を見たことがあります。内部でメンバ変数を書き換えていたのです。 operator +() は C++ の機能で、自作クラスに関する加算(a + b)を独自に定義できるのですが、加算演算子ですからもちろん(外から見える)副作用を持つべきではありません。これは、関数が名前から想像できない処理をしてしまっている例です。

動的型付け言語ではもっと注意が必要なことがあります。例えば get_host() という関数を見たとき、これが Host クラスのインスタンスを返すのか、ホスト名を文字列として返すのか、新人さんは迷うはずです。ただ get_host() 自体は適切な関数名ですから、これを変えるのは難しいです。それより、ホスト名を扱う変数は常に host_name という名前にする、 get_host() の戻り値を host_obj という変数で受け取るなど、ホスト名と Host インスタンスを区別するように意識しましょう。

boolを返す関数はcheck…にしない

例えば hoge.check_available() だと hoge が有効なのが戻り値が真のときか偽のときか分かりません。 hoge.is_available() であれば紛れはありません。if 文の条件に書いたときに英文として自然になる名前を選ぶようにすると良いでしょう。

is, has など英語の三人称単数形を使うと適切なことが多いですが、敢えて動詞を使わないこともあります。例えば C++ のコンテナには、要素が空かどうかを調べる関数 empty() があります。これは形容詞だけの名前ですが、下手に check_empty() とするより分かりやすい関数名ですね。

コメントを付ける

これも基本中の基本でしょう。コメントはソースコードの理解を大幅に助けてくれますし、自分が後で読み返すときも、ソースコード断片を一つのかたまりとして思い出すのに大いに役立ちます。

ただ、闇雲にコメントを書けばよいかというとそうでもありません。多くの入門書にも書いてあることですが、コメントにも悪いコメントと良いコメントがあります。一般に、ソースコードから一瞬で分かることをコメントで書いてはいけません。関数コメントの例で言えば、get_size() に対して「サイズを返す」とコメントするのは単なるタイプ数の無駄というものです。関数名だけで十分な情報は伝わっていますから。一方で get_host() に対し「Host クラスのインスタンスを返す」とコメントするのは価値があります。この関数がホスト名でなく Host クラスのインスタンスを返すことが分かるからです。

一見して分かる情報はコメントしないという一般原則を頭に置きつつ、以下ではコメントのパワーが最大に発揮できる場面を考えていきましょう。

関数コメント

関数コメントは本当に役立ちますので、是非書いて下さい。関数コメントは普通、その関数の利用者に向けて書くものですが、関数の実装者にとってもその関数をより良いものにするために役立ちます。容易くコメントが書ける関数は、利用者にとっても直感的に使える関数になりやすく、長く複雑なコメントを書かねばならない関数は、利用も困難な関数になりがちです。関数の利用しやすさを図る尺度として、関数コメントが役立つのです。

関数コメントには最低限、その関数が何をするかという一言を書いて下さい。簡単な関数ならそれだけで十分です。少し大きな関数には、引数と戻り値の意味をコメントします。実践に則した関数の使用例があるとなお良いです。Python なら関数コメントに実行例を含めて python -m doctest hoge.py などと自動テストができますから、そのテストに合格するように書くと完璧です。

def fetch_hosts(conn, klass):
    '''class が `klass` であるホストオブジェクトすべてをテーブルから読み込んで返す。

    conn    - DB コネクションオブジェクト
    klass   - Host クラスのサブクラスオブジェクト

    >>> conn = MySQLdb.connect(host='127.0.0.1', db='hostdb')
    >>> hosts = fetch_hosts(conn, AppHost)
    >>> conn.close()
    '''

    # do something

ソースコードコメント

ソースコード断片に対してコメントを書くことも可読性の向上に役立ちます。ソースコードを意味のまとまりごとに空行を入れて分け、それぞれのソースコード断片に対して1行から数行のコメントを書くと調度良い感じです。コメントは、その断片が何をしようとしているのか、意図を書くようにします。例えば、注文履歴をもとに各商品が何個売れたかを計算するプログラムにコメントを書いてみます。

# 昨日の注文履歴をすべて取得する。
begin_dt = date.today() - timedelta(days=1)
end_dt = date.today()
sells = fetch_sells(conn, begin_dt, end_dt)

# 商品ごとの注文数をカウントする。 { 商品名 : 注文数 }
item_sells = defaultdict(int)
for sell in sells:
    item_sells[sell.item_name] += sell.count

このように、それぞれのソースコード断片が何を目的としているかを簡単に書いておくと、他人がソースコードを理解するときにも、自分が後で処理の全体像を読み返すときにも役立ちます。プログラムを単に日本語に変換しただけのような、ソースコードを読めば分かるコメントにならないように注意してください。

グローバル変数を使わない

グローバル変数やシングルトンなど、どこからでも参照できる変数があるとソースコードが読みにくく、保守しにくくなる場合が多いです。あるソースコード断片が上手く動くかどうかがその変数の状態に依存する場合、分かりにくいバグの原因となります。例えば、DBに接続してユーザ一覧を取得するプログラムを考えます。

setup_db()
user_db = UserDB.instance()
users = user_db.get_users()

setup_db() は UserDB.instance() を初期化してデータベースに接続する関数です。User.instance() はシングルトンの実装になっていますので setup_db() はあるプロセスの中で1回だけ呼び出せば良く、他の場所でユーザ一覧が欲しいと思えば以下のように書くことになります。

# setup_db() ?
user_db = UserDB.instance()
users = user_db.get_users()

ここで、プログラマは setup_db() を呼び出すべきかどうかを考えます。もし、この部分より前で setup_db() が呼び出されるなら、基本的にはここで呼ぶ必要はありません。しかし、もしこの2行が直前の setup_db() の呼び出しと別プロセスで動くなら、再度 setup_db() を呼ぶ必要があります。別プロセスで動くかどうかはこの2行からは判断できず、プログラム全体のアーキテクチャを理解する必要があります。この例では、グローバル変数の使用が保守性を著しく低下させています。