C#プログラミング言語は、オブジェクト指向プログラミングの強力な概念を提供しています。その中でも、インターフェース(Interface)は、クラスや構造体に特定のメンバーを実装することを強制するための重要なツールです。この記事ではC#のインターフェースについて重要性と使い方について説明します。
インターフェース(Interface)とは?
C#は強力なオブジェクト指向プログラミング言語ですが、使い方をしっかり理解しておく必要があります。その中でも重要なのが「インターフェース」と呼ばれるものです。まずはインターフェースとは何か?を理解しておく必要があります。
C#のインターフェースは、クラスや構造体などの型が提供しなければならないメンバーの一連の契約を指しています。
少し難しい言い方になっていますが、要するに「クラスがどのようなメソッド、プロパティ、イベント、およびインデクサーを提供しなければならないかを定義する」ということです。
インターフェースでは具体的な処理は記述せず、あくまでも「定義のみ」を記述するものになります。
public interface ILogger
{
void LogMessage(string message);
void LogError(string error);
}
上記のサンプルを見てみてください。これは ILogger というインターフェースを定義しています。ILogger インターフェースは見てわかる通り、 LogMessage メソッドと LogError メソッドを定義していますね。実際にインターフェースを使ってクラスを実装(実現)する際は、インターフェースに記述された定義の実装がされている必要があります。
インターフェース(Interface)の命名規則
インターフェースの名前は通常、I で始まることが多いです。これはインターフェースであることを明示するための慣習的な命名規則です。例えば、IDrawable、IComparable、ILoggerなどが一般的なインターフェースの名前です。
C#のソースコードを読んでいると、例えば IList や IEnumerableなどに行き着くことがあります。これはインターフェースの頭文字である I と機能の特性である List や Enumerable などを組み合わせた定義になっていることがよくわかります。インターフェースを定義するときは I〇〇 で定義する、と覚えておきましょう。
インターフェース(Interface)の重要性
C#(本質的にはC#だけに限らず、オブジェクト指向プログラミング)においてインターフェースは、とても重要な役割を果たしています。
1.契約の定義
インターフェースは、クラス間の契約を定義する、とは前述したとおりです。クラスが特定のインターフェースを実装すると、そのクラスはその契約を守る必要があります。先ほどの例でいえば ILogger インターフェースを持ったクラスは、LogMessage メソッドと LogError メソッドが実装されているクラスである必要があります。
この構造はコードの「信頼性」と「予測可能性」を向上させるのです。
他のプログラマーがあなたのコードを使用する際、インターフェースによって提供されるメソッドやプロパティが期待通りに機能することを保証できます。つまり、初めてクラスの実装を見たときに、ILogger インターフェースを持ってるということは、「LogMessage メソッドと LogError メソッドが実装されていて、引数と戻り値がインターフェース通りに存在している」ことが分かるからです。
ソースコードを受け継いだ側からみると、「インターフェースに定義されているメソッド等は、クラスにおいて必ず実装されている」ということが確約されており、インターフェースを見ることでクラスの処理が抽象的に理解できるということに他なりません。
2.多重継承の代替
C#は多重継承(一つのクラスで複数のクラスを継承すること)をサポートしていませんが、インターフェースはそれに近い機能をサポートしています。つまり、1つのクラスに対して、複数のインターフェースを同時に実装できるということです。
この機能により、クラスは異なるコンポーネントや機能を組み合わせて柔軟な設計を実現できます。
異なるインターフェースのメンバーを提供することにより、クラスに様々な役割を担わせることができるのです。オブジェクト指向プログラミングにおいて継承はよく使用されますが、多重継承はソースコードの見通しが悪くなり、機能全体が読みに難いという点で問題視されることが多いです。
その点においては、先ほどの「契約の定義」が持つ特性により、クラスが保有する機能がインターフェースを通してわかりやすく、複数機能を持っていてもインターフェースの定義を確認することで確認も容易になります。
3.コードの再利用
インターフェースを使用することで、コードの再利用が容易になります。同じインターフェースを実装する異なるクラスは、同じメソッドやプロパティを提供します。これにより、他のプロジェクトやアプリケーションでクラスを再利用しやすくなり、開発効率が向上するという利点があります。
4.テストとモック
インターフェースはユニットテストやモックオブジェクトの作成にも役立ちます。特定のインターフェースを実装する代替実装を提供することで、コードの単体テストを容易に行うことができます。これは、コードの品質と信頼性を向上させるのに役立ちます。
インターフェース(Interface)の実装例
インターフェースの定義と、インターフェースを実装するクラスのサンプルを紹介します。まずは先ほど出てきた ILogger インターフェースの定義を再確認しておきます。メソッドが2つ定義されていることが分かります。
public interface ILogger
{
void LogMessage(string message);
void LogError(string error);
}
上記のインターフェースを実装したクラスは以下に定義できるでしょう。
public class ConsoleLogger : ILogger
{
public void LogMessage(string message)
{
Console.WriteLine($"Message: {message}");
}
public void LogError(string error)
{
Console.WriteLine($"Error: {error}");
}
}
ConsoleLogger クラスにはサンプルみてわかる通り、ILogger インターフェースに定義されているメソッドがしっかりと実装されています。LogMessage メソッドと LogError メソッドです。これを見てわかる通り、ILogger インターフェースを持っているので、その定義の実装(実態)が強制されていることが理解できます。
「定義の実装(実態)が強制されている状態」つまり、定義の存在が確約されているから「契約」となるのです。
インターフェースの利点・本質
インターフェースについて基本的にな事柄を紹介してきました。それでもインターフェースってなんで良いの?と思う人は多いと思います。インターフェースを理解するにはオブジェクト指向プログラミングの理解を深める必要がありますが、シンプルに言ってしまうと「相手をあまり気にしなくてよくなる」という利点があります。
インターフェースは「実装が約束されている」ことでした。これは「相手のことを知らなくても使える」ということです。
インターフェースに定義されたものはクラスに存在していることが確約されているのだから、クラスに実装されている存在を気にすることなく「利用する」ことが出来ます。これは、インターフェースの定義を境界として、クラス間の結合を疎結合にすることに繋がります。
インターフェースを利用した実装での理想形は「モジュールを利用する側はインタフェースに依存させ、本体もインターフェースをもとに実装する」ことです。利用する側がインターフェースに依存し、クラスに依存することがなくなります。インターフェースに依存するということは、同じインターフェースを持っている別クラスでの代替が可能になるということです。
例えば、特定のデータを取得するためのインターフェースを定義しておけば、その取得先がテキストファイルだろうがRDBであろうが、実装がしっかりしていれば、モジュールを利用する側はその変更による影響がないに等しくなるのです。つまり、「変更に強くなるソースコード」になるということです。
利用する側がインターフェースに依存することで、SOLID原則にある「依存性逆転の原則」が実現できます。そして、そういった性質を体現することがインターフェースの利点であり、インターフェースの本質になります。
依存関係性逆転の原則
依存関係逆転の原則(Dependency Inversion Principle、DIP)は、SOLID原則の一部です。この原則は、高レベルのモジュールは低レベルのモジュールに依存してはならず、両方は抽象に依存すべきだと主張しています。また、抽象は詳細に依存すべきではなく、詳細が抽象に依存すべきであるとも述べています。これにより、システム全体の柔軟性が向上し、変更が容易になります。
具体的な例で説明すると、以下のような構造になります。
高レベルのモジュール(上位モジュール)は抽象に依存するべきです。低レベルのモジュール(下位モジュール)もまた抽象に依存するべきで、具体的な実装に依存してはいけません。具体的な実装は抽象に依存するべきで、抽象が具体的な実装に依存してはいけません。
これにより、モジュール間の依存関係が逆転し、システムが柔軟で保守しやすくなります。抽象と具体的な実装が分離されることで、変更が発生しても他の部分に影響が及びにくくなります。具体的なプログラミング言語や実装方法によって、依存関係逆転の原則をどのように実現するかが異なります。例えば、インターフェースや抽象クラスを使用して抽象を定義し、具体的なクラスがこれを実装するなどがあります。
簡単に言ってしまうと先ほどの「モジュールを利用する側はインターフェースで操作し、利用されるモジュールはインターフェースをもとに実装する」ということです。そうすることで抽象が実態の間に境界を作り、影響を最小限に留めてくれるということになります。
まとめ
インターフェースの入門的な内容から、その利点・本質、はたまた依存性逆転の原則まで解説してきました。インターフェースを知れば知るほど、プログラミングが面白くないますし、その反面、プログラミングが難しくなっていくように感じます。実態ありきの実装から、抽象度の高いプログラミングに移行していくということです。
C#のインターフェースは、クラスや構造体が特定のメンバーを提供する契約を定義する強力な方法です。
インターフェースを使用することで、柔軟で拡張可能なコードを設計でき、プログラムの保守性と再利用性を向上させることができます。しっかりと設計されたインターフェースは、柔軟で信頼性のあるコードを設計でき、さまざまなクラスが同じ契約を守ることを保証できます。
こうした保証により、C#のプログラムはより効果的に設計することができ、メンテナンス性が向上します。それに伴ってバグの発生を減らすのに役立つのです。インターフェースを覚えておくことは、オブジェクト指向プログラミングにおいても重要な概念を覚えることでもあるので、この記事で解説してきた内容をしっかりと理解しておくとよいでしょう。
上記のような記事もあわせてチェックしておくと、より抽象を利用したプログラミングについて理解を深められるのではないかと思います。