読者です 読者をやめる 読者になる 読者になる

1 つのメソッドは 1 つのことに専念する

メソッドはできるだけ単純にしよう。読んで解読する人のことを考えよう。

ここでは例として、指定の文字列中の指定の単語を "<B>" と "</B>" で囲んで強調表示に設定する場合を考える。この動作を分解すると、

  • 指定の文字列から単語を抽出する。
  • 抽出した単語が指定のものならば強調表示する。

という 2 つの内容に分けられる。これを 1 つのメソッドで実現してみると、

using System.Collections.Generic;

namespace WordReading
{
    public class ReadMethods
    {
        /// <summary>
        /// 指定の文字列中の指定の単語を強調表示に設定する。一つのメソッドで実装する場合。
        /// </summary>
        public static string ReadAndEmphasizeInOneMethod(
            string line, List<string> wordsToEmphasize)
        {
            string result = string.Empty;

            for (int index = 0; index < line.Length; /* index は場合に応じて増やす */)
            {
                // 現在位置の文字を取得し、抽出した単語に設定し、次の文字に移動する。
                char firstChar = line[index];
                string extractedWord = firstChar.ToString();
                ++index;

                if (IsAlpha(firstChar))
                {
                    // アルファベットなら、続くアルファベットを抽出する。
                    for ( ; index < line.Length; ++index)
                    {
                        char nextChar = line[index];
                        if (!IsAlpha(nextChar))
                        {
                            break;
                        }

                        extractedWord += nextChar;
                    }
                }

                // 抽出した単語を強調する場合は "<B>" と "</B>" で囲む。強調しない場合はそのまま。
                if (wordsToEmphasize.Contains(extractedWord))
                {
                    result += "<B>" + extractedWord + "</B>";
                }
                else
                {
                    result += extractedWord;
                }
            }

            return result;
        }

        public static bool IsAlpha(char ch)
        {
            return char.IsUpper(ch) || char.IsLower(ch);
        }
    }
}

ということになる。ループや if 文の構成も複雑だが、文字列の現在位置を保持する変数 index を 2 つの for ループで使った上、
いろいろなところで参照・変更しており、それが解読をより難しくしている。

この動作を、

  1. 指定の文字列から単語を抽出するクラス WordReader
  2. WordReader が抽出した単語が指定のものならば強調表示するメソッド

の 2 つに分けてみる。WordReader クラスは、以下のようになる。

using System;

namespace WordReading
{
    public class WordReader
    {
        private readonly string m_line;
        private int m_index;
        private string m_word;

        public WordReader(string line)
        {
            m_line = line;
            m_index = 0;
            m_word = string.Empty;
        }

        /// <summary>
        /// ReadNextWord() で読み込んだ単語の文字列を取得します。
        /// </summary>
        public string Word
        {
            get { return m_word; }
        }

        /// <summary>
        /// 次の単語を読み込みます。読み込む単語は、アルファベットの並び、あるいは、
        /// アルファベット以外の場合はその 1 文字です。読み込んだ単語は Word プロパティで取得します。
        /// </summary>
        /// <returns>
        /// 単語を読み込んだ場合は true を、
        /// 文字列の最後まで到達しており単語が読み込めなかった場合は false を返します。
        /// </returns>
        public bool ReadNextWord()
        {
            m_word = string.Empty;

            if (EndOfLine)
            {
                return false;
            }
            else
            {
                char firstChar = CurrentChar;
                AddCurrentChar();

                if (ReadMethods.IsAlpha(firstChar))
                {
                    // 最初の文字がアルファベットならば、それに続くアルファベットの並びを読み込む。
                    while (!EndOfLine && ReadMethods.IsAlpha(CurrentChar))
                    {
                        AddCurrentChar();
                    }
                }

                return true;
            }
        }

        private bool EndOfLine
        {
            get { return m_line.Length <= m_index; }
        }

        private char CurrentChar
        {
            get
            {
                if (EndOfLine)
                {
                    throw new InvalidOperationException("EndOfLine のとき CurrentChar は取得できません。");
                }

                return m_line[m_index];
            }
        }

        private void AddCurrentChar()
        {
            if (EndOfLine)
            {
                throw new InvalidOperationException("EndOfLine のとき AddCurrentChar() は呼び出せません。");
            }

            m_word += CurrentChar;
            ++m_index;
        }

        public override string ToString()
        {
            string str = string.Format("Token=\"{0}\"", Word);
            return str;
        }
    }
}

WordReader が抽出した単語が指定のものならば強調表示するメソッド ReadAndEmphasizeByUsingWordReader は、次のようになる。

        /// <summary>
        /// 指定の文字列中の指定の単語を強調表示に設定する。
        /// "単語の抽出" と "指定の単語の強調表示" を分離する場合。
        /// </summary>
        public static string ReadAndEmphasizeByUsingWordReader(string line, List<string> wordsToEmphasize)
        {
            string result = string.Empty;
            WordReader reader = new WordReader(line);

            // 単語が読める間ループ
            while (reader.ReadNextWord())
            {
                if (!wordsToEmphasize.Contains(reader.Word))
                {
                    // 読み込んだ単語が強調するものではない場合、そのまま結果に追加する。
                    result += reader.Word;
                }
                else
                {
                    // 強調する単語の場合は "<B>" と "</B>" で囲んで結果に追加する。。
                    result += "<B>" + reader.Word + "</B>";
                }
            }

            return result;
        }

1 つのメソッドは 1 つのことに専念し、他のメソッドやクラスに任せられることを分離すると、
プログラムは単純になり、より理解しやすくなる。