インターフェースの何が嬉しいのか
これはなに?
- 自分が初心者だったころ、インターフェースの理解はさっぱりだった。
- 同じように悩める初心者がいたら、理解を助ける手伝いをしたい。
- 概念の話ではなく、インターフェースがあると具体的に何が嬉しいのか。
誰に読んで欲しいの?
- インターフェースの概念はわかった、使い方もわかった、でも使い道がわからない人
そもそもインターフェースって?
インターフェースは機能の実装を強制する仕組みです。
このインターフェースを継承するクラスは○○の機能を持ってるよ、ということを保証します。
参考:
http://ufcpp.net/study/csharp/oo_interface.html
https://qiita.com/IganinTea/items/e1d35db0a14a84bda452
インターフェースを使うと嬉しいこと
便利な構文が使える
IDisposable
私が一番よく使っている標準クラスライブラリのインターフェースはIDisposableです。
このインターフェースを継承するとDispose
の実装が強制されます。
Dispose
の実装が強制されて何が嬉しいかというと、using
ステートメントを使用することができます。
そしてusing
ステートメントが利用できるとアンマネージリソース1を扱うときにリソースの解放忘れを気にしなくて良くなります。
利用例
usingを使う代表的な例がStreamReaderクラスです。
StreamReaderの親クラスであるTextReaderがIDisposableを継承しています。
using System.IO;
using (StreamReader stream = new StreamReader("<File Path>")) {
Console.WriteLine(stream.ReadToEnd());
}
ちなみに上記のコードは以下のコードと等価です。
using System.IO;
StreamReader stream = new StreamReader("<File Path>");
try {
Console.WriteLine(stream.ReadToEnd());
} finally {
if (stream != null)
stream.Dispose();
}
実装例
IDisposableを継承してテキストファイルを表すクラスを実装すると以下のようになります。
(全く意味ないクラスだけど無理やり作ってみた)
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace ConsoleApp {
public class TextFile : IDisposable {
private StreamReader stream;
public TextFile(string path) {
stream = new StreamReader(path, Encoding.UTF8);
}
public IEnumerable<string> ReadLines() {
string line;
while((line = stream.ReadLine()) != null) {
yield return line;
}
}
public void Dispose() {
stream.Close();
}
}
}
Open/Closeという概念があるクラスの場合、IDisposableを継承しusingステートメントを利用できるようにしておくと分かりやすく堅牢になるため良いです。
後述のIEnumerable
/IEnumerable<T>
インターフェースを使うと、foreach
ステートメントを利用できたりもします。
自作のコレクションなんて使ったこと無いけど。。。
余計なことは気にしなくていい
Linqに見るインターフェースの活用
みなさんLinq使ってますか?
私はLinqの為にC#を使っていると言っても過言ではないくらいLinq大好きです。
LinqでSelect
とかWhere
とか使うと、返ってくるのはIEnumerable<T>
ですよね。ここではIEnumerable<T>
が返ってくると何が嬉しいのか考えてみます。
まずListをSelect
した例を見てみたいと思います。
List<string> list = new List<string>() { "hoge", "fuga", "piyo" };
IEnumerable<string> results = list.Select(x => x);
Listに対してSelect
を通しただけの文ですが、この時resultsの実体は何なのでしょうか?
正解はWhereSelectListIterator<TSource, TResult>
クラスのインスタンスです。
http://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,37c6daad6e403a3b,references
これがListではなく配列だった場合、WhereSelectArrayIterator<TSource, TResult>
が返ってきますし、
これがSelect
ではなくWhere
だった場合、WhereListIterator<TSource>
が返ってきます。
この時Select
の実装はこんな感じです。
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) { if (source == null) throw Error.ArgumentNull("source"); if (selector == null) throw Error.ArgumentNull("selector"); if (source is Iterator<TSource>) return ((Iterator<TSource>)source).Select(selector); if (source is TSource[]) return new WhereSelectArrayIterator<TSource, TResult>((TSource[])>source, null, selector); if (source is List<TSource>) return new WhereSelectListIterator<TSource, TResult>((List<TSource>)source, null, selector); return new WhereSelectEnumerableIterator<TSource, TResult>(source, null, selector); }
様々なクラスのインスタンスがreturnされますが、戻り値はIEnumerable<T>
の為これらのインスタンスはIEnumerable<T>
としかふるまいませんし扱われません。つまりインスタンスが何であるかは気にしなくて良いのです。
私たちがSelect
やWhere
に期待することはIEnumerable<T>
を継承するコレクションから呼び出せて、結果をIEnumerable<T>
として戻すこと、それだけで十分なのです。
もちろんインデクサを使ったアクセスや要素の追加・削除がしたい場合は
IEnumerable<T>
では不十分です。
その場合はToList()
してListとして扱うと良いです。
またIEnumerable<T>
を継承さえしていればインスタンスは何でも良い状態は変化に強いと言えます。仮にSelect
の実装に変更があり、WhereSelectEnumerableIterator<TSource, TResult>
が全く違うクラスに置き換わったとしても、利用する側に影響は無いでしょう。
業務システムで使うインターフェース
もう一つ例を見てみます。
業務システムで受注データをファイルから読み取り、DBへ格納するという処理は割と一般的です。
そして受注データのファイルが取引先によって形式もフォーマットもバラバラということもまた一般的です。
この処理を実装してみたいと思います。
取引先 | ファイル形式 |
---|---|
A社 | テキスト(固定長) |
B社 | CSV |
C社 | XML |
まずDBへ格納するパラメータは商品(ItemId)・納期(DeliveryDate)・数量(Quantity)の3つとしましょう。
それを1件分保持する構造体を作ります。
public struct Order {
public Order(string itemId, DateTime deliveryDate, int quantity) {
ItemId = itemId;
DeliveryDate = deliveryDate;
Quantity = quantity;
}
public string ItemId { get; private set; }
public DateTime DeliveryDate { get; private set; }
public int Quantity { get; private set; }
}
そして受注ファイルのインターフェースIOrderFile
を作成します。
ファイルの内容(受注データ)を列挙して返すRead
メソッドと、読み終わったファイルを削除するDelete
メソッドをふるまいとして定義します。
using System.Collections.Generic;
namespace ConsoleApplication1 {
public interface IOrderFile {
// 受注データを列挙して返す
IEnumerable<Order> Read();
// ファイルを削除する
void Delete();
}
}
受注ファイルの具象クラスOrderFileFromA
・OrderFileFromB
・OrderFileFromC
も作成します。
各クラスでそれぞれのメソッドに対し個別の処理を実装をします。
コンストラクタでファイルパスを受けてローカル変数として保持していますが、Read
メソッドで必要になる(はず)です。
using System.Collections.Generic;
namespace ConsoleApplication1 {
public class OrderFileFromA : IOrderFile {
private string filePath;
public OrderFileFromA(string filePath) {
this.filePath = filePath;
}
public IEnumerable<Order> Read() {
// 固定長テキストの処理
}
public void Delete() {
// ファイル削除処理
}
}
public class OrderFileFromB : IOrderFile {
private string filePath;
public OrderFileFromB(string filePath) {
this.filePath = filePath;
}
public IEnumerable<Order> Read() {
// CSVファイルの処理
}
public void Delete() {
// ファイル削除処理
}
}
public class OrderFileFromC : IOrderFile {
private string filePath;
public OrderFileFromC(string filePath) {
this.filePath = filePath;
}
public IEnumerable<Order> Read() {
// XMLファイルの処理
}
public void Delete() {
// ファイル削除処理
}
}
}
前提となるコードが多いですが頑張りましょう。
IOrderFile
の具象クラスを3つ定義しましたが、これらの生成(new
)はどうやってやりましょう?
それぞれのファイル形式が異なる(拡張子で判別できる)ので、ファイルパスから判別できます。2
(ファイル名から取引先が判別できればそれでも良いと思います。)
ファイルパスをインプットして、具象クラスをアウトプットするFactoryクラス
が今回は適当です。3
using System;
namespace ConsoleApplication1 {
public static class IOrderFileFactory {
public static IOrderFile Create(string filePath) {
// ファイルパスで判別して具象クラスを返す。
if (<判別>) {
return new OrderFileFromA(filePath);
} else if (<判別>) {
return new OrderFileFromB(filePath);
} else if (<判別>) {
return new OrderFileFromC(filePath);
} else {
throw new ApplicationException();
}
}
}
}
くどいようですが、具象クラスのインスタンスをreturnしても戻り値はインターフェースという点がポイントです。
最後にMainクラスです。ファイルパスはargsから受け取る想定です。
using System.Collections.Generic;
namespace ConsoleApplication1 {
class Program {
static void Main(string[] args) {
IOrderFile file = IOrderFileFactory.Create(args[0]);
IEnumerable<Order> orders = file.Read();
foreach(var order in orders) {
// orderをDB登録する処理
}
file.Delete();
}
}
}
MainクラスではIOrderFile
の具象クラスは登場せず、インターフェースのみで動いています。
必要なのはRead()
とDelete()
の機能のみであり、ファイルがテキストだろうがCSVだろうが関係ないのです。
そして具象クラスに依存するコードは全てIOrderFileFactory
にあります。
当然取引先が増えることも減ることもありえるでしょう。しかしその際はIOrderFileFactory
だけの変更で済みます。変更に対する影響範囲が限定されることは大きなメリットの一つでしょう。
インターフェースを使う癖をつけよう
今までインターフェースを使ったことが無い人にはIDisposable
がオススメです。利点もわかりやすいし、実装も簡単です。
インターフェースは使おうと思わないと使わない機能なので、意識して使っていく気持ちが大事です。
なんでもかんでもインターフェースにすれば良いというものではありませんが、「これってインターフェースにできないかな?」と考える癖をつけてみましょう。
最後に
「インターフェースに対してプログラミングしろ」という原則は特に目新しいものでもなく、本稿の結論も全く同じです。
しかし車とか林檎とかの概念で説明されても正直わかりません。
私はデザインパターンを勉強する中で実際に手を動かしやっと利点がつかめた気がしました。
初心者・初級者の方のふわっとした理解が少しでも固まる手助けになれば幸いです。