LoginSignup
1396

More than 3 years have passed since last update.

【C#】インターフェイスの利点が理解できない人は「インターフェイスには3つのタイプがある」ことを理解しよう

Last updated at Posted at 2020-09-08

はじめに

C#を始めとするオブジェクト指向言語には「インターフェイス」という機能があります。
これを使うと良い設計になるというのはよく言われていますが、具体的にインターフェイスを使うとどう良いことがあるのか、というのは実感しづらい人も多いと思います。

僕もC#学びたての頃はほんとうにインターフェイスの利点が理解できず苦しみました。しかし、この記事で説明する「インターフェイスには3つのタイプがある」ことを理解して以来、もうインターフェイスが便利すぎて、インターフェイスなしではコーディングできない体質になってしまいました

そこでこの記事では、インターフェイスを使う利点がいまいち理解できていない人が、インターフェイスを使いたくて使いたくて仕方がなくなるようにすることを目的として書きました。

注意点として、僕はC#の開発者でもなければ指導者でもないので、あくまで個人的な意見として参考にしていただけるとうれしいです。
間違っていたり、意見がお有りの方は、ぜひコメントでお知らせください。

インターフェイスについて「よくある間違い」

インターフェイスについて、以下のような間違いがよくされています。

  • インターフェイスは、ポリモーフィズムを実現するためだけに存在する
  • インターフェイスは、複数のクラスに実装しないと意味がない

僕もC#学びたての頃はよくこのような勘違いをしていて、そのせいでまったく理解が進みませんでした。
しかし、これらは間違いです。

インターフェイスは、これによってポリモーフィズムが実現されなくても大きな意味があるし、たった一つのクラスにしか実装されないインターフェイスにも、重要な意味があります

これらの混乱が生じる原因として、一言に「インターフェイス」と言っても3つのタイプが存在するからではないかと考えました。

3つのタイプのインターフェイス

インターフェイスは、以下の3つのタイプに分けられます。

1. 疎結合を目的としたインターフェイス
2. クラスの機能を保証することを目的としたインターフェイス
3. クラスへの安全なアクセスを提供することを目的としたインターフェイス

コードだけを見ていると、どのインターフェイスも同じように使われているように見えますが、実はその目的は全く異なることがあるわけです。

よくあるインターフェイスの説明は分かりづらい

また、インターフェイスの説明は分かりづらいものが多すぎます。
「C# インターフェイス とは」などで調べると出てくるよくある説明に次のようなものがあります。

インターフェイスは、実装するクラスにメソッドの実装を強制するものです

これがよくわからないんですよね。
メソッドの実装を強制されるとどんないいことがあるのかわかりません。

また、「インターフェイス」という名前からも想像がつかない機能というか。
なぜ「メソッドの実装を強制する」という機能に「インターフェイス」という名前が付けられているのかわかりません

これは、上記の「インターフェイスは、実装するクラスにメソッドの実装を強制するものです」という説明が、インターフェイスの本質を説明したものではないから、混乱が生じるんだと思います。

じゃあインターフェイスの本質ってなに

インターフェイスとは「インターフェイス」です。
「ユーザーインターフェイス」とかの「インターフェイス」と同じ意味です。

では何のインターフェイスかというと、クラスのインターフェイスです。
もっと言うと、「クラスにアクセスするためのインターフェイス」といえます。

「クラスにアクセスするためのインターフェイス?別にインターフェイスがなくても普通にインスタンス名.メンバ名でアクセスできるが?」と思うかもしれません。
ところが、インターフェイスがないと大きな問題となる場合がたくさんあります。

QiitaPostクラスで考えてみる

例えば以下のような、Qiitaの記事を表すQiitaPostクラスを考えてみます。


class QiitaPost 
{
    private string m_title;
    private string m_text;

    /// <summary>
    /// タイトルと本文を入力して、記事を新規作成します。
    /// </summary>
    /// <param name="title">記事タイトル</param>
    /// <param name="text">記事本文</param>
    public QiitaPost(string title, string text)
    {
        this.m_title = title;
        this.m_text = text;
    }
    /// <summary>
    /// 記事のURL
    /// </summary>
    public Uri PostURL { get; }
    /// <summary>
    /// 記事タイトル
    /// </summary>
    public string Title => m_title;
    /// <summary>
    /// 記事本文
    /// </summary>
    public string Text => m_text;
    /// <summary>
    /// LGTM数
    /// </summary>
    public int LGTMCount { get; private set; }
    /// <summary>
    /// ストック数
    /// </summary>
    public int StockCount { get; private set; }

    /// <summary>
    /// LGTMする
    /// </summary>
    public void LGTM()
    {
        ++LGTMCount;
    }
    /// <summary>
    /// ストックする
    /// </summary>
    public void Stock()
    {
        ++StockCount;
    }
    /// <summary>
    /// 記事を削除する
    /// </summary>
    public void Delete()
    {
        m_title = string.Empty;
        m_text = string.Empty;
    }
}

Qiitaの記事を書く人

Qiitaはまず記事がなきゃ始まりません。
記事を書く人がこのQiitaPostクラスのインスタンスを生成しました。


//Qiitaの記事を書いた!
QiitaPost post = new QiitaPost("タイトル", "本文");

これをQiitaのサーバーにアップロードします。

//Qiitaのサーバーに記事をアップロードした!
QiitaServer.Upload(post);

これで晴れてみんなに読んでもらえます。

Qiitaの記事を読む人

Qiitaの記事を読む人は、QiitaServerから記事を取得してタイトルと本文を確認します。

//Qiita記事を取得
QiitaPost downloadedPost = QiitaServer.Download("https://~");
//取得した記事のタイトルと本文を読む
string title = downloadedPost.Title;
string text = downloadedPost.Text;
//記事をLGTMする
downloadedPost.LGTM();

記事を読んだあとに良かったと思ったのでLGTMもしちゃいました。

問題点

さて、このQiitaPostクラスの問題点は、次のようなことができてしまう点です。

//Qiita記事を取得
QiitaPost downloadedPost = QiitaServer.Download("https://~");
//取得した記事のタイトルと本文を読む
string title = downloadedPost.Title;
string text = downloadedPost.Text;
//記事をLGTMする
downloadedPost.LGTM();

//勝手に人の書いた記事を消す!!
downloadedPost.Delete();

なんと、読み手が勝手に人の書いた記事を消せてしまいます。
そりゃそうですよね、何しろQiitaPostクラスにはDeleteメソッドがpublicで定義されているのですから…。

また、次のような問題も発生します。

//記事を書く
QiitaPost post = new QiitaPost("タイトル", "本文");
//記事をアップロードする
QiitaServer.Upload(post);
//自分の記事にLGTMする!!
post.LGTM();

自分の記事に自分でLGTMできちゃいます。

なぜこのような問題が起きるのか?

なぜこのような問題が起きるのかというと、ズバリ「QiitaPostクラスにアクセスするための適切なインターフェイスが定義されていないから」に尽きると思います。

確かに、インターフェイスがなくてもQiitaPostクラス自体にはインスタンス名.メンバ名でアクセスできます。
アクセスできますというか、誰でも彼でもフリーでアクセスし放題です。

自販機で言えば、「売上金をすべて排出する」ボタンが客が触れられる場所に配置してあるようなものです。
普通の自販機は客用のインターフェイスと管理者用のインターフェイスが完全に分けられ、客は「売上金をすべて排出する」ボタンを押すことはできません。

それと同じで、クラスにも使用者に応じて適切なインターフェイスが定義されていないと、あとあと問題が発生することがあります。

これが、C#の「インターフェイス」の本質的な意味です。
…と僕は思います。

QiitaPostクラスにインターフェイスを定義してみる

では、実際にQiitaPostクラスのインターフェイスを定義して、先ほどの問題が発生しないようにしてみます。

記事を投稿する人には次の機能が必要でしょうか。


/// <summary>
/// 記事投稿者用のQiitaPostインターフェイス
/// </summary>
interface IAuthorQiitaPost
{
    /// <summary>
    /// LGTM数を取得する
    /// </summary>
    int LGTMCount { get; }
    /// <summary>
    /// ストック数を取得する
    /// </summary>
    int StockCount { get; }
    /// <summary>
    /// 記事を削除する
    /// </summary>
    void Delete();
}

記事を閲覧する人には次のようなインターフェイスを用意しました。


/// <summary>
/// 記事閲覧者用のQiitaPostインターフェイス
/// </summary>
interface IReaderQiitaPost
{
    /// <summary>
    /// 記事タイトルを取得する
    /// </summary>
    string Title { get; }
    /// <summary>
    /// 記事の本文を取得する
    /// </summary>
    string Text { get; }
    /// <summary>
    /// LGTM数を取得する
    /// </summary>
    int LGTMCount { get; }
    /// <summary>
    /// ストック数を取得する
    /// </summary>
    int StockCount { get; }
    /// <summary>
    /// 記事にLGTMする
    /// </summary>
    void LGTM();
    /// <summary>
    /// 記事をストックする
    /// </summary>
    void Stock();
}

おっと、ここで被っているメンバがありますね。
被っているメンバは更に抽象的なIQiitaPostインターフェイスにまとめてしまいましょう。


interface IQiitaPost
{
    /// <summary>
    /// LGTM数を取得する
    /// </summary>
    int LGTMCount { get; }
    /// <summary>
    /// ストック数を取得する
    /// </summary>
    int StockCount{ get; }
}

それに伴って、IAuthorQiitaPostインターフェイスとIReaderQiitaPostインターフェイスも次のように変更します。

/// <summary>
/// 記事投稿者用のQiitaPostインターフェイス
/// </summary>
interface IAuthorQiitaPost : IQiitaPost
{
    /// <summary>
    /// 記事を削除する
    /// </summary>
    void Delete();
}
/// <summary>
/// 記事閲覧者用のQiitaPostインターフェイス
/// </summary>
interface IReaderQiitaPost : IQiitaPost
{
    /// <summary>
    /// 記事タイトルを取得する
    /// </summary>
    string Title { get; }
    /// <summary>
    /// 記事の本文を取得する
    /// </summary>
    string Text { get; }
    /// <summary>
    /// 記事にLGTMする
    /// </summary>
    void LGTM();
    /// <summary>
    /// 記事をストックする
    /// </summary>
    void Stock();
}

とてもすっきりしました。

QiitaPostクラスにインターフェイスを実装する

では早速、作成したインターフェイスをQiitaPostクラスに実装させます。
といっても、すでに内部実装はされているのでクラス定義のところにインターフェイス名を書くだけですね。


class QiitaPost : IAuthorQiitaPost, IReaderQiitaPost
{
    //~略~
}

今回はインターフェイスに定義されたすべてのメンバが、すでにQiitaPostクラスに実装されているのでエラーが出ませんが、一つでも実装されていないメンバがあるとコンパイルエラーになります。
これが冒頭で説明した「インターフェイスは、実装するクラスにメソッドの実装を強制するものです」という説明に通じるわけですね。

QiitaPostクラスにインターフェイスを通じてアクセスさせる

それでは、インターフェイスを作成したので、「Qiitaの記事を作成する人」と「Qiitaの記事を読む人」にはQiitaPostクラスに直接アクセスするのをやめてもらい、きちんとインターフェイス経由でアクセスしてもらいましょう。


//記事を書く
IAuthorQiitaPost post = new QiitaPost("タイトル", "本文");
//記事をアップロードする
QiitaServer.Upload(post);
//自分の記事にLGTMできない〜〜〜
//post.LGTM();

postIAuthorQiitaPost型で定義していますので、自分で自分の記事にLGTMできません。
なぜなら、IAuthorQiitaPostインターフェイスのメンバに、LGTMメソッドが存在しないからです。

これで、記事を書いた人は自分でLGTMするとか余計なことができなくなり、おかしな使い方をされることはなくなりました。

続いて、記事を読む人にもインターフェイス経由で読んでもらいます。

//Qiita記事を取得
IReaderQiitaPost downloadedPost = QiitaServer.Download("https://~");
//取得した記事のタイトルと本文を読む
string title = downloadedPost.Title;
string text = downloadedPost.Text;
//記事をLGTMする
downloadedPost.LGTM();

//勝手に人の書いた記事を消せない〜〜〜
//downloadedPost.Delete();

きちんと読む人専用のIReaderQiitaPostインターフェイス経由で読んでもらうことで、勝手に人の記事を消すなどという酷いことはできなくなりました。

このように、クラスを作るときには「誰に、どのように使ってほしいか」を意識した上で、適切にインターフェイスを用意することがとても重要です
適切なインターフェイスを用意し、使う側がきちんと然るべきインターフェイス経由でクラスにアクセスするようにすることで、想定外の使い方をされて不具合が発生することを防ぐことができます。

クラスを作る前にインターフェイスから作る

もっと言えば、クラスを作成する前にまずインターフェイスから設計することが好ましいと思います。

クラスの使い手と使い方を意識して、まずクラスのインターフェイスを作ります。
それが終わったあとに、クラスに実装させて、エラーが出なくなるまで内部の実装を書くという流れを心がけると、うっかりクラスに直接アクセスされてしまった!なんてことが少なくなると思います。

また、インターフェイスの設計は、クラス内部でどうやって実装しようかなどと考えることもなく、必要な機能をただ列挙していくだけなので、必要な機能を抜かりなく記述することができるというメリットもあります。

たとえば、今回の例だとIAuthorQiitaPostインターフェイスを設計するときに、「あれ、削除するだけじゃなくて編集する機能もいるな」と気づきやすくなると思います。

インターフェイスの「3タイプ」解説

さて、ここからが本題です。
タイトルで、「インターフェイスには目的に応じて3つのタイプがある」と書きました。
もう一度書くと次の3タイプです。

1. 疎結合を目的としたインターフェイス
2. クラスの機能を保証することを目的としたインターフェイス
3. クラスへの安全なアクセスを提供することを目的としたインターフェイス

前項で扱ったインターフェイスは、このうちどれにあたるでしょうか?

もうおわかりですね、「3.クラスへの安全なアクセスを提供することを目的としたインターフェイス」にあたります。

では、他の2つはどんなインターフェイスなのかを解説していきます。

1. 疎結合を目的としたインターフェイス

1つ目は疎結合を目的として作られるインターフェイスです。
例えば次のようなものがあります。


//疎結合を目的として作られたインターフェイス
interface ITextReader
{
    string Read(string path);
} 
class TextReader : ITextReader
{
    public string Read(string path)
    {
        //テキストファイルを読み込んだ結果を返す処理
    }
}

この例のITestReaderインターフェイスは、疎結合を目的として作られたインターフェイスです。

利点1.クラスへの結合を弱くして変更に強くなる

image.png

使い手が、クラスに直接アクセスせずにインターフェイス経由でアクセスさせることで、クラス同士の結合度を下げることが目的です。
このようにしておくことで、たとえTextReaderクラスに変更が生じたとしても、TextReaderクラスがITextReaderインターフェイスを実装している限り、User側の変更は不要になります。

利点2.クラスへの結合が弱いので機能追加も容易になる

例えば、実際にテキストファイルを読み込むのではなく、デバッグ用に用意したダミーデータを読み込ませたいとします。
このとき、ITextReaderインターフェイスがあるおかげで、以下のようにすることができます。

image.png

ITextReaderインターフェイスを実装したDebugTextReaderクラス新たに登場しています。
しかし、ユーザー側はあくまでITextReaderインターフェイスにアクセスしています。

User側はただITextReaderだけを知っていて、その参照先が具体的にどのクラスなのかは知りませんから、これまたUser側の変更は不要になるのです。

このタイプのインターフェイスがないと変更がダイレクトに影響する

以下のように、インターフェイスを用意せずにクラス同士をダイレクトにアクセスさせると、UserクラスがTextReaderクラスの変更の影響をダイレクトに受けるようになります
image.png

例えば、TextReaderクラスのReadメソッドの名前がLoadに変わったとします。

class TextReader
{
    public string Load(string path)
    {
        //テキストファイルを読み込んだ結果を返す処理
    }
}

これだけで、TextReaderクラスを使っているUser側はReadLoadへの変更を余儀なくされます。
一つのクラスなら良いですが、これがたくさんのクラスから依存されていた場合、影響するすべてのコードを変更しなければなりません。

このようなことにならないよう、インターフェイスを用意しておくことで、TextReaderクラスは必ずITextReaderインターフェイスに準拠した実装にならなければなりません。
つまり、ITextReaderクラスを実装してさえいれば、ITextReaderインターフェイス経由でアクセスしている他のクラスへの影響はまったくなくなるのです。

どうでしょう。このタイプのインターフェイスの利点がおわかりいただけたでしょうか。

理想は1クラスにつき少なくとも1インターフェイス

どのようなクラスにも必ずアクセス用のインターフェイスを用意しておくことが理想と思います。
面倒と思わずに、インターフェイスを用意しておくだけで、万が一の変更があったときに大いに役立ってくれるでしょう。

2. クラスの機能を保証することを目的としたインターフェイス

続いて、「クラスの機能を保証することを目的としたインターフェイス」について説明します。

このタイプのインターフェイスは、記事の序盤で「よくあるインターフェイスの説明」として挙げた「インターフェイスは、実装するクラスにメソッドの実装を強制するもの」という説明を受けたとしても最も納得しやすいタイプです。

要は、インターフェイスを実装したクラスは、そのインターフェイスに定義されたメソッドは必ず実装されるのだから、特定の機能があることが保証されますよ、ということですね。

このタイプのインターフェイスとして、有名なものがいくつかありますので列挙します。

インターフェイス 保証する機能
IEnumerable foreachで回すことができる
IEqautable 値の等価性を評価することができる
IDisposable 明示的にメモリを開放することができる
IObservable クラスからの通知を受け取ることができる

例えば、IEnumerable<T>インターフェイスを実装したクラスは、IEnumerator<T>を返すGetEnumerator()メソッドの実装を強制されるので、foreachステートメントで回すことができることが保証されます。
IEquatable<T>インターフェイスを実装したクラスは、bool Equals(T other)メソッドの実装を強制されるので、他のオブジェクトとの等価性を比較できることが保証されます。

このように、クラスに一定の機能があることを保証するために使われるインターフェイスが、このタイプです。

また、クラスの使い手側も、「あ、このクラスはIEnumerableだからforeachで回せるな」「このクラスはIDisposableだから使い終わったらDisposeしなくちゃいけないんだな」などと、クラスの定義を見ただけでそのクラスの性質を簡単に理解できることも利点の一つですね。

もちろんポリモーフィズムによる利点も

もちろん、利点はそれだけではなく、ポリモーフィズムを利用した利点もあります。

例えばIEnumerable<T>インターフェイスは、List<T>, Dictionary<TKey, TValue>, T[]など、様々なクラスが実装しているので、IEnumerable<T>型の変数には、それを実装した様々なクラスを受けることができます。

例えばメソッドの引数として、IEnumerableで受けておけば、使う側はそこに代入できるインスタンスの選択肢が大幅に増えるわけです。
逆に、メソッドの引数をListなどの具象クラスにしてしまうと、使う側はListのインスタンスしか代入できなくなってしまいます。1

このように、ポリモーフィズムによるメリットも享受することができます。

3. クラスへの安全なアクセスを提供することを目的としたインターフェイス

最後に、記事中盤でも解説した「クラスへの安全なアクセスを提供することを目的としたインターフェイス」です。

このインターフェイスの利点はもうお分かりいただけたと思うので、このタイプで有名なインターフェイスを紹介します。

インターフェイス 提供するアクセス
IReadOnlyList Listを読み取り専用で提供する
IReadOnlyCollection IReadOnlyListから更にインデクサによるアクセスを削除
IReadOnlyReactiveProperty 購読と値の読み取りだけができるReactiveProperty

見ての通り、ただ単にアクセスを制限させるものばかりですね。
しかし、これが非常に重要な役割を持ちます。

Listをそのまま渡すのではなく、IReadOnlyListとして渡すだけで、渡した先で勝手に書き換えられる危険性が皆無23になりますから、これを使わない手はありません。
詳しくは下記記事で詳しく解説されているので、参考にしてください。自分も大変お世話になった記事です。
https://qiita.com/lobin-z0x50/items/248db6d0629c7abe47dd

最後に

以上、「インターフェイス」の本質的な意味と、それからインターフェイスの3つのタイプを解説しました。

すごく長くなってしまいましたが、一口に「インターフェイス」と言っても、目的に応じて3つのタイプが有ることをご理解いただけたでしょうか。
インターフェイスの利点があまり良くわかっていない方も、「インターフェイスには3つのタイプがある」ことを意識するだけで、ぐっと理解度が深まると思います。

ただ、冒頭でも書きましたがこの記事の内容は一個人の持論です。
もし説明がおかしい、間違っている等の他、ご意見ご感想などありましたら、ぜひぜひコメントください。すごく喜んで返信します。


  1. もちろん、メソッド内でListの固有メソッドを使う場合は素直にListで受けるべきです。 

  2. [2020/09/10追記]@htsign さんにご指摘いただきました。IReadOnlyListとして渡しても、IListにキャストされてしまえば普通に追加削除可能なので、危険性が「皆無」というわけではないようです。ただ、IReadOnlyListとあからさまに「リードオンリーなリスト」をわざわざ書き換え可能となるようにキャストするというのは、あまり考えられない行為と思いますので、IReadOnlyListとして渡す有用性は十分にあると感じます。もし本当に書き換えられるのを阻止したい場合、ImmutableListもしくはReadOnlyCollectionを使えば実現可能です。 

  3. [2020/09/15追記]自分なりに理解してImmutableList<T>ReadOnlyCollection<T>記事書きました。よろしければどうぞ。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1396