xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

脱・読みづらいコード!今日から一段上のプログラマーになる方法 5 選

「ソースコードを綺麗に書く」というのは、プログラマーであれば誰しもが心掛けたいと思っている極めて重要な事柄です。そもそも「綺麗なコードってなんぞ?」という感じですが、いくつかあると思います。

  • 改行位置/空行の数/インデントなどに一貫性があり整っていて、パッと見が綺麗
  • 「こんな難しいことがこんなに簡単に書けるなんて!」というエレガントさ
  • 理解しやすい
  • etc...

ひとつ目は美的感覚の問題なので、基本的には人それぞれということで今回は言及しないことにします。チーム開発の場合はソースコード整形ツールで一定のクオリティを維持するのがお手軽ですね。ふたつ目もさておき、今回は「理解しやすさ」について考えてみようと思います。主にプログラミングの勉強を始めた人向け。

リーダブルコード

※ 画像を借りているだけで、この本のオススメをする記事ではありません!

1. そのコードを誰が読むのかを考える

「理解しやすいコード」は「可読性の高いコード」と言い換えても良いでしょう。書いた人の意図がすぐ理解できることは保守性が高いということなので、可読性は非常に重要視されます。自分が書いたコードは自分で読めて (ほぼほぼ) 当然なのですが、保守担当が他の人に変わったり、チームの人にコードレビューしてもらったりするときに時間や手間を取らせないことが大切です。つまり、将来の自分と他人がこのコードを読むんだということを常に頭に入れておかなければなりません。じゃないと後で痛い目を見ます。

  • テクぃコードを書いたときはコメントをしっかり残す
  • 参考にした URL を貼っておく
  • コメントに図とか絵を描いて思考の跡を残しておく

しばらく年月が経ってコードを見直したとき、そのコードから意図を汲み取れるかどうか。別の人にこの処理の意図をコードファイルだけで伝えらえるかどうか。すべて、自分が書き落としたコードだけで説明できないとダメなんです。そういう意識を持ってプログラムを書くように心がけましょう。

2. 粒度を揃える

では、普段相手に何かを説明するときどうしているでしょうか?ちょっと例を出してやってみましょう。

A : キミの自宅 (ex.福井) から東京ディズニーランドまで、どうやって行くの?

多くの人はまずこう答えるでしょう。

B : (小松空港から) 飛行機で

私が 5 歳ぐらいの子に同じ質問してもそうでした。まさかここで

B : 玄関を出て家の前の道を左に進んで、次の交差点を右に曲がって福井 IC から高速道路に乗って、安宅 IC で降りて小松空港に行って、チェックインして手荷物を預けて...

などとダラダラと説明したりはしませんよね。「何言ってんの?早く結論言えよ」みたいな気持ちになります。これが俗にいうスパゲッティコードです。つまり、コードの書き方は普段の会話と同等であるべきということです。質問の粒度と回答/解答の粒度が同程度でないといけません。なぜならそれが一番理解しやすいからです。コードという媒体を通して説明するのだとしても、それは全く同じです。

なので以下のような粒度でコードが書いてあると理解しやすいわけです。ダラダラと説明されたら (= コードが長ったらしく書かれていたら) 何をしたいのか分かりにくいというのは当然のことでしょう。

//--- あくまでイメージ
public void 自宅から東京ディズニーランドに行く()
{
    自家用車で小松空港に行く();
    飛行機で羽田空港に行く();
    高速バスで東京ディズニーランドに行く();
}

よく「関数の中に書いていいコードは 20 行までだよね~(キャハハ」みたいなメンドクサイことを言う人がいますが、「質問の回答は明確かつ端的に!」という点ではあながち間違っていません。とは言え、その行数が重要なのではなく、そういった強制をすること自体が重要なのではなく、あくまで粒度を合わせるということに意識を向けましょう。逆に言えば、粒度さえ合っていればコードは長くても何も問題ないと思っています。

3. コメントコーディングで鍛える

コードの粒度を揃えるトレーニングとしては、コメントコーディングという方法をオススメします。いきなりコードを書き始めるのではなく、そこで何をしたいのかをまずコメントとして書き起こします。以下の例を考えてみましょう。

ビットマップに線の太さ2で半径3の赤色の円を描く

void Main()
{
    ビットマップに線の太さ2で半径3の赤色の円を描く();
}

void ビットマップに線の太さ2で半径3の赤色の円を描く()
{
    //--- 描画対象となるビットマップを作る
    //--- 描画オブジェクトを作る
    //--- 太さ2の赤色をペンを作る
    //--- 作ったペンと描画オブジェクトで半径 3 の円を描く
    //--- ペンを破棄
    //--- 描画オブジェクトを破棄
}

関数名がやりたいことにあたります。非常に面倒に思うかもしれませんが、その中に ToDo を並べていきます。そしてこの ToDo リストの粒度が関数名に対する説明として適切かどうかを検討します。もし粒度が細か過ぎる場合は、その処理はもうひとつ関数を作ってその中に書くべきです。ちなみに、もしここで Main 関数に直接 ToDo リストを並べていたら完全なスパゲティコードなので注意してください。Main 関数に書かれているコードの粒度が細か過ぎるためです。

しかし、このままでは汎用性が低いです。定数になっている部分はパラメーターとして外出してしまいましょう。以下のようになりますね。

void Main()
{
    ビットマップに円を描く(3, 2, 赤);
}

void ビットマップに円を描く(半径, 太さ, 色)
{
    //--- 描画対象となるビットマップを作る
    //--- 描画オブジェクトを作る
    //--- ペンを作る
    //--- 作ったペンと描画オブジェクトで円を描く
    //--- ペンを破棄
    //--- 描画オブジェクトを破棄
}

では、上記のコメントに対応するようにコードに落とし込んでみましょう。だいたい以下のようになります。

void Main()
{
    ビットマップに円を描く(3, 2, Color.Red);
}

void ビットマップに円を描く(int radius, int width, Color color)
{
    //--- 描画対象となるビットマップを作る
    var canvas = new Bitmap(...);

    //--- 描画オブジェクト / ペンを作る
    using (var graphics = Graphics.FromImage(canvas))
    using (var pen = new Pen(color, witdh))
    {
        //--- 作ったペンと描画オブジェクトで円を描く
        graphics.DrawEllipse(pen, 0, 0, radius, radius);
    } //--- ペン / 描画オブジェクトを破棄
}

コードを見れば分かると思いますが、ここまで来るともはやコードの表現は (ほぼ) コメントそのもの表現していることになります。なのでコメント自体要らなくなるでしょう。これがよく言われる「コードに書いてあることをコメントで書かない」です。だって冗長ですものね。...と「コメントをいっぱい書くやつは 2 流」みたいなことも言われますが、僕はコメントはいっぱい書いて良いと思います。分かりやすいならその方がイイじゃない。意図が伝わらないよりも 100 億倍マシ。ただし、コメントがメンテされておらず嘘になってしまっているのはダメです。

上手になってきたらコメントコーディングは減っていくと思います。むしろ頻度が減ったら粒度を揃えて書けるようになっている証拠です。トレーニング以外にも、複雑なコードを書くときや、頭の整理ができていないときにも使いましょう。(まぁ普通やるよね

4. 概念を捉え、切り出す

上の例題では「ビットマップに円を描く」ということについて考えてみました。しかし、これは以下のように考えることもできます。

  • 円というモノについて考える (= 「円とは」と自問自答する)
  • 円は自身の半径 / 線の色 / 線の太さを知っている
  • 円は自身を描く方法を知っている
  • ただし、円自身は何に自分を描いて良いかは知らない

こう考えた場合、先ほど例題は以下のように書き換えられます。

void Main()
{
    var target = new Bitmap(...);
    var circle = new Circle(3, 2, Color.Red);
    circle.Draw(target);  //--- 「円に描け」と命令している!
}

//--- 「円」という考え方を新たに導入し、定義する
class Circle
{
    //--- ここで定義した円は以下のパラメーター (= 状態) を持っている
    public int Radius { get; }
    public int Width { get; }
    public Color Color { get; }

    //--- 円の作り方 (= コンストラクタ)
    public Circle(int radius, int width, Color color)
    {
        this.Radius = radius;
        this.Width = width;
        this.Color = color;
    }

    //--- 円は自分の書き方を知っている
    //--- 引数は何に描くか (ここでは画像ということにしている)
    public void Draw(Image target)
    {
        using (var graphics = Graphics.FromImage(target))
        using (var pen = new Pen(this.Color, this.Witdh))
            graphics.DrawEllipse(pen, 0, 0, this.Radius, this.Radius);
    }
}

ここで非常に重要なことは「円を描け」と言っているのではなく「円に描け」と命令していることです。新しく円という概念を切り出しています。この概念の切り出し作業が「設計」 (= ここではオブジェクト指向設計) です。この例ではあまり感じられないかもしれませんが、これを上手くしないと開発生産性や保守性が著しく損なわれます。

なぜこのようなことを言っているかというと、概念の切り出しを上手く行うことは粒度を綺麗に揃えることと役割分担に通じるからです。例えば以下のようなやり取りを考えてみてください。

開発部の社員 A : 「この備品 (C) の購入をお願いします」
総務部の社員 B : 「わかりました、購入したらお渡しします」

これをコードで書くと、例えばこんな感じです。

var employeeA = new 開発部の社員();
var employeeB = new 総務部の社員();
var itemC = employeeB.Buy(employeeA.WantItem());
employeeA.Set(itemC);

このとき、開発部の人が総務部の人の購入作業を代理してしまっては当然ダメですよね。それだけでなく、総務部の購入作業の詳細 (ex. ローソンに行って備品 C を探し、レジに持って行ってお金を払う) が開発部の社員に筒抜けになっていてもダメです。開発部の社員と総務部の社員、それぞれが全うすべき職務は基本的に不可侵であり、どういう作業をしているかを細かく知る必要はないわけです。

  • どういう概念 (= クラス/構造体/メソッド/プロパティなど) で切り出すのか
  • 誰 (= インスタンス) がするべきのか
  • その人は何を知っている (= プロパティ) べきなのか
  • どういう作業 (= メソッド) をさせるべきなのか
  • 何を教えたら (= 引数) その作業ができるのか
  • その作業の結果 (= 戻り値) は何か

このあたりをしっかり意識して設計すると綺麗なコードになりやすい、ということですね。冒頭にも書きましたが、普段の会話とそう遠くない表現がコード上で行われるように意識することが非常に重要です。コード上でそれを表現するために概念の切り出しを上手に行いましょう。

5. 適切な名前を付ける (= ネーミング)

「概念を切り出す」ということは「その考え方自体に名前を付ける」ということです。先ほどの例では「円要素」という考え方を導入し、それに Circle という名前を付けました。まさかこれに Line (線) と付けることはないでしょうし、SpecializedEllipse (特殊化された楕円) と名付けることもないでしょう。

関数名もプロパティ名も、突き詰めればローカル変数名ですら全く同じで、それ自身を最も端的に表現する名前を付けるべきです。逆にネーミングをしっかりすることさえ意識すれば、自ずと概念は綺麗に分割されていくはずです。その名前が利用されるスコープが十分狭いのであれば、名前を一単語にしてもよいですし、x という一文字の名前でもよいです。

また、名前を適切に付けるということは「その処理をどう読ませたいか」ということです。例えば以下のふたつは処理結果としては同じですが、より意図が通るものがどちらかという後者でしょう。であれば後者で記述するのが良いと思いませんか?

//--- NOT コレクションに要素がひとつでもある (= ひとつもない)
var result = !collection.Any();

//--- コレクションは空だ
var result = collection.IsEmpty();

個人的にはネーミングこそすべてと言ってもよいぐらい、名前の付け方は最後までこだわるべきです。

注意

ただし、この概念分割の作業をやり過ぎるとコードの見通しが悪くなることもあります。納期が短かくて突き詰められなかったり、影響範囲を極小にするために理想とは思えない泥臭いコードを書かなければならないこともあります。

やりたいことを実現するためには「どこかに」そのコードを書かなければなりません。分割すれば複雑さも分散して可読性が向上する傾向にはありますが、場合によっては愚直に書き並べる方が分かりやすいこともあります。そういったサジ加減も大事にしていけると現実的で非常に良いと思います。

まとめ

ここまでダラダラと書いてきましたが、今一度今回紹介したエッセンスをまとめておきましょう。

  1. そのコードを誰が読むのかを考える
  2. 粒度を揃える
  3. コメントコーディングで鍛える
  4. 概念を捉え、切り出す
  5. 適切な名前を付ける

まずはこれらを意識して「アイツのコードってクッソ読みづらいんだよなー」などと言われないようにしましょう!特にネーミングはキモ。ネーミングがキモですよ!(大事なので 2 回言いました

どういう文脈でこう書いているのかは正直わかりませんが、コードの読みづらさは基本的に英語とかそういう問題ではないと思うんです。

今回のお話は新人の頃から僕にプログラミングを教えてくれた @Fujiwo さんのウケ売りです。以下の資料は非常に大切なことが詰まっているので、是非読んでみてください。超オススメ!