⚖️

【ソフトウェア設計】ドキュメントとしてのコメント

2024/03/17に公開

はじめに

こちら最近書いてるソフトウェア設計シリーズです。モジュールをどう分割するのか?【ソフトウェア設計】モジュールになぜ分けるのか?などの記事を書いています。今回はコメントの話を中心に書いていこうと思います。

TL;DR

  • コード「も」仕様書、コメント「も」仕様書
  • Howでは無く、What。WhatよりWhy
  • Whatが多い場合はモジュール化も検討

コードは仕様書なのか?

「コードが仕様書か否か」はプログラマーにとってお気に入りの争いの一つです。ある人は「仕様書は要らない。コードを読め」と言い、ある人は「仕様書は大事」という話をします。それぞれの主張はあるでしょうが、この記事では 「コードも仕様書」 という立場を取ります。

この議論は、前提として 「仕様書とは?」 を定義する必要があります。ソフトウェア開発の現場では様々な仕様書が存在します。ともすればやや古典的ですらある分類として如何考えられます。

  • 要件定義書
  • 基本設計書 (外部設計書)
  • 詳細設計書 (内部設計書)
  • テスト仕様書

さらにそれぞれの仕様書と呼ぶものの中に、業務フロー図だったり、クラス図だったり、色々あります。「コードが仕様書か否か」を議論する場合は、暗黙のうちに詳細設計書を指す場合が多いと思います。特にその中でも話題になるのが プログラム設計書 です。プログラム設計書は変数名からクラスや関数定義、そしてプログラムのフローやロジックを書いたもので、この設計書の通りにコードを書くとプログラムが完成します。パンチカードやアセンブラの時代ならいざ知らず、このレベルの設計者と実装者を分ける意味はありません。高級言語であればプログラム設計書とコードは一致するので、二重管理や伝言ゲームのデメリットしかありません[1]。この意味において 「コードが仕様書」 は正しいです。

一方で、全体を俯瞰したりビジュアライズして理解するのにはソースコードそのものはあまり向いていません。ここにはシーケンス図や何かしらのドキュメントがあった方が良い場合が多いです。とはいえ、システム全体を理解するものではなく、システムの実際の詳細の振る舞いであればメンテナンス不備の観点も含めてコードを読むほうが正確ですし、対応するドキュメントをコードから直接生成するという手段もあります。特にメソッドの詳細や、APIの振る舞いなどはjavadocやOpenAPIなどコードと密接に関連させて生成するのが普通です。いずれにしても必要に応じたドキュメントを複数種類作るべきでしょう。ソースコードはドキュメントの一つではりますが全てではありません。

また、仮にコードを唯一の詳細設計書として考えた場合はいくつかの機能が足りません。特に決定的に不足しているのが 「なぜそう作るのか?」 というWhyの部分です。ソースコードは実際に書いた通りに動くのでHowの部分は完璧です。バグすら漏れなく記述されています。一方で、それ以上のことを読み取るのは困難なので、Whyを知るドキュメントとしてはそのまま使う事ができません。そして、それを補間する役割が 「コメント」 です。良いコメントは、ソースコードの 「システムを理解するためのドキュメント」 としての性質を劇的に高めます。

ドキュメントとしてのコメント

コメントとはソースコードにソースコードに最も近い位置にあるドキュメントであり、WordやWiki/Markdownで記述され、別で管理されているドキュメントと比較してメンテナンス性も高いので生産性と信頼性の高いドキュメントといえます。

では、どのようなコメントを書くべきでしょうか? 順を追って考えていきます。例えば初心者が 「コメントを書いてください」 と言われて記述しがちなコメントが以下となります。

// 整数を初期化する
var number = 10; // numberに10を割り当てる
// 文字列を初期化する
var message = "Hello, World!"; // messageに"Hello, World!"を割り当てる
// リストを初期化する
var list = List.of(1, 2, 3); // listに1,2,3のリストを割り当てる
// マップを初期化する
var map = Map.of("key", "value"); // mapに"key"と"value"のペアを割り当てる

System.out.println(number); // numberを表示する
System.out.println(message); // messageを表示する
System.out.println(list); // listを表示する
System.out.println(map); // mapを表示する

勉強のためを除いてこのコメントの書き方は良くありません。むしろ可読性を損なうのでコードレビュー等で指摘されるでしょう。なぜならば前述したとおりソースコードは「正確な振る舞い」を説明するドキュメントとしては完璧であり、同じ内容を自然言語で記述しただけでは、プログラム設計書と同じく重複した内容を記述しているに過ぎません。そのためドキュメントの二重管理のコストのみを受けることになります。

もう少し別の例を見てみましょう。これはユーザ登録に関する処理です。

public static void main(String[] args){
    // ユーザー情報の初期化
    var userName = "John Doe"; 
    var userAge = 30;
    var userEmail = "john.doe@example.com";
    
    // ユーザー登録に関する処理
    db.save(new User(userName, userAge, userEmail));
    sendEmail("Welcome " + userName + "!", userEmail);
}

このコメントはコードに対して逐次書くのではなく、ある程度意味のまとまりで概要を説明する内容になっています。これはHowを要約したWhatであり、シーケンス図などによるビジュアライズと同じく認知負荷を下げるために効果的なドキュメントです。とはいえ、このケースは別の解決方法もあります。モジュールを分割することです。例えば以下のように記載することが出来ます。

static User initUser(String name, int age, String email){
    return new User(name, age, email);
}

static void addUser(User user){
    db.save(user);
    sendEmail("Welcome " + user.name() + "!", user.email());
}

public static void main(String[] args){
    var user = initUser("John Doe", 30, "john.doe@example.com");
    addUser(user);
}

基本的にWhatのコメント はモジュール化が可能な単位のサインです。このコードは元々十分に小さいので、この例では過剰な分割となり認知負荷を逆に上げるリスクもありますが、長いメソッドの中等でWhatのコメントが頻出する時はモジュール化を検討してください。適切なモジュール名はWhatを伝えるための良いドキュメントになりますし、利用パラメータのスコープを小さくできます。加えて単なる行コメントよりもJavadocのような体系化されたコメントを使う事でより詳細に中身を説明することも出来ます。

/**
 * Creates a user.
 *
 * @param name  user's name
 * @param age   user's age
 * @param email user's email
 * @return      initialized user
 */
static User initUser(String name, int age, String email){
    return new User(name, age, email);
}

/**
 * Adds a user to the database and sends a welcome email.
 *
 * @param user the user to add
 */
static void addUser(User user){
    db.save(user);
    sendEmail("Welcome " + user.name() + "!", user.email());
}

さらにコメントはWhyを書くのが望ましいとされます。これはコードを読むだけでは振る舞いは分かっても、何故そのような振る舞いにさせているのか、が分からないからです。

public class SystemArchitectureExample {
    /**
     * システムのメモリ制限によりキャッシュサイズを小さく設定する必要がある。
     * この制約はメモリ使用量を最適化し、オーバーヘッドを最小限に抑えるため。
     * 以下のキャッシュサイズは現在のハードウェアのスペックに基づいて選択。
     */
    private static final int CACHE_SIZE = 256;
  ...
}

こうしたWhyはWordやWiki/Markdownなどの別なドキュメントにまとめておいた方が良いことも多いですが、コードの近くにある事で見逃しにくいというのは地味に重要なポイントです。特によりコードに近いWhyである設計方針やパラメータの理由、一般的にアンチパターンとされる記述を意図的にしている場合などは理由をコメントで記載しておくと、システム全体の振る舞いを理解するのに役立ちます。

動く仕様書としてのコード

本章は主にコメントに関する話題なので余談となりますが 「コードと仕様書」 という観点では、「動く仕様書としてのコード」 もあるので軽く触れておきます。動く仕様書とは、仕様書をコードとして記述することです。もっともそれを上手く使っているのはTDD(Test-Driven Development)やBDD(Behavior Driven Development)です。例えば以下はRubyのRspecを使ったBDDの例です。

describe Calculator do
  describe "加算" do
    it "二つの数を正しく加算する" do
      calculator = Calculator.new
      expect(calculator.add(5, 2)).to eq(7)
    end
  end

  describe "減算" do
    it "第二の数を第一の数から減算する" do
      calculator = Calculator.new
      expect(calculator.subtract(5, 2)).to eq(3)
    end
  end
end

これを実行すると正常な場合以下のような結果になります。

Calculator
  加算
    二つの数を正しく加算する
      should eq 7

  減算
    第二の数を第一の数から減算する
      should eq 3

Finished in 0.002 seconds (files took 0.048 seconds to load)
2 examples, 0 failures

これは本質的にCalculatorクラスの詳細設計書であり、かつコードであるので検証を自動的にしてくれるものです。TDDにおけるテストコードは品質保証的な意味の単体テストのフェーズで実行すためのもの、というよりは(検証機能が付いた)詳細設計書としての性質を持ちます。その意図を強調するためにTestではなくSpec (=仕様)という呼ぶ形で発展したのがBDDとなります。

BDDをサポートするツールとしては、RSpec以外にもJavaのJUnit5や基本設計書(=結合テスト仕様書)となるCucumber、サーバの設計を記述するServerspecなどがあります。BDDではりませんが、OpenAPIのドキュメントもcurlで実際のロジックを試しに実行できるという点では動く設計書の一種と言えるかもしれませんし、要件定義の世界であればAlloy等の形式仕様記述言語も広義には含める事が出来るかもしれません。

「うちの仕様書はコード」というコメントに対して、ある意味で逆から回答してるのも面白いですよね。「動く仕様書」は実行可能であるがゆえに設計書の曖昧さや揺らぎも無くなるので中長期のメンテナンスをするのに非常に有効なアプローチだと考えています。

まとめ

コメントに何を書くべきか? は話題になりやすい内容です。個人的にはコードもコメントもすべてドキュメントの一種である、と捉える事で何を書くべきかはイメージがしやすくなると思います。具体的な指針やサンプルも多くのネットの記事や書籍に掲載されているので、そちらを読むとより理解が深まり実践でも使いやすくなると思います。

それではHappy Hacking!

脚注
  1. とはいえ、顧客に要求されるから納品物に入れないといけない、とか様々な理由で要求される現場も少なからず残っています。悲しい現実ですね。 ↩︎

Discussion