C++/C#/Java オーバーロードを使うべき3つのポイント

overload_2-1

はじめに

今回は、オーバーロードを使ったワンランク上の設計・実装方法を説明します。
使いどころとして3つのポイントがあり、
・コードの視認性向上
・クラスの再利用性
・既存コードへの影響低減
を意識する時に使います。
今回もC#で書きますが、C++, Javaなどオブジェクト指向言語でも同じです。

やさしさのためのオーバーロード

オーバーロードはライブラリや共通クラス、ユーティリティーを作る人が気を利かせて実装するものだと考えています。

例えば.NETのクラスライブラリにあるSystem.IO.StreamReaderはファイルを読み込むときに使います。
こいつのコンストラクタは11個用意されていて、
ファイルパスとStreamの両方からインスタンスを生成することができ、
エンコード、BOMの有無、バッファーサイズ、ストリームを開いたままにするかの指定はオプションとなっています。
詳しくはこの辺で
https://msdn.microsoft.com/ja-jp/library/system.io.streamreader(v=vs.110).aspx?cs-save-lang=1&cs-lang=csharp#code-snippet-1

つまり.NETのクラスライブラリの様に不特定多数の人が利用するようなクラスは、
オーバーロードを定義して使う人への配慮を行います。
小規模でも複数の人がかかわるプロジェクトでは共通クラスを作る際も、
オーバーロードを定義すると、「これを作ったやつはやさしいなー」と思われると思います。

やさしくないUtilityの例

データーベース(配列)から成績を取ってきて、
ファイルに書き込むプログラムの例を書いてみました。
これは優しくない方です。

class Program
{
    static void Main()
    {
        int[] Scores = { 80, 70, 90, 60, 100 };

        foreach (int one in Scores)
        {
            FileUtility.Write(one.ToString());
        }
    }
}
static class FileUtility
{
    static public void Write(string score)
    {
        using (System.IO.StreamWriter sw = new System.IO.StreamWriter(@"E:\temp\2015_3q_test.txt", true))
        {
            sw.WriteLine(score);
        }
    }
}

わざわざ、ループさせて、ToString()で文字の変換までさせていますね…

やさしいUtilityの例

やさしくないUtilityをやさしく書き換えてみました。

class Program
{
    static void Main()
    {
        int[] Scores = { 80, 70, 90, 60, 100 };
        FileUtility.Write(Scores);
    }
}
static class FileUtility
{
    static public void Write(int score)
    {
        FileUtility.Write(new int[] { score });
    }
    static public void Write(int[] scores) {
        string[] temp = new string[scores.Count()];
        for(int count = 0; count < scores.Count(); count++)
        {
            temp[count] = scores[count].ToString();
        }
        FileUtility.Write(temp);
    }
    static public void Write(string[] scores) 
    {
        foreach (string one in scores)
        {
            FileUtility.Write(one);
        }
    }
    static public void Write(string score)
    {
        using (System.IO.StreamWriter sw = new System.IO.StreamWriter(@"E:\temp\2015_3q_test.txt", true))
        {
            sw.WriteLine(score);
        }
    }
}

なんと利用者のコードは1行…しかもシンプル…
だけど、FileUtilityの中は泥臭いですね。
しかし、変換処理をFileUtilityで吸収し利用者への配慮が感じられます。

ポイント

  1. オーバーロードで必要な機能(今回はファイルの出力)は極力一か所で行う。
    ->void Write(string score)
  2. 1以外のオーバーロードはデータの変換に徹する。
  3. 2は最終的に1を利用する。
    int -> int[] -> string[] -> string -> ファイルへ

こうすることで、複雑なファイル処理をあちこちにばらまくこともなくなります。
単体試験も普通に一個ずつオーバーロードを書いて行くよりかは楽になるはずです。

まとめ

上のコードは

  • コードの視認性向上
  • クラスの再利用性

が上がっています。
利用者側のコードがすっきりしたし、
FileUtilityクラスはテスト結果以外も出力できそうですから、
クラスの再利用性も確認できました。

力強いオーバーロード

オーバーロードの使いどころはもう一個あって、
すでにある機能と似たような処理が必要になった時、
既存コードを利用して影響を極力与えず実装することができます。
これはたぶん小技みたいなものだと思いますが、強力です。

例えば

データベース(配列)から取得した文字列を整数に変換し、
0から100の範囲だったとき整数を表示、範囲外なら表示しない。
と言うようなことを行うプログラムがあったとします。

class Program
{
    static void Main()
    {
        string[] Scores = { "80", "70", "90", "-1", "100" };
        foreach (string one in Scores)
        {
            Display.Show(one);
        }
    }
}
static class Display
{
    static public bool Show(string str)
    {
        int score;
        if (!int.TryParse(str, out score))
        {
            return false;
        }

        if (!(0 <= score && score <= 100))
        {
            return false;
        }

        System.Console.WriteLine(score);
        return true;
    }
}

Show(string str)の様に既存のコードに、”すでにある機能と似たような処理”として、
データが0から100であるかを事前にチェックしたい。
範囲外の値があったら、エラーを出力してデータの出力はしなくてよい。
と言う追加の要望があった場合、

static public bool Check(string str)
{
    int score;
    if (!int.TryParse(str, out score))
    {
        return false;
    }

    if (!(0 <= score && score <= 100))
    {
        return false;
    }

    // System.Console.WriteLine(score); <- ここだけ消しちゃうみたいな
    return true;
}

と言う感じでコピペしてメソッドを追加しますか?
オーバーロードを使うとスマートに実装できます。

class Program
{
    static void Main()
    {
        string[] Scores = { "80", "70", "90", "-1", "100" };
        bool IsOK = true;
        foreach (string one in Scores)
        {
            IsOK &= Display.Check(one);
        }

        if (!IsOK)
        {
            System.Console.WriteLine("データが不正です。");
            return;
        }

        foreach (string one in Scores)
        {
            Display.Show(one);
        }
    }
}
static class Display
{
    static public bool Check(string str)
    {
        return Show(str, true);
    }
    static public bool Show(string str)
    {
        return Show(str, false);
    }

    static public bool Show(string str, bool CheckOnly)
    {
        int score;
        if (!int.TryParse(str, out score))
        {
            return false;
        }

        if (!(0 <= score && score <= 100))
        {
            return false;
        }

        if (!CheckOnly)
        {
            System.Console.WriteLine(score);
        }
        return true;
    }
}

overload_2-2

ポイント

  1. 既存のメソッドに引数を追加する。チェックだけならtrue, 表示もするならfalse。
  2. 既存のメソッドと同じ引数のオーバーロードを作成する
  3. Check用のメソッドを作成する
  4. 2,3は1を呼び、true, falseを変更する

既存のShowメソッドはほとんど変更せずに済みました。
単体試験も簡単に通せるでしょう。
また、仕様変更でint から doubleにして!
と言われても1メソッド修正すれば済みます。

まとめ

C++とC#にはデフォルト引数が使える場合があるので、
デフォルト引数で代用すれば、わざわざShow(string str)とCheck(string str)を作らなくてもいいです。
しかし、メソッド名で機能を推測できるようにするためには、Checkを用意した方がいいと思います。

オーバーロードの力強さを感じていただけましたか?

overload_2-3

総まとめ

今回は、新人の子に「オーバーロードはなんのためにあるか考えてみて」と課題を出しましたが、
色々探したけど、使いどころみたいのってあまり説明されてないんですよね…
なのでちゃんと自分も答えを示せるようにブログにまとめました。

オーバーロードは使い方を間違えると意味の分からないコードを生み出しますが、
使い方によっては、ワンランク上の設計・実装ができるようになります。
いいポイントを見つけて実装してみましょう!!

Follow me!

C++/C#/Java オーバーロードを使うべき3つのポイント” に対して1件のコメントがあります。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です