今回は「インターフェイス」について解説します。
インターフェイスとは、関連性のないクラス間で共通の振る舞いを定義し、違うクラスを共通に扱えるようにする仕組みです。
例えば、音を出させるというインフェーフェイス型があり、この型の変数soundMakerがあるとしましょう。
soundMaker.MakeSound()という音を出すメソッドを呼び出したとき、soundMakerに代入された音を出せるインターフェイスを実装したインスタンスが、
- 犬型インスタンスならば、「ワン」と出力する
- 猫型インスタンスならば、「ニャア」と出力する
- ドラム型インスタンスならば、「ドンドン」と出力する
というふうに型に応じて処理を切り替えられます。
…と、ここまで話を聞いて「あれ、どこかで聞いた話だな」と思われたでしょうか。
「インターフェイス」は、オブジェクト指向におけるポリモーフィズムの1形態なのです。
クラス継承とメソッドのオーバーライドを使ったポリモーフィズムの実現については「C#入門編(10)」の記事で解説しました。
本記事はオブジェクト指向の基本知識(カプセル化、継承、クラス継承によるポリモーフィズム)を前提とするため、あらかじめ見ておいてもらえるとより理解が深まると思います。
クラス継承によるポリモーフィズムとインターフェイスによるポリモーフィズムの違いは以下のようなイメージです。
インターフェイスの特徴は、関連性のないクラス間で共通の振る舞いを定義できる点です。
本記事では以下について解説します。
- インターフェイスとは何か、そして具体的にどのようなシチュエーションで使うのか(JSONファイルの出力という具体例で解説)
- クラスとインターフェイスの違い
演習のコードはGitHubにあります。
YouTubeの動画は以下になります。
演習1:さまざまなクラスをJSON出力する仕組みを作る
元となるプログラム
まず演習1の土台となるプログラムとして、レストランメニューをHTMLで生成するプログラムを使います。
これまで入門編で使ってきたコードですね。入門編(11)で名前空間・ファイル分割を行ったものを使います。コードはGitHubにもあります。
追加する機能
レストランではメニューが多岐にわたり、その中には飲み物、主菜、デザートのカテゴリーが存在しています。
今回、あるプロモーションの一環として、レストランは「飲み物メニュー」の詳細データをパートナーアプリへ提供することになったとしましょう。
飲み物メニューのデータはJSON形式のファイルとして提供します。
今回作成する機能:
レストランの飲み物メニューをJSON形式のテキストとして出力する
例えば、以下のようなDrinkMenu型のインスタンスであるメロンソーダとホットコーヒーがあったとしましょう。
DrinkMenu melonSoda = new DrinkMenu("メロンソーダ", "爽やかな甘さが楽しめるメロンソーダです。", 400, true);
DrinkMenu hotCoffee = new DrinkMenu("ホットコーヒー", "丁寧に焙煎されたコーヒー豆を使用しています。", 500, false);
これらを次のようなJSON形式のテキストデータを出力するプログラムを作ります。
[
{
"Type": "Drink",
"Name": "メロンソーダ",
"Description": "爽やかな甘さが楽しめるメロンソーダです。",
"Price": 400,
"IsCold": true
},
{
"Type": "Drink",
"Name": "ホットコーヒー",
"Description": "丁寧に焙煎されたコーヒー豆を使用しています。",
"Price": 500,
"IsCold": false
}
]
今回の演習ではJSONについて、以下のことを知っていれば大丈夫です。
JSONとは?
- JSON(JavaScript Object Notation)はデータ交換形式の一つで、データを人間が読み書きできるテキスト形式で表現する。
- JSONでは、データは「キー: 値」ペアの形式で表され、これを{}内に列挙することでオブジェクトを形成する。
- 配列は[]内に値をコンマ区切りで列挙することで表現する。
今回は飲み物メニューに関してのみJSON形式のテキストに変換できればよいのですが、将来、他のメニュー(他のクラス)もJSON形式へ変換したくなるかもしれませんね。
なので、JSON形式のテキストへ変換する仕組みはなるべく汎用性が高くなるように作るため、インターフェイスを使います。
インターフェイスとは?
インターフェイスとは以下のようなものです。
インターフェイスとは、
- あるクラスがどのような操作(メソッド)を行い、どのような情報を持つべきか(プロパティ)を定義する
- しかし、その操作が具体的に何をするか、または情報がどのように格納されるかについては定義しない
- 情報の具体的な保存場所(フィールド)をインターフェイスでは定義できない
インターフェイスは、クラスがどのような操作を行えばいいか、どんな情報を持つべきかの「外部仕様」を提供します。
しかし、それが具体的にどのように行われるか、またはデータがどのように保存されるかという「内部実装」は提供しないのです。
インターフェイスを使った機能追加の方針
インターフェイスを具体的にどのように使うか見てみましょう。
インターフェイスを使って、もともとのプログラムを以下のように機能拡張します。
追加・修正する部分は以下です。
- JsonGeneratorクラス:GenerateJsonメソッドでIJsonWritableインターフェイスの配列を受け取り、そこからJSON形式のテキストを生成
- IJsonWritableインターフェイス:JSON形式でテキストを出力するToJsonというメソッドを宣言
- DrinkMenu:IJsonWritableを実装し、飲み物のデータをJSON形式のテキストとして出力するToJsonメソッドを実装
これによりDrinkMenuは、(1)に加えて(2)のように振る舞えるようになります。
- (1) Menuの子クラスであるため、Menuとして振る舞える
- (2) IJsonWritableを実装するため、IJsonWritableとして振る舞える
以下のようにインターフェイスは外部仕様を定義し、クラスはインターフェイスを実装し、インターフェイスの外部仕様を満たすよう内部実装を定義します。
演習1コード
方針に従って、レストランメニュー生成のコードを以下のように修正、拡張します。
\---Restaurant
| Program.cs ★(a)修正
+---Generators
| MenuTableGenerator.cs
| JsonGenerator.cs ★(b)追加
+---Menus
| DessertMenu.cs
| DrinkMenu.cs ★(c)修正
| MainMenu.cs
| Menu.cs
| IJsonWritable.cs ★(d)追加
GitHubに全コードがありますので、それも参考にしてください。
それぞれのコードでポイントを解説します。
(d) IJsonWritable.cs
IJsonWritableインターフェイスを定義します。
namespace Restaurant.Menus
{
internal interface IJsonWritable
{
string ToJson();
}
}
インターフェイスは「interface インターフェイス名 {…}」と定義します。
アクセス修飾子(今回はinternal)の付け方はclassと同じです。
インターフェイス名は一般的にIJsonWritableのように名前の先頭をIにして、インターフェイスであることがすぐわかるようにします。
インターフェイスでは、メソッドやプロパティを定義できます。
ここでは外部仕様としてToJsonメソッドを定義しています。
IJsonWritableを実装するクラスは外部仕様で定められたToJsonメソッドへ具体的な内部実装を提供する必要があるというわけですね。
(c) DrinkMenu.cs
IJsonWritableを実装し、ToJsonメソッドを実装したDrinkMenuクラスは以下のようになります。
using System.Text;
namespace Restaurant.Menus
{
//飲み物メニューの情報を格納するクラス(Menuから派生した子クラス)
internal class DrinkMenu : Menu, IJsonWritable
{
private bool isCold;
public DrinkMenu(string name, string description, int price, bool isCold)
: base(name, description, price)
{
this.isCold = isCold;
}
…省略…
public string ToJson()
{
StringBuilder jsonBuilder = new StringBuilder();
jsonBuilder.AppendLine("{")
.AppendLine($" \"Type\": \"Drink\",")
.AppendLine($" \"Name\": \"{Name}\",")
.AppendLine($" \"Description\": \"{Description}\",")
.AppendLine($" \"Price\": {Price},")
.AppendLine($" \"IsCold\": {IsCold.ToString().ToLower()}")
.Append("}");
return jsonBuilder.ToString();
}
}
}
クラスを継承するときと同様に、「class クラス名 : 実装するインターフェイス名 {…}」というふうに、記述してインターフェイスを実装します。
継承するクラスがある場合は、「class DrinkMenu : Menu, IJsonWritable」とカンマで区切って書きます。
そして、インターフェイスで定義されているメソッド等へ内部実装を与えます。ここではDrinkMenuのデータを出力するToJsonメソッドを実装しています。
今回、標準ライブラリで提供されているStringBuilderクラスを使って文字列の連結を行いました。
StringBuilderを使うと複数の文字列の連結を、文字列連結演算子を使うよりも良いパフォーマンスで行えます。
StringBuilderについてはこちらも参考にしてください。
(b) JsonGenerator.cs
IJsonWritableの配列から配列構造をもつJSON形式のテキストを出力する役割を持つJsonGeneratorクラスを定義します。
using Restaurant.Menus;
using System.Text;
namespace Restaurant.Generators
{
internal class JsonGenerator
{
public string GenerateJson(IJsonWritable[] items)
{
StringBuilder jsonBuilder = new StringBuilder();
jsonBuilder.AppendLine("[");
for (int i = 0; i < items.Length; i++)
{
// items[i]がIJsonWritableインターフェイスを実装しているため、ToJsonメソッドを呼び出すことができます。
// ToJsonメソッドは、そのオブジェクトのJSON表現を文字列として返します。
jsonBuilder.AppendLine(items[i].ToJson());
// 最後の要素以外の後にはカンマを追加します。
if (i < items.Length - 1)
{
jsonBuilder.AppendLine(",");
}
}
jsonBuilder.Append("]");
return jsonBuilder.ToString();
}
}
}
GenerateJsonメソッドはIJsonWritable型配列に格納されているインスタンスについて、それぞれToJsonメソッドを呼び出し、配列構造をもつJSON形式のテキストを作成します。
ここで重要なのは、IJsonWritableインターフェイスを実装しているクラスのインスタンスであれば、中身がどのようなクラスであってもよいという点です。
ToJsonメソッドをもつという外部仕様を満たしていればよく、インスタンスの型に応じて適切なToJsonの内部実装が呼び出されます。
(a)Program.cs
飲み物データをJSON形式で出力します。
using Restaurant.Generators;
using Restaurant.Menus;
namespace Restaurant
{
internal class Program
{
static void Main(string[] args)
{
DrinkMenu melonSoda = new DrinkMenu("メロンソーダ", "爽やかな甘さが楽しめるメロンソーダです。", 400, true);
DrinkMenu hotCoffee = new DrinkMenu("ホットコーヒー", "丁寧に焙煎されたコーヒー豆を使用しています。", 500, false);
IJsonWritable[] drinks = new IJsonWritable[] { melonSoda, hotCoffee };
JsonGenerator generator = new JsonGenerator();
string json = generator.GenerateJson(drinks);
Console.WriteLine(json);
}
}
}
演習1コードを実行
このプログラムを実行すると、飲み物データ(メロンソーダ、ホットコーヒー)のJSON形式テキストを出力します。
これで、飲み物のデータについてJSON形式のテキストとして、パートナーアプリへ渡せるようになりましたね。
将来、もしデザートのデータについてもJSON形式で出力したくなったら、
- DessertMenuクラスへIJsonWritableインターフェイスを実装
- DessertMenuのデータを出力するToJsonメソッドを実装
としてあげれば、JsonGeneratorでDessertMenuについても出力できるように容易に拡張可能です。
そして、以下のようにDrinkMenuとDessertMenuをIJsonWritable型として扱い、GenerateJsonメソッドへ渡すことが可能です。
DrinkMenu melonSoda = new DrinkMenu("メロンソーダ", "爽やかな甘さが楽しめるメロンソーダです。", 400, true);
DrinkMenu hotCoffee = new DrinkMenu("ホットコーヒー", "丁寧に焙煎されたコーヒー豆を使用しています。", 500, false);
DessertMenu chocolateCake new DessertMenu("チョコレートケーキ", "しっとりとしたチョコレートケーキです。", 600, 4),
IJsonWritable[] menus = new IJsonWritable[] { melonSoda, hotCoffee, chocolateCake};
…
string json = generator.GenerateJson(menus);
GenerateJsonメソッドは修正せずにそのまま使えます。
実践では、クラスからJSON形式のテキストへ変換するための既存ライブラリは色々とあるため、そちらを使うとよいでしょう。
講義1:クラスとインターフェイスの違いとは?
クラスとインターフェイスは何が違うのでしょうか?
少し整理してみましょう。
端的に言うと、クラスは「設計図」のようなもので、インターフェイスは「約束事」のようなものなのです。
以下が、両者の比較です。
演習1のコードならば、Menu、DrinkMenuといったクラスはメニューの設計図です。
IJsonWritableインターフェイスは、JSON形式のテキストを出力可能という約束事です。
インターフェイスは違うクラス階層に属するクラスであっても、継承関係とは別に実装できます。
演習1コードのIJsonWritableインターフェイスは、まさにクラス階層(例えばレストランメニューの階層)とは関係なく、様々なクラスで横断的に使える「約束事」といえますね。
また、クラスの継承は1つしか行えないのですが、インターフェイスについては複数実装できます。
例えば、以下のようなことも可能です。
この例では、ILogWritableというログ出力可能という約束事をもつインターフェイスを定義し、DrinkMenuへILogWritableも実装しています。
このときDrinkMenuは、
- Menu型として振る舞える
- IJsonWritable型として振る舞える
- ILogWritable型として振る舞える
というふうに、様々な型として振る舞うことが可能となります。
例えば、BlazorなどのWebアプリケーションのフレームワークでは、インターフェイスを使ってこのような設計(「依存性注入」と呼ばれます)を実現しています。
おまけ:多重継承と単一継承
クラスとインターフェイスの違いについてはなんとなくわかったでしょうか?
説明を聞いていて、以下のように思った人がいるかもしれません。
インターフェイスってなぜ必要なのか?
クラスを複数継承できるようにすれば、インターフェイスは不要ではないか?
仮にクラスを複数継承することができたら、IJsonWritableインターフェイスは抽象クラスと抽象メソッドを使って次のように書けるでしょう。
このような仕組みを多重継承と言います。C#では多重継承ができず単一継承のみ使えます。
C++、Pythonなど多重継承を使えるプログラミング言語もあります。
しかし、多重継承はコードを複雑にする可能性があります。
複数の親クラスから継承を行うと、それぞれの親クラスからどの振る舞いが継承されるのか、それらがどのように相互作用するのかを把握することが難しくなります。
そのため、C#を含めた多くのモダンなプログラミング言語は単一継承しか許さず、代わりにインターフェイスなどの仕組みを設けていることが多いです。
まとめ
今回は、インターフェイスについて説明しました。
インターフェイスはクラス階層と関係なく、様々なクラスで横断的に使える「約束事」のようなものです。
これにより、関連性のないクラス間で共通の振る舞いを定義して、それらのクラスを同じように扱うことが可能になります。
ソフトウェアは外部仕様と内部実装をきれいに分けて設計し、内部実装を交換可能にしておくと後々修正や拡張が楽になります。
そのような設計のためにインターフェイスは役立ちます。
既存のライブラリ・フレームワークでもインターフェイスは多用されているため、基本的な考え方や使い方を知っておくのは重要です。