【C#】抽象クラスを定義して使用する方法

抽象クラスを使う方法について解説していきます。今回のアプリケーションでは簡単な帳票のようなモノを出力するコンソールアプリケーションを作成していきます。デザインパターンの1つとして数えられるビルダーパターンを使って、抽象クラスを解説していきたいと思います。

抽象クラスを定義する

まずはじめに抽象クラスを定義します。今回の抽象クラスでは帳票を出力するような機能としますが、持っているのは2つのメソッドと帳票IDのプロパティだけとしています。新規のクラス「ReportBase.cs」を追加して以下をコーディングしていきましょう。

namespace App1
{
    public abstract class ReportBase
    {
        public int Id { get; }

        public ReportBase(int id)
        {
            Id = id;
        }

        public abstract string GetHeader();

        public abstract string GetContent();
    }
}

このReportBaseクラスではIdプロパティ、コンストラクタ、ヘッダー部分とコンテンツ部分を取り出すメソッドの2つを定義しています。メソッドはどちらもabstractを付けた抽象メソッドとして定義しているので、具体的な実装は記載しておりません。スーパークラスにて記載していきます。

スーパークラスを定義する

次に行うのはReportBaseを継承した具象クラスです。今回は2つの帳票を作ります。新規のクラスを「FirstReport.cs」というファイル名で作成し、以下のコードを記載してください。

namespace App1
{
    public class FirstReport : ReportBase
    {
        public FirstReport(int id) : base(id)
        { }

        public override string GetContent()
        {
            return $"合計金額:{ GetAmount() }円";
        }

        public override string GetHeader()
        {
            return $"帳票A(帳票ID:{ Id })";
        }

        private int GetAmount()
        {
            return 11000;
        }
    }
}

また同様に2つ目の帳票であるSecondReportも作成していきます。新規ファイルを「SecondReport.cs」として作成し、以下のコードを記載してください。

namespace App1
{
    public class SecondReport : ReportBase
    {
        public SecondReport(int id) : base(id)
        { }

        public override string GetContent()
        {
            return $"合計金額:{ TotalAmount() }円";
        }

        public override string GetHeader()
        {
            return $"帳票B(帳票ID:{ Id })";
        }

        private int TotalAmount()
        {
            return 18000;
        }
    }
}

ここで作成した2つのクラスは共に抽象クラスである「ReportBase」を継承する具体的なクラスになります。

それぞれのクラスでは、抽象クラスに定義された2つの抽象メソッドをオーバーライドしているのが分かると思います。先述でも解説済みですが、抽象クラスを継承した場合、抽象メソッドを定義して実装が記載されていないとコンパイルエラーとなります。

GetHeaderメソッドは帳票の名称とIDを返却し、GetContentメソッドは各帳票で算出された合計金額を返却する様にしています。本来は様々な計算をして金額を取り出すのですが、ここでは簡略化して決め打ちで数字を記載しています。

それぞれのクラスで合計金額を求めるメソッドはprivateとして外に出す必要がないので、FirstReportとSecondReportで別メソッドで記載しています。これはpublicに公開しないのであれば、別に統一した名称にする必要がないからです。

帳票の機能としてはヘッダー部分とコンテンツ部分を取り出す口が統一されていれば十分に機能を果たします。ただし、外側から合計金額を取り出す必要があればReportBaseの抽象クラスに合計金額を計算して返却する「統一した定義」を記載すると便利です。

オブジェクトの組み立て部分を定義する

それでは定義した2つのReportクラスから、実際に帳票部分を組み立てる部分を実装していきます。今回はそんなに複雑な構造とはしていないので、大したことはありません。しかし実装に使っているパターンはデザインパターンを使用しているので汎用性は高いと思います。

今回はReportを組み立てるので「ReportBuilder.cs」という名前にしたいと思います。プロジェクトに新規ファイルを追加して以下を記載してください。

using System.Collections.Generic;

namespace App1
{
    public class ReportBuilder
    {
        public ReportBuilder()
        { }

        public List Build(ReportBase report)
        {
            var r = new List();
            r.Add(DecorateHeader(report.GetHeader()));
            r.Add(report.GetContent());
            return r;
        }

        private string DecorateHeader(string header)
        {
            return $"【{ header }】";
        }
    }
}

publicに公開しているメソッドはBuildのみですが、ここでメソッドの引数に注目してください。ReporBaseという抽象クラスを引数の型にしているのが分かると思います。これが意味することは「ReportBaseを継承したクラスを受け付けることができる」という意味です。

もし具体的なクラスごとに「BuildFirstReport」「BuildSecondReport」として型をそれぞれのクラスしたらコード量が多くなりますし、たくさんのレポートが存在している場合に大量の同じようなメソッドが生成されてしまいます。そうしたバラツキを防ぐためにも、抽象クラスを引数の型として使用すると汎用性が高くなります。

抽象クラスを引数にするというのは、抽象クラスに定義された処理のみを使用するという明確な方針でもあります。抽象クラスに定義した処理は、「スーパークラスに強制されている」状態と言えるので、個々の個別実装によらず抽象クラスのみで組み立てられるようにする必要があります。なのでBuildメソッド内はReportBaseに定義されたGetHeaderとGetContentメソッドが使用されています。

ここではヘッダー文字列を各レポートクラスで作成する様にしていますが、帳票名やIDを受け取れるようにしてBuildクラスの内部でヘッダー文字列を組み立てられるようにしてもよいかもしれません。そのあたりはプロジェクトの規模によって、どれくらい強制力を持たせるべきかの工夫次第かなと思います。

プログラミングの実行箇所を定義する

それでは最後にメイン処理を実行するprogram.csを定義していきます。ここでは単純に二つのレポートを生成して画面に表示させるだけの簡単な処理としたいと思います。初期から存在するprogram.csに対して、以下を記載してみましょう。

using System;
using System.Collections.Generic;

namespace App1
{
    class Program
    {
        static void Main(string[] args)
        {
            //ビルダーの生成
            var builder = new ReportBuilder();

            //帳票A
            var firstReport = new FirstReport(1234);
            WriteReport(builder.Build(firstReport));

            //帳票B
            var secondReport = new SecondReport(4567);
            WriteReport(builder.Build(secondReport));

            Console.ReadLine();
        }

        private static void WriteReport(List report)
        {
            foreach(var line in report)
            {
                //一行ごとにコンソールに出力する
                Console.WriteLine(line);
            }
            Console.WriteLine();    //最終行は改行を行う
        }
    }
}

細かい説明は省略することにしますが、各レポートのオブジェクトを生成した後の「builder.Build(…)」を注目してみてください。FirstReportとSecondReportから生成したオブジェクトを、そのままBuildメソッドに渡していますね。これはそれぞれのクラスがReportBaseクラスを継承した具体的なクラスのオブジェクトだからです。

このプログラムを実行すると以下のように出力されると思います。

【帳票A(帳票ID:1234)】
合計金額:11000円

【帳票B(帳票ID:4567)】
合計金額:18000円

上記のように表示されていればプログラムは完成です。抽象クラスとビルダーパターンを活用して、汎用性の高いシンプルな帳票を出力するプログラムが完成しました。これをマスターできれば、もっと難しいプログラムにも応用を効かせることができるはずです。