##はじめに
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();
post
をIAuthorQiitaPost
型で定義していますので、自分で自分の記事に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.クラスへの結合を弱くして変更に強くなる
使い手が、クラスに直接アクセスせずにインターフェイス経由でアクセスさせることで、クラス同士の結合度を下げることが目的です。
このようにしておくことで、たとえTextReader
クラスに変更が生じたとしても、TextReader
クラスがITextReader
インターフェイスを実装している限り、User
側の変更は不要になります。
####利点2.クラスへの結合が弱いので機能追加も容易になる
例えば、実際にテキストファイルを読み込むのではなく、デバッグ用に用意したダミーデータを読み込ませたいとします。
このとき、ITextReader
インターフェイスがあるおかげで、以下のようにすることができます。
ITextReader
インターフェイスを実装したDebugTextReader
クラス新たに登場しています。
しかし、ユーザー側はあくまでITextReader
インターフェイスにアクセスしています。
User側はただITextReader
だけを知っていて、その参照先が具体的にどのクラスなのかは知りませんから、これまたUser
側の変更は不要になるのです。
####このタイプのインターフェイスがないと変更がダイレクトに影響する
以下のように、インターフェイスを用意せずにクラス同士をダイレクトにアクセスさせると、User
クラスがTextReader
クラスの変更の影響をダイレクトに受けるようになります。
例えば、TextReader
クラスのRead
メソッドの名前がLoad
に変わったとします。
class TextReader
{
public string Load(string path)
{
//テキストファイルを読み込んだ結果を返す処理
}
}
これだけで、TextReader
クラスを使っているUser
側はRead
→Load
への変更を余儀なくされます。
一つのクラスなら良いですが、これがたくさんのクラスから依存されていた場合、影響するすべてのコードを変更しなければなりません。
このようなことにならないよう、インターフェイスを用意しておくことで、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つのタイプがある」ことを意識するだけで、ぐっと理解度が深まると思います。
ただ、冒頭でも書きましたがこの記事の内容は一個人の持論です。
もし説明がおかしい、間違っている等の他、ご意見ご感想などありましたら、ぜひぜひコメントください。すごく喜んで返信します。
-
もちろん、メソッド内で
List
の固有メソッドを使う場合は素直にList
で受けるべきです。 ↩ -
[2020/09/10追記]@htsign さんにご指摘いただきました。
IReadOnlyList
として渡しても、IList
にキャストされてしまえば普通に追加削除可能なので、危険性が「皆無」というわけではないようです。ただ、IReadOnlyList
とあからさまに「リードオンリーなリスト」をわざわざ書き換え可能となるようにキャストするというのは、あまり考えられない行為と思いますので、IReadOnlyList
として渡す有用性は十分にあると感じます。もし本当に書き換えられるのを阻止したい場合、ImmutableList
もしくはReadOnlyCollection
を使えば実現可能です。 ↩ -
[2020/09/15追記]自分なりに理解して
ImmutableList<T>
とReadOnlyCollection<T>
の記事書きました。よろしければどうぞ。 ↩