NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    未来の開発者に誇れるための、可読性を高めるコードの整理方法 ~ドキュメントコメントによる実装の自然言語化~

    本記事は  年度末の振り返りウィーク  1日目の記事です。
    🌸  告知記事  ▶▶ 本記事 ▶▶  2日目  📅

    はじめに

    こんにちは、アプリケーションエンジニア2年目の松澤です。私は業務でネイティブアプリケーションの開発をしており、新規開発に加えリファクタリングの開発タスクにも多く取り組んできました。具体的には、300行以上のメソッドをわかりやすく分割して整理したり、大規模な重複ロジックをまとめて、1500行以上削ったりしてきました。

    今回は「振り返りウィーク」ということで、その中で培ってきた可読性の高いコードを実装するときの考え方について執筆します。開発しているプロダクトのルール等に準拠することを前提として、利用できる考え方を取り入れていただければと思います。

    ※今回例に出すソースコードはC#となっていますが、Javaなどのほかの言語でも同じ考え方を適用できます。

    こんな方におすすめ

    • 莫大な量の複雑なコードを目の前にし、何から手を付けてよいのかわからない方
    • 読みやすいコードの考え方の一例を学びたい方

    私の考える読みやすいコード

    チームで開発を行う以上、将来新しく参画される方が実装を早く理解できるように、可読性の高いコードを実装する必要があります。ただ、既存で実装されているコードの中には、ロジックが入り組んでいたり、複雑かつ冗長なコードが実装されていたりと、可読性の低い状況となっている場合もあります。

    そのような時、リファクタリングを行うことが多いでしょう。リファクタリング自体、可読性・保守性を高めることやデバッグをしやすくすることなど様々な目的がありますが、本質的には「自分も含め、未来の開発者が見ても理解させやすいコードにする」ことがゴールだと私は考えています。

    実装しているコードの処理自体は複雑だとしても、コードを見ればクラス・メソッドの実装を把握できる姿が理想です。

    ドキュメントコメントを整備しよう

    完全にリファクタリングされている場合は、コードを読んだだけで理解ができると思いますが、ぱっと見ではどんなロジックを書いているかはわからない場合があります。そのような場合はドキュメントコメントにビジネスロジックを積極的に記載することを推奨します。

    ドキュメントコメント*1はC#のXMLを用いてユーザー定義の型・メンバーの説明文を記載することができる機能です。JavaではJavadocが同等の機能として利用できます。

     /// <summary>
     /// 2つの整数の和を計算する
     /// </summary>
     /// <param name="x">足す整数</param>
     /// <param name="y">足される整数</param>
     /// <returns>合計値</returns>
     public int Sum(int x, int y)
     {
         return x + y;
     }
    

    上記例に挙げられるタグではこのような要素を説明しています。

    • <summary>:メソッドの説明
    • <param name="x">:引数の説明
    • <returns>:返り値の説明

    これらのタグ以外にも、例外処理を示す<exception>タグ、備考を示す<remarks>タグなど、様々な要素の補足説明を行うことができます。

    ドキュメントコメントを利用すれば、自然言語でロジックを説明することができます。他メソッドから利用する際にもドキュメントを参照できるので、メソッドにおける契約事項と言い換えることもできます。ビジネスロジックをドキュメントコメントに記載できる粒度でメソッドを実装すれば、ドキュメントコメントさえ見れば実装を理解できる状況を作ることもできます。

    ただし、仕様が変わった時にドキュメントコメントの修正コストがかかるというデメリットがあります。実際にはこのようなメリット・デメリットを加味したうえで、チーム内でドキュメントコメント自体を書くか否か、ドキュメントコメントの形式、公開API中心に書くかなどを決めて運用していくことにはなるでしょう。

    開発歴2年未満の私目線では、ドキュメントコメントのおかげで実装が理解できたり、理解が速くなった経験があります。したがって、特に書く必要がない場合以外はメソッドの補足説明として実装しておくと、開発しやすいコードになるのではないかと思います。

    本ブログでは、ドキュメントコメントを整備することを目標として、私がどういった流れでロジックを整理していくかを紹介します。


    1. Privateメソッドに切り分ける

    まず初めに行うことは、冗長なメソッドを切り分けることです。

    メソッドは本来単一責任の原則に則り、一つのメソッドに一つのロジックのみを実装するのがベターです。しかし、「単一責任の原則」と言われても、どの粒度まで分割したらよいのか、なかなか判断が難しいかもしれません。

    そこで私が考える、切り分けの指標となる考え方を3つご紹介します。

    コメントを書きたくなる粒度で切り分ける

    「コメントを書きたくなる=ロジックとしてまとめることができる」という風に言い表すことができます。もう少し突き詰めると、「1メソッドのロジックが複数存在することになり、単一責任の原則から外れている」と言い換えることができます。ロジックについてコメントが書ける粒度なら切り分けましょう。

    // Deprecated(ロジックが多すぎる)
    /// <summary>
    /// ショッピングカートに入っている商品リストを表示する
    /// </summary>
    public void DisplayCartItems()
    {
        // ショッピングカートに入っている商品リストを取得する
        // 商品の合計金額を計算する
        // 商品の情報を表示する
    }
    
    // Good(コメント=ロジックに合わせてメソッドを切り分ける)
    /// <summary>
    /// ショッピングカートに入っている弁当のリストを表示する
    /// </summary>
    public void DisplayCartItems()
    {
        var lunchBoxList = GetCartItems();
        var totalPrice = CalculateTotalAmount(lunchBoxList, 10);
        // 商品の情報を表示する(これはこのメソッドの責務)
    }
    
    /// <summary>
    /// ショッピングカートに入っている弁当リストを取得する
    /// </summary>
    /// <returns>弁当リスト</returns>
    private List<LunchBox> GetCartItems()
    {
        // ショッピングカートに入っている商品リストを取得するロジックを実装する
    }
    
    /// <summary>
    /// 弁当の合計金額を計算する
    /// </summary>
    /// <param name="lunchBoxList">合算対象の弁当のリスト</param>
    /// <param name="taxRate">消費税率</param>
    /// <returns>合計金額</returns>
    private int CalculateTotalAmount(List<LunchBox> lunchBoxList, decimal taxRate)
    {
        // 弁当の合計金額を計算するロジックを実装する
    }
    

    また、コメントを書きたくなる粒度で切り分けると、そのコメントをビジネスロジックをドキュメントコメントに記載することができます。ロジックをドキュメントコメントにするという意識を持ち、privateメソッドに分割すると、自ずと単一責任の原則に則った形になります。

    ローカル変数のスコープを最適化させるように切り分ける

    ローカル変数が本来使われない場所で、変数が利用できる状況にならないようにメソッドを分割しましょう。

    // Deprecated(fugaの生存期間が長い)
    public void Hogehoge()
    {
        int fuga = Sum(1, 2);
        if (fuga == 0)
        {
            // 何らかの処理
        }
    
        // fugaが使われる最後の処理
        // fugaを使わない何らかの処理
    }
    
    // Recommend(
    public void Hogehoge()
    {
        int fuga = Sum(1, 2);
        if (fuga == 0)
        {
            // 何らかの処理
        }
        // fugaが使われる最後の処理
        PiyoPiyo();
    }
    
    private void PiyoPiyo()
    {
        // fugaを使わない何らかの処理。ここではfugaは扱わない
    }
    

    このコードでは、変数fugaが出てきますが、「// fugaを使わない何らかの処理」以降はfugaが利用されない想定です。このままだと、後続の処理で「fugaが使われないのにfugaが使える状況」となってしまいます。

    この例ならば変数が一つ増えたとしても大したことないかもしれませんが、メソッドのコードが100行以上もあり、ローカル変数が沢山あるという場合を想像してみましょう。そうなると、様々なローカル変数の生存期間が長くなり、どの変数が後続の処理にかかわっているかがわかりにくくなります。

    そういったときは、ローカル変数のスコープをなるべく小さくするようにメソッドを分割してみましょう。メソッドごとに必要なローカル変数だけが登場するようになり、それぞれのメソッドの可読性が上がります。

    共通処理を切り分ける

    繰り返し実装している共通処理を切り分けるというのは、リファクタリングではよく提案される手法です。DRY原則に従っており、直感的にわかりやすいリファクタリング手法です。

    ただし、過度に共通処理を切り出すのも考え物です。見た目が同じロジックでも共通化してはいけないケースがあったり、共通化によりメソッド間の往復が増えて可読性が低下する場合があります。

    共通化するか否かは、「ビジネスロジックをドキュメントコメントとして記載できるか」という基準で考えてみましょう。


    2. メソッド内のコードの可読性自体を上げる

    privateメソッドに分割した次は、分割したメソッドの中のコードの書き方自体を変えることで可読性を上げます。可読性を上げる方法自体はいくつかありますが、今回は3つご紹介します。

    early returnでネストを減らす

    ネストの深さ=コードの読み辛さといっても過言ではありません。極力ネストは減らしていきましょう。それを可能にする手法の一つがearly returnです。early returnは名前の通り、処理されない条件分岐で早期にreturnを行う手法です。

    // Deprecated
    private void Validate(bool isHoge, bool isFuga, bool isPiyo)
    {
        if (isHoge)
        {
            if(isFuga)
            {
                if(isPiyo)
                {
                    Console.WriteLine("すべての条件が true です");
                }
            }
        }
    }
    
    // Recommend
    private void Validate(bool isHoge, bool isFuga, bool isPiyo)
    {
        if (!isHoge) return;
        if (!isFuga) return;
        if (!isPiyo) return;
        Console.WriteLine("すべての条件が true です");
    }
    

    こちらの例を見ると、if文の条件を反転させるだけで、ネストも減っていることが確認できます。

    early returnのメリットは、デバッグ時にも発揮されます。もしif文の中に数十行などの長い処理が実装されていた場合、early returnをしなければ上から下までメソッドを追いかける必要がありますが、early returnを行う場合は一行を追いかけるだけで事足ります。

    early returnは、後続の処理がある場合は実装できないため、early returnができるようにメソッドを分割するといったように、メソッドの分割の基準としても考えることができます。

    変数名・メソッド名

    変数名は、省略しすぎず、簡潔に記載しましょう。メンバ変数の場合、ドキュメントコメントを追記できると尚良いでしょう。

    // Deprecated
    List<string> bre = [];
    
    // Recommend
    /// <summary>
    /// 購入された弁当のリストを保持するプロパティ
    /// </summary>
    List<string> PurchasedLunchBoxList = [];
    

    ローカル変数の場合はそのメソッドでの役割がはっきりわかる場合(つまりそのスコープでの役割が把握できる場合)は、詳細な変数名にしなくとも問題ありません。

    private int HogeHoge()
    {
        // resultが示すものがはっきり説明できる場合は問題ない
        var result = Sum(1, 2);
    
        // resultを使用して何らかの処理を行う
    }
    

    モダンな構文の利用

    モダンな構文を利用することで、可読性が上がることがあります。

    例えば、C# 12ではコレクション式という新たな構文が利用できるようになりました。これにより、new演算子を使った少し冗長な宣言から[] を利用した簡潔な宣言にすることができます。

    // old
    private readonly List<string> DummyList = new List<string>() { "Hoge", "Fuga", "Piyo" };
    
    // new
    private readonly List<string> DummyList = ["Hoge", "Fuga", "Piyo"];
    

    また、この章で記載している内容は、細かなコード修正となるので生成AIとも相性が良いと考えています。私はよくGitHub Copilotに「最適化の余地はありますか」といった風にプロンプトを投げて、知りえなかった簡略化などを学びながら修正を行っています。


    3. あるべきクラスに引っ越しをする

    現段階だと、1つのクラスに複数のprivateメソッドを実装したことになります。ここでprivateメソッドや依存するメンバ変数を、存在するべき場所へ引っ越しさせます。

    関心事を分離する

    クラスを分割するときは、関心事を分離しましょう。関心事というのは、「そのクラスに関わる登場人物」と言い表すことができます。登場人物が少なければ少ないほど、クラスがシンプルになります。この関心事をいかに少なくするかが可読性の高いコードを実装する上で重要になってきます。

    関心事を分離させる一例をご紹介します。

    /// <summary>
    /// 弁当にまつわるビジネスロジックを担当するクラス
    /// </summary>
    public class LunchBoxService
    {
        private List<LunchBox> CalculateTotalCalorie()
        {
            var lunchBoxList = GetAllLunchBox();
            // 各弁当のカロリーを計算する処理
        }
    
        private List<LunchBox> GetAllLunchBox()
        {
            // データベースから弁当の情報を取得する処理
        }
    }
    

    弁当のカロリーを計算するCalculateTotalCalorieメソッドから、データベースから弁当の情報を取得するGetAllLunchBoxメソッドを事前に分離しました。GetAllLunchBox メソッドが存在するべきクラスは、弁当ドメインのデータベースアクセスを司るLunchBoxRepository となるので移動してみましょう。

    /// <summary>
    /// 弁当にまつわるビジネスロジックを担当するクラス
    /// </summary>
    public class LunchBoxService
    {
        private readonly ILunchBoxRepository _lunchBoxRepository; // コンストラクタインジェクション
    
        public LunchBoxService(ILunchBoxRepository lunchBoxRepository)  
            => _lunchBoxRepository = lunchBoxRepository;
    
        private List<LunchBox> CalculateTotalCalorie()
        {
            var lunchBoxList = _lunchBoxRepository.GetAllLunchBox();
            // 各弁当のカロリーを計算する処理
        }
    }
    
    /// <summary>
    /// 弁当ドメインにまつわるデータアクセスを担当するクラス
    /// </summary>
    public class LunchBoxRepository
    {
        public List<LunchBox> GetAllLunchBox()
        {
            // データベースから弁当の情報を取得する処理
        }
    }
    

    このように、DBに対するCRUD操作という関心事は、リポジトリパターンにおけるリポジトリにまとめることができます。関心の分離方法に迷ったら、デザインパターンを適用してみるのも一つの手です。

    クラス間の参照関係を簡素にする

    参照先のクラスに対しても、関心事の対象といえるので、極力少なくし、依存関係を少なくします。つまり、C#で言うところのusing、Javaで言うところのimportを少なくします。

    この参照関係をなくせるように、メソッドをまとめて切り離しましょう。

    また、クラス間の関係は常に一方通行でなければならず、相互参照してはいけないことにも注意しましょう。

    アクセス修飾子を見直す

    本来ならば、クラス外からアクセスする必要がないメソッドがpublicメソッドになっていたり、逆にクラスをまたいだことでアクセスレベルを上げなければならなくなる場合があります。

    基本的にアクセスレベルは低く設定して、コンパイルエラーが生じた場合に、それに応じてアクセスレベルを上げるなどして調節しましょう。


    4. ドキュメントコメントを整備する

    メンバ変数・メソッドをあるべきクラスに引っ越させた後、コード自体の修正が不要になったら、ドキュメントコメントを整備しましょう。

    書き方のお手本は標準ライブラリ

    また、ドキュメントコメントの書き方は、標準ライブラリが特に参考になります。例えばLINQで用いられるWhereメソッドを参照すると、Whereメソッドの処理詳細、返り値、投げうる例外が記載されています。

    知らない情報は書かない

    ドキュメントコメントのロジックはあくまでメソッドの実装しか知りえません。

    実装を行う上で、ロジック全体を見渡すと、開発者はそのメソッド外の処理も理解しているので、注意しなければ、下記の例のようにドキュメントコメントに対しても、その処理を知った風に書いてしまいます。

    internal class ShoppingCart
    {
        private readonly LunchBoxService lunchBoxService = new LunchBoxService();
        private void DisplayTotalPrice(List<LunchBox> lunchBoxList, decimal taxRate)
        {
            int totalPrice = lunchBoxService.CalculateLunchBoxPrice(lunchBoxList, taxRate);
            // 合計価格を表示する処理
        }
    }
    
    public class LunchBoxService
    {
        // Deprecated(ShoppingCartの存在を知っている)
        /// <summary>
        /// ショッピングカートに入っている弁当の合計金額を計算する
        /// </summary>
        /// <param name="lunchBox">合算対象の弁当のリスト</param>
        /// <param name="taxRate">消費税率</param>
        /// <returns>弁当の合計価格</returns>
        public int CalculateLunchBoxPrice(List<LunchBox> lunchBox, decimal taxRate)
        {
            return // 弁当の合計価格を計算
        }
    
        // Recommend(他クラスに依存しない)
        
        /// <summary>
        /// 指定した税率を用いて弁当の合計金額を計算する。
        /// </summary>
        /// <param name="lunchBox">価格を持つ弁当の一覧。null要素は不可。</param>
        /// <param name="taxRate">0.0~1.0 の範囲の税率(例:10% は 0.1)。</param>
        /// <returns>税込の合計金額(小数第2位で四捨五入)。</returns>
        /// <exception cref="ArgumentOutOfRangeException">taxRate が範囲外。</exception>
        /// <exception cref="ArgumentNullException">lunchBox が null。</exception>
        public int CalculateLunchBoxPrice(List<LunchBox> lunchBox, decimal taxRate)
        {
            return // 弁当の合計価格を計算
        }
    }
    

    CalculateLunchBoxPrice は、この時ShoppingCartクラスに利用されることを前提にドキュメントコメントを記載してはいけません。あくまで、メソッドの処理内容しか知らないのでそもそも書けませんし、もしShoppingCartクラスの実装を知っていたら相互参照になってしまいます。

    上記で説明したLINQのWhereメソッドを考えてもらうとわかりやすいかと思います。LINQはShoppingCartクラスの実装を把握していない前提で実装されているはずです。

    ドキュメントコメントを書くときは、あくまでどう使われているかは知らない体で、ロジックを記載しましょう。


    最後に

    既存のコードを修正するときの注意事項

    修正を行うときは、デグレが生じないようにしましょう。複雑なロジックでは、その都度簡易的なユニットテストを作成したり、シリアライザーでJSONなどに出力し、ファイル比較ツールを利用して差分を確認したりなど、都度都度確認を行いましょう。

    まとめ

    私は開発タスクにてリファクタリングをするときに、責務・関心という言葉を意識するようになりました。このメソッドの責務は何か?関心事を分離することはできないか?実装しているコードと向き合って、突き詰めていったときに、今回執筆したような内容の考え方が身に付きました。

    何回読んでも理解できないコードにも向き合ったことがあります。そんな時に、ビジネスロジックをドキュメントコメントに記載していたら、読みやすかっただろうなと思います。将来、自分のコードを触る開発者に誇れるように、わかりやすいコードを実装していきましょう。

    執筆者 松澤武志

    2025 Japan AWS Jr. Champion
    趣味でAWSをいじるネイティブアプリエンジニア(.NET/C#/Java/Angular)

    ・執筆記事一覧: https://tech.nri-net.com/archive/author/t-matsuzawa
    ・X:https://x.com/2357_takeshi
    ・Speaker Deck:https://speakerdeck.com/matsuzawatakeshi