C#での抽象クラスの使い方を解説

オブジェクト指向の中でも重要なポジションである「抽象クラス」について、その基本的な使い方から簡単なコンソールアプリケーションを作成していきます。抽象クラスは抽象メソッドを持つクラスで、実体として生成することが特殊なクラスです。

抽象クラスの概要

抽象クラスとは「抽象」の世界でプログラミングを行うために必要な概念になります。抽象クラスは抽象メソッドを一つ以上持っているクラスとして定義されます。C#では「抽象」をabstractキーワードで表現しており、具体的な実装を持っていません。イメージとしてはインターフェースに近い感じです。抽象クラスは以下のような感じで定義されます。

public abstract class Sample
{
    public int Id { get; }
    
    public abstract void DoSample();

    public abstract string GetTitle();
}

上記のようにabstractで定義されたクラスには、abstractで定義されているメソッドが一つ以上必要となります。abstractメソッドには具体的な実装は記載せず、あくまでもメソッドとしての呼出口だけの定義を行います。通常のプロパティ・メソッドも普段通りに記載することが可能です。

抽象メソッドを継承したスーパークラスでは、インターフェースの時と同様に抽象メソッドとして定義された処理の具象部分を定義しなくてはなりません。スーパークラスとして以下のようになります。スーパークラスにおいて具体的な実装をコーディングする場合は、overrideキーワードを使用します。

public class SuperSample : Sample
{
    public override void DoSample()
    {
        //Do Something.
    }

    public override string GetTitle()
    {
        return "タイトルだよ。";
    }
}

「抽象」を言い換えると「具体的な実装を強制させる」というイメージであると言えると思います。「抽象」として定義されると、具体的な実装では「ないと困る処理」になるので、抽象で定義された処理がオーバーライドされてないとコンパイルエラーとなります。

抽象クラスのメリット

抽象クラスのメリットは、クラスの規約や雛形として活用されることが多く、大規模な開発において一定のルールとしてメリットがあります。例えば、何個かのファイル出力処理を数人で分担して作業する場合、指定のプロパティやメソッドを持たせることを強制できます。それぞれが動くようにメソッドやプロパティを定義してしまうと、機能間の統一性がなく保守性が落ちてしまいます。そうしたことを防ぐために必要なクラスの要素を規約として強制させることができます。

抽象クラスは抽象メソッドを持つクラスですが、抽象メソッドは継承されたサブクラスから処理をオーバーライドして具体的な実装をコーディングしていきます。この辺りはインターフェースと近い感じです。なお、インターフェースと同様に、抽象クラスのサブクラスにおいて抽象メソッドがオーバーライドされていないとコンパイルエラーとなります。

インターフェースは「機能」としてのクラスの要素を強制することができますが、抽象クラスでは「クラス」として要素を強制することができます。もちろん多重継承はできないので、サブクラスは必ず抽象クラスの規約を満たす必要があります。インターフェースは複数持つことができますが、抽象クラスでは一つだけなので大きな違いとなります。

抽象クラスの使い方

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

抽象クラスを定義する

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

namespace App6
{
    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 App6
{
    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 App6
{
    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 App6
{
    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 App6
{
    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円

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

抽象クラスは「分類」かつ「強制」

以上、オブジェクト指向においても重要といえる抽象クラスについて解説しました。抽象クラスは様々なクラスを分類しつつ、かつ具体的な処理を強制することができるので、大規模な開発におけるルールやクラスの雛形として非常に有効な手段となります。抽象クラスを使用することでクラスをより設計書のような形式で扱うことが可能になります。

オブジェクト指向プログラミングにおいては、こうした「抽象」の世界でやり取りを考えることも重要になりますので、インターフェースと合わせて使い方をマスターしておくとよいと思います。またプログラマーでも「抽象」を扱える人は少ない印象なので、こうしたスキルを磨いておくと個人の評価も高まっていくと言えるでしょう。

コメント付きのソースコードはGitHubに挙げているので自由に使ってもらえたらと思います。「cs-basic-oop/App6」のページから確認することができるので、活用してみても良いでしょう。