C#インターフェイスの使いどころ

C# コンピュータ
C#

インターフェイスはメソッド及びプロパティを定義し、クラスで実装して使う機能です。
インターフェイスは自体でインスタンスを生成することは出来ませんが、インターフェイスを実装したクラスのオブジェクトを受け入れる変数を定義することが出来ます。

機能的には以上ですが、インターフェイスを使うことでオブジェクトに依存しない振る舞いをプログラミングすることが出来る点がインターフェイスの特徴です。

インターフェスで定義したメソッドを呼び出す

基本的に静的型のプログラミング言語の変数はあらかじめ決められた型のオブジェクトしか受け入れることは出来ません。

以下のサンプルコードの場合、Sample1クラスのインスタンスと、Sample2のインスタンスは、型が異なるので、同じ変数に代入することは基本的には出来ません

ところが、インターフェイスISample型の変数とすることで、Sample1とSample2のいずれのインスタンスも受け入れることが出来るようになっています。

interface ISample
{
    void Method();
}

class Sample1 : ISample
{
    public void Method()
    {
        Console.WriteLine("Sample1");
    }
}

class Sample2 : ISample
{
    public void Method()
    {
        Console.WriteLine("Sample2");
    }
}

public class Program
{
    public static void Main()
    {
        ISample obj = new Sample1();
        obj.Method();
        // Sample1;
        obj = new Sample2();
        obj.Method();
        // Sample2;
    }
}

ただ、こちらのサンプルのようなコードが必要になる場面は少ないと考えます。

委譲とコンストラクタインジェクション

クラスの継承は「Is a」の関係のオブジェクトを表現する機能です。それに対し委譲は「Has a」という関係を表現し、具体的にはクラスのメンバー変数として所有させたいオブジェクトを定義することになります。この委譲という考え方とインターフェイス型の変数を組み合わせると、以下のようなコードになります。

/*
interface ISample
class Sample1
class Sample2
同じなので省略
*/
class DelegationClass
{
    ISample _sample;
    public DelegationClass(ISample sample)
    {
        _sample = sample;
    }
    public void Method()
    {
        _sample.Method();
    }
}

public class Program
{
    public static void Main()
    {
        Sample1 sample1 = new Sample1();
        Sample2 sample2 = new Sample2();

        var obj1 = new DelegationClass(sample1);
        obj1.Method();
        // Sample1

        var obj2 = new DelegationClass(sample2);
        obj2.Method();
        // Sample2
    }
}

DelegationClassにSample1又はSample2のオブジェクトを所有したい。その場合ISampleというインターフェイス型にすることで両方の型を受け入れることが出来ます。また、インターフェイスで定義したMethod()はSample1とSample2のいずれも実装されいていますので(実装の強制)、オブジェクトの型がSample1になるかSample2になるか決まっていない状態でも、DelegationClass内でMethod()を呼び出すコードを記述することが出来ます(オブジェクトに依存しない振る舞い)。

また、委譲により所有されるオブジェクトを、DelegationClassのインスタンス生成と同時に登録することを強制するために、DelegationClassのコンストラクタの引数とすることをコンストラクタインジェクションというそうです。

こちらのサンプルコードは、所有されるオブジェクトをデータベースへのアクセスするクラスなどにすると、接続するデータベース事に異なるインスタンスを切り替えたり、SQLiteやPostgreSQLなど異なるRDBMSを同じインターフェイスでコードを記述出来たりと、中々使いどころが多いコードだと思います。

こちらのコードを継承で実装することも出来ますが、Sample1とSample2が共通の祖となるクラスを定義する必要があり、その関係が(たまたま)「Is a」である必要があるので、適用できる状況が意外と限定的だったりします。継承はオブジェクト指向プログラミングを代表する機能らしく、強力かつ便利な機能ではありますが、使いどころの汎用性では委譲とインターフェイスの組み合わせの方が優れていると思います。

委譲するオブジェクトを切り替える

コンストラクタインジェクションの場合、委譲するオブジェクトが事前に生成されるている必要がありますが、任意のタイミングで切り替えたい場合は以下のように対応します。

/*
interface ISample
class Sample1
class Sample2
同じなので省略
*/
class DelegationClass2
{
    List<ISample> _samples = [];
    public DelegationClass2()
    {
    }
    public ISample Sample
    {
        set
        {
            if (0 < _samples.Count)
            {
                if (_samples[0] != value)
                {
                    _samples[0] = value;
                }
            }
            else
            {
                _samples.Add(value);
            }
        }
    }
    public void Method()
    {
        _samples.ForEach(o =>
        {
            o.Method();
        });
    }    
}

public class Program
{
    public static void Main()
    {
        Sample1 sample1 = new Sample1();
        Sample2 sample2 = new Sample2();

        var obj = new DelegationClass2();

        obj.Method();
        // なにも表示されないが、エラーにもならない。

        obj.Sample = sample1;
        obj.Method();
        // Sample1

        obj.Sample = sample2;
        obj.Method();
        // Sample2
    }
}

Sample1とSample2が切り替えられることが確認できます。また、Sampleプロパティにオブジェクトをセットしていない状態でMethod()を呼び出してもエラーが発生しないような仕組みにしてあります。

感想

自分なりのインターフェイスの使いどころをまとめて見ました。

コード量が多くなるので使うことを避けていた機能ですが、作成するプロジェクトが少し大きくなると、役割や機能ごとにクラスに分割する必要が出てきます。そうなるとクラスごとの単体テストが重要になってきて、単体テストをする場合クラス同士が依存度が高いとテストしずらいので、インターフェイスと委譲の出番となるわけです。

継承は関係性の条件が厳しく、場当たり的なプログラミングを好む筆者としては、条件が緩い委譲の方が好みで、インターフェイスを絡めることで、継承を概ね置き換えることが出来ると思います。

コメント