この記事 is 何?
インターフェースを利用すると何がうれしいのかを簡単解説している記事です.
ただし, 「インターフェースの使い方の入門記事」ではありませんので, ご注意ください.
インターフェースって?
インターフェースとは, 平たく言うと**設計書
** / 仕様書
のことです. もう少し言うならば, クラスや構造体が必ず実装していなければならないルール
や規約
, 契約
, **規格
**などをまとめているもののことをいいます.
すなわち, インターフェースを実装するということは, その契約や規格に同意したということであり, そのクラスや構造体は必ずその規格に準じていることが保証されます.
これは, クラス図レベルの設計に対応しています.
クラス図レベルの設計の場合, 詳細実装ベースではなく, **INPUT
とOUTPUT
**ベースのより粒度の粗いやりとりに興味があるわけです. つまり, クラス同士がどう関わりあうかに主眼が置かれるわけです.
その際に, 何を利用して関わらせるのかが重要になります. それを定義するのが**インターフェース
**なのです.
**インターフェース
**は,「何かと何かを関わらせるために定義しているもの」であるため, **規格
やルール
**を定義していると言えるわけです.
インターフェースを利用するとできること
1. プロパティ/メソッドの実装を強制できる
たとえば, **ICar
**というインターフェースが以下のような宣言になっていた場合を考えてみます.
interface ICar
{
string Make { get; } // [プロパティ] メーカー名
string Model { get; } // [プロパティ] モデル名
string Year { get; } // [プロパティ] 製造年
void StartEngine(); // [メソッド] エンジンをかけるためのメソッド
}
type ICar =
abstract member Make:string
abstract member Model:string
abstract member Year:string
abstract member StartEngine:unit->unit
**ICar
**には, 「3つのプロパティ」と「1つのメソッド」が宣言されています.
しかし, その中身については定義されていません.
そこで, **Carクラス
**を作成してみたいと思います.
class Car : ICar
{
}
type Car (make, model, year) =
interface ICar with
**Carクラス
**には, まだ何も定義がありません. が, **ICarインターフェース
**を実装しています(正確には, 実装していることになっています). ICarインターフェースに準拠しているとも言い換えることができます(正確には, 準拠している(ry).
このとき, **Carクラス
**には以下のようなエラーが発生しています.
これは, **ICarインターフェース
で宣言されているプロパティ/メソッドが, その実装先であるCarクラス
**で, "まだ実装されていない", つまり未定義であるために発生しているものです.
このエラーは, **Carクラス
がICarインターフェース
**で宣言されているプロパティ/メソッドを実際に実装するまで解消されません. つまり, インターフェースで宣言したものの実装をクラス(または構造体)に強制することができるということです.
これを利用することによって, たとえば設計時に実装しなければいけないと決めたインターフェース(= プロパティやメソッド)の実装漏れを回避することが可能になるわけです.
コードベースで仕様の実装漏れが回避できるなんて, すばらしいですね!
実際にインターフェースで宣言されているプロパティ/メソッドを実装することで, エラーを解消することができます.
たとえば, 以下のような実装をすることでエラーを解消することが可能です.
class Car : ICar
{
public string Make { get; } // ICar.Makeプロパティ の実装
public string Model { get; } // ICar.Modelプロパティ の実装
public string Year { get; } // ICar.Yearプロパティ の実装
// ICar.StartEngine()メソッド の実装
public void StartEngine() => Console.WriteLine("エンジンがかかったよ!");
public Car(string make, string model, string year)
=> (Make, Model, Year) = (make, model, year);
}
type Car (make, model, year) =
interface ICar with
member this.Make = make
member this.Model = model
member this.Year = year
member this.StartEngine () = printfn "エンジンがかかったよ!"
2. ジェネリクスでインターフェース制約を使える
次のような**IAdd<T>インターフェース
と, 類似している2つのクラスについて考えます.
2つのクラスは, 一方がIAdd<T>
を実装しているクラスで, もう一方がIAdd<T>
**を実装していないクラスとなります. それ以外については, 両クラスは非常に似ているクラスとなっています.
interface IAdd<T>
{
T Add(T val);
}
// IAdd<T> を実装している Moneyクラス
class Money : IAdd<Money>
{
// [プロパティ] 合計金額
public decimal Amount { get; private set; } = 0M;
// コンストラクタ
public Money(decimal amount) => Amount = amount;
// [メソッド] IAdd<T>.Add()の実装
public Money Add(Money val) => new Money(Amount + val.Amount);
}
// IAdd<T> を実装していないけれど, Add()メソッドを独自で実装している Numberクラス
class Number
{
public double Value { get; private set; } = 0.0;
public Number(double value) => Value = value;
public Number Add(Number val) => new Number(Value + val.Value);
}
type IAdd<'T> =
abstract member Add : 'T -> 'T
// IAdd<'T> を実装している Moneyクラス
type Money (amount:decimal) =
member this.Amount = amount
interface IAdd<Money> with
member this.Add (m) = Money (this.Amount + m.Amount)
// IAdd<'T> を実装していないけれど, Add()メソッドを独自で実装しているNumberクラス
type Number (value: double) =
member this.Value = value
member this.Add (v:Number) = Number (this.Value + v.Value)
ここで, 2つの値を足し合わせるSum()メソッド
を考えます.
class Program
{
static T Sum<T>(T lhs, T rhs)
{
// 以下のように書ければベストですが, 実際は書けません.
return lhs + rhs;
}
}
これを回避するために, **IAdd<T>インターフェース
のAdd()メソッド
**を利用して, **Sum()メソッド
**を実現しようとすると以下のようなコードを真っ先に思いつくかもしれません.
class Program
{
static T Sum<T>(T lhs, T rhs)
{
var l = lhs as IAdd<T>;
var r = rhs as IAdd<T>;
return l.Add(r);
}
}
しかし, このコードはいつもうまく動作するとは限りません. なぜなら, T型は必ずしも IAdd<T>インターフェース
を実装しているとは限らないからです.
これを回避するために, 「インターフェース制約
」という仕組みを利用します.
class Program
{
static T Sum<T>(T lhs, T rhs)
where T : IAdd<T> // T型に対して「IAdd<T>インターフェースを実装していないとダメ!」という制約を設けられる.
{
// IAdd<T>を実装していることが前提のため, Add()メソッドを利用することが可能に!!
return lhs.Add(rhs);
}
}
let Sum<'T when 'T :> IAdd<'T>> (lhs:'T) (rhs:'T) = lhs.Add(rhs)
この仕組みを利用することによって, パラメータに渡せる型をある程度制限することが可能になっています.
また, インターフェース制約を利用することによって, インターフェースで宣言されているプロパティやメソッドを利用することが可能になります. 以下はそれを確認するためのスクリーンショットです.
これは, 「1. プロパティ/メソッドの実装を強制できる」で紹介したように, プロパティやメソッドの実装を強制できるからこそ, この制約が成立するわけですね.
サンプルコードは以下のようになります.
class Program
{
static T Sum<T>(T lhs, T rhs)
where T : IAdd<T>
{
return lhs.Add(rhs);
}
static void Main(string[] args)
{
var money1 = new Money(500);
var money2 = new Money(2_000);
var sum = Sum(money1, money2);
Console.WriteLine($"amount= {sum.Amount}");
// output:
// amount= 2500
}
}
let Sum<'T when 'T :> IAdd<'T>> (lhs:'T) (rhs:'T) = lhs.Add(rhs)
Sum (Money 500M) (Money 1_500M)
|> (fun m -> printfn "amount= %A" m.Amount)
// output:
// amount= 2000M
ここで, **IAdd<T>
**を実装していないNumber型を渡せるか気になりますよね?
**IAdd<T>
を実装していないとはいえ, 実際にAdd()メソッド
は実装しています. このSum()メソッド
**を利用できるか確認してみましょう.
- C#
class Program
{
static T Sum<T>(T lhs, T rhs)
where T : IAdd<T>
{
return lhs.Add(rhs);
}
static void Main(string[] args)
{
var num1 = new Number(500);
var num2 = new Number(2_000);
// 以下のコードはエラーに...
var sum = Sum(num1, num2);
Console.WriteLine(sum.Amount);
}
}
- F#
// 以下のコードはエラーに...
Sum (Number 500.) (Number 1_500.)
|> (fun m -> printfn "amount= %A" m.Amount)
**Add()メソッド
**を実装していてもエラーとなってしまいました.
これは, 当然といえば当然で, インターフェース制約
という名前からもわかるとおり, インターフェースを実装しているかどうかが重要なわけです. C#やF#のように強い静的型付け言語においては, さまざまな場面において型によって解決をはかります. その機能のひとつとして, インターフェース制約があるわけですね.
3. 単体テストが可能に!
これは, **ジェネリクスのインターフェース制約
**を利用することによって, 単体テストを作成することが非常に容易となります.
特に, DBやネットワーク通信など, 外部とやりとりするような箇所についての単体テストで威力を発揮します. もちろん, 通常の単体テストを作成する上でも非常に役立ちます.
これについては part2
で詳しく紹介しようと思います.
余談
ここまで長い文章をお読みいただき, ありがとうございました.
part2
は近日中に公開したいと思いますが, いつになるかはわかりませんので, あしからず....
もし, この記事をお読みいただいて, 単体テストのくだりに興味をもっていただいた方につきましては, part2
までお待ちいただくか, **Youtube Live
**の方で直接ご質問いただければと思います.
Youtube Liveはこちらからチャンネル登録していただければ幸いです.
また, この記事ではF#についてもご紹介してみました.
もし, F#に少しでも興味がわいたかたがいらっしゃいましたら, **Youtube Live
**の方にお越しいただければと思います.
私自身, F#のリファレンスサイトを現在作成中なので, そちらを参考にしていただいても問題ございません.
ぜひ, 来ていただければなと思います.
今回の記事の参考になりそうなページは以下になります.
|> F# | クラス
|> F# | インターフェース
|> F# | パイプラインと関数合成
以上です.