C#でインターフェースを使うときの基本を解説

必要な処理を閉じ込めたりして公開範囲を決めて「必要なものしか公開しない」というのは、オブジェクト指向の重要な概念のうちの一つです。「多相性」について解説していきます。

多相性とは「インターフェース」という呼び名で通っています。この記事ではC#でインターフェースを使用するときの基本的な事項を網羅していきます。

インターフェースは「強制」させる

「多相性」を一言で解説すると「特定の動作の要求に対して、オブジェクトごとに異なる処理をする」という感じでまとめることができます。特定の処理をする呼び出し方は統一されているが、中身はオブジェクトに応じて異なる処理をするようになっているということです。

C#では主に多相性を表すために「インターフェース」が用いられるのが一般的です。インターフェースを用いることで、そのクラスに特定のプロパティや処理を持たせることを「強制」することができる機能になります。インターフェースの使い方については、後ほどソースコードを交えて解説します。

とにかく「インターフェースを使うことで、クラスに処理やプロパティを持たせることを強制できる」と覚えておきましょう。インターフェースを使用することで、クラスに対して「機能」を持たせることです。

とはいえ、インターフェースには実際の処理を定義することは出来ず、あくまでも「メソッドやプロパティの定義」だけができるようになっています。インターフェースを持っているクラスは、そのインターフェースに定義されたメソッドやプロパティの実処理を記述しなければコンパイルエラーとなります。この「コンパイルエラー」が「処理の強制」を可能としています。

インターフェースには定義のみ記載し、クラスには実処理を記載するというルールが「呼び出し方は同じだが、オブジェクトによって処理が異なる」という多相性を表すことに繋がっています。インターフェースの基礎的な部分については以上になります。

インターフェースの使い方:概要

それではインターフェースの使い方をサンプルコードを交えながら解説していきます。まずはインターフェースのコード全体を見てしまいたいと思います。以下のコードがインターフェースの書き方になります。

public interface ISample
{
    //プロパティ
    string Test { get; set; }

    //メソッド1
    string TestMethod(int test);

    //メソッド2
    void TestMethod2();
}

前提としては先ほど解説した通り、インターフェースにはプロパティやメソッドといったpublicに公開するクラスの要素を定義することができます。private要素を使用するにはC#8.0以上である必要があります。また、フィールドの定義をインターフェースに追加することはできません。

インターフェースを定義する時は大文字の “I” を先頭につけて「インターフェースの定義ですよ」と分かるようにするのが一般的になります。上記の場合はISampleインターフェースと呼び、ファイル名は”ISample.cs”となるのが一般的になります。

using System;

namespace ConsoleApp1
{
    public class Sample : ISample
    {
        public string Test 
        { 
            get => _test; 
            set => _test = value; 
        }
        private string _test;

        public string TestMethod(int test)
        {
            return test.ToString();
        }

        public void TestMethod2()
        {
            Console.WriteLine("TestMethod2");
        }
    }
}

インターフェースで先ほどのISampleインターフェースを搭載したクラスを定義してみました。クラスにインターフェースを持たせるには上記の通り「:」の後に持たせたいインターフェースを記載するだけです。複数持たせる場合には「,」で続けて記載することで可能となります。

インターフェースに定義された「Test」や「TestMethod」、「TestMethod2」といったプロパティや要素が定義され、処理の内部がクラスで記述されていることが分かります。インターフェースを定義することで、「呼出は統一、処理内容は個別に実装」が実現できていることが分かります。メソッドの内部などはクラスにより独自に記載が可能です。

インターフェースの使い方:実践

インターフェースを持たせることで何がよくなるかというと、インターフェースで処理をやり取りすることができるという点にあります。インターフェースの特徴は「機能を定義できる」ということですが、処理の呼出を「クラス」ではなく「機能」で呼び出せることにあります。

特定のインターフェースを持っていることで「クラス」という実体に縛られることなく、「定義」という契約の元で処理を実行することが可能となるということになります。実際に簡単なサンプルを作って解説してきます。まずはApp5というコンソールアプリケーションを作成してください。

インターフェースを定義する

作成したApp5に対してインターフェースを定義していきます。今回は簡単なメッセージをやり取りするためのコンソールアプリケーションを作成します。またクラスという実体に縛られないことを実感するために、デザインパターンとして知られているFactoryパターンで実装したいと思います。

ではインターフェースを定義するために、IMessageというインターフェース作成していきます。プロジェクトにIMessage.csというファイルを作成して以下を記述してみましょう。

namespace App5
{
    public interface IMessage
    {
        string GetMessage();
    }
}

このインターフェースではメッセージを返却する定義だけを記載している、かなりシンプルなインターフェースとなっています。後はクラスを作成していき、そのクラスにインターフェースの定義を追加していきます。

クラスを定義する

インターフェースを持たせるためのクラスを定義していきます。プロジェクトに新しいクラスを追加します。ファイル名はMessages.csとしたいと思います。ファイルを追加できたら、以下のコードを記載してクラスを追加していきましょう。

namespace App5
{
    public class Morning : IMessage
    {
        public string GetMessage()
        {
            return "おはよう。";
        }
    }

    public class Daytime : IMessage
    {
        public string GetMessage()
        {
            return "こんにちは。";
        }
    }

    public class Night : IMessage
    {
        public string GetMessage()
        {
            return "こんばんは。";
        }
    }
}

ここではクラスを3つ定義しています。それぞれに対してIMessageインターフェースを持っており、GetMessageメソッドの戻り値でそれぞれメッセージを固定で埋め込むような形で定義するようにしてみました。ここから「多相性」の意味が分かるかと思います。

呼出は統一されるが処理はオブジェクトによって異なる、という意味が明らかですよね。GetMessageメソッドという呼出に対して、オブジェクト毎に「おはよう。」「こんにちは。」「こんばんは。」と返却するメッセージが異なっています。このイメージが「多相性」となるのです。なので「多相性」を表現するときにインターフェースが使用されます。

Factoryパターンを実装する

では先ほど定義した「Morning」「Daytime」「Night」というクラスを実体化する部分を実装していきます。デザインパターンと呼ばれる実装のレシピ集の中でも、Factoryパターンは実践しやすく、とても汎用性のあるパターンなので覚えておくと便利です。では新規のクラス「MessageFactory.cs」を作成して以下を記述してください。

namespace App5
{
    public static class MessageFactory
    {
        public enum MessageType
        {
            Morning,
            Daytime,
            Night,
        }

        public static IMessage Create(MessageType type)
        {
            if (type == MessageType.Morning)
            {
                return new Morning();
            }
            if (type == MessageType.Daytime)
            {
                return new Daytime();
            }
            if (type == MessageType.Night)
            {
                return new Night();
            }
            return null;
        }
    }
}

MessageFactoryクラスで重要なのはCreateメソッドです。ここに注目するとインターフェースの良さが分かると思います。Createメソッドの戻り値を見てみてください。ここでの戻り値がインターフェースであるIMessageになっています。これが意味することは「IMessageインターフェースを持っているオブジェクトを返却する」というものです

もしインターフェースを持たせずにオブジェクトを返却しようと思ったら、クラスごとにオブジェクトを返却するメソッドを作成して記述する必要があると想像つきます。例えばCreateMorningメソッドなどを定義して、戻り値をMorningクラスとして記述するなど、オブジェクト毎の対応が必要となりソース量が増えてしまいますね。

それを回避するために「特定の機能を持っている」という前提でメソッドを記述することで、一つのメソッドで様々なクラスのオブジェクトを操ることができるようになります。これまでは具体的な側面から処理を記載していたものが、定義しかもたない抽象的な側面で処理を記述できるようになるのです。

メイン処理部分を実装する

さて、最後にこれまで記述してきた部分を使っていく箇所を実装していきます。とは言ってもそんなに難しいことはありませんのでご安心ください。コンソールアプリケーションを作成したときに作られるProgram.csに以下を記載していきましょう。

using System;
using static App5.MessageFactory;

namespace App5
{
    class Program
    {
        static void Main(string[] args)
        {
            IMessage morning = MessageFactory.Create(MessageType.Morning);
            Console.WriteLine(morning.GetMessage());

            IMessage day = MessageFactory.Create(MessageType.Daytime);
            Console.WriteLine(day.GetMessage());

            IMessage nignt = MessageFactory.Create(MessageType.Night);
            Console.WriteLine(nignt.GetMessage());

            Console.ReadLine();
        }
    }
}

処理としては単純にMorning・Daytime・Nightクラスのオブジェクトを順に生成して、それぞれのGetMessageメソッドで取得したメッセージを画面に表示させるだけの簡単な処理となっています。実行結果は以下のようになります。

おはよう。
こんにちは。
こんばんは。

また各オブジェクトを生成している箇所の戻り値の方をIMessageインターフェースとしています。これはあえてインターフェースの型で戻ってくることを明記しています。IMessageのオブジェクトであることが分かれば、その定義に記載されている処理しか使用できないという制限を課すことができます。この例ではGetMessageメソッドしか使用できないという制限です。

こうすることで、例えば各クラスにIMessageインターフェースに定義されている処理以外が記載されていても、それら個別の処理を使用することはできず、「あくまでもIMessageインターフェースの処理だけを使用してくださいね」という実装者のメッセージを表現することが可能となるのです。

インターフェースは「抽象」への入口

インターフェースについての説明は以上となります。かなりボリューム感のある内容となりましたが、基本的なインターフェースのイメージはこの記事で説明することができました。インターフェースの本来の意味は「契約」ですが、この意味が分かっていただけたかと思います。

クラスにインターフェースを持たせることで「特定の機能」を「強制」することができ、そのインターフェースに持たせているプロパティやメソッドが実装されていることを「約束」することができます。またインターフェースでオブジェクトを定義することで「この機能でしかやり取りさせない」という実装者のメッセージも伝えることができます。

インターフェース自体は実際の処理部分を持つことがありませんが、その強制力はクラスに対して大きなものを持っています。そして定義のみでコーディングを考えることを「抽象」と呼んでいます。抽象でプログラミングをすることは難しいのですが、「抽象」で実装を考えられるようになることがオブジェクト指向の最終目的です。インターフェースはそんな「抽象的なプログラミング」の入口でもあります。