LoginSignup
9

More than 3 years have passed since last update.

インターフェース超超超入門 part1

Last updated at Posted at 2019-04-07

この記事 is 何?

インターフェースを利用すると何がうれしいのかを簡単解説している記事です.
ただし, 「インターフェースの使い方の入門記事」ではありませんので, ご注意ください.

インターフェースって?

インターフェースとは, 平たく言うと設計書 / 仕様書のことです. もう少し言うならば, クラスや構造体が必ず実装していなければならないルール規約, 契約, 規格などをまとめているもののことをいいます.
すなわち, インターフェースを実装するということは, その契約や規格に同意したということであり, そのクラスや構造体は必ずその規格に準じていることが保証されます.

これは, クラス図レベルの設計に対応しています.
クラス図レベルの設計の場合, 詳細実装ベースではなく, INPUTOUTPUTベースのより粒度の粗いやりとりに興味があるわけです. つまり, クラス同士がどう関わりあうかに主眼が置かれるわけです.

その際に, 何を利用して関わらせるのかが重要になります. それを定義するのがインターフェースなのです.
インターフェースは,「何かと何かを関わらせるために定義しているもの」であるため, 規格ルールを定義していると言えるわけです.

インターフェースを利用するとできること

1. プロパティ/メソッドの実装を強制できる

たとえば, ICarというインターフェースが以下のような宣言になっていた場合を考えてみます.

C#
interface ICar
{
    string Make { get; }    // [プロパティ] メーカー名
    string Model { get; }   // [プロパティ] モデル名
    string Year { get; }    // [プロパティ] 製造年

    void StartEngine();     // [メソッド] エンジンをかけるためのメソッド
}
F#
type ICar =
    abstract member Make:string
    abstract member Model:string
    abstract member Year:string
    abstract member StartEngine:unit->unit

ICarには, 「3つのプロパティ」と「1つのメソッド」が宣言されています.
しかし, その中身については定義されていません.

そこで, Carクラスを作成してみたいと思います.

C#
class Car : ICar
{
}
F#
type Car (make, model, year) =
    interface ICar with

Carクラスには, まだ何も定義がありません. が, ICarインターフェースを実装しています(正確には, 実装していることになっています). ICarインターフェースに準拠しているとも言い換えることができます(正確には, 準拠している(ry).
このとき, Carクラスには以下のようなエラーが発生しています.

  • C#
    image.png
  • F# image.png

これは, ICarインターフェースで宣言されているプロパティ/メソッドが, その実装先であるCarクラスで, "まだ実装されていない", つまり未定義であるために発生しているものです.
このエラーは, CarクラスICarインターフェースで宣言されているプロパティ/メソッドを実際に実装するまで解消されません. つまり, インターフェースで宣言したものの実装をクラス(または構造体)に強制することができるということです.

これを利用することによって, たとえば設計時に実装しなければいけないと決めたインターフェース(= プロパティやメソッド)の実装漏れを回避することが可能になるわけです.
コードベースで仕様の実装漏れが回避できるなんて, すばらしいですね!

実際にインターフェースで宣言されているプロパティ/メソッドを実装することで, エラーを解消することができます.
たとえば, 以下のような実装をすることでエラーを解消することが可能です.

C#
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);
}
F#
type Car (make, model, year) =
    interface ICar with
        member this.Make = make
        member this.Model = model
        member this.Year = year
        member this.StartEngine () = printfn "エンジンがかかったよ!"

IDEからエラーが消えていることを確認することができます.
image.png
image.png

2. ジェネリクスでインターフェース制約を使える

次のようなIAdd<T>インターフェースと, 類似している2つのクラスについて考えます.
2つのクラスは, 一方がIAdd<T>を実装しているクラスで, もう一方がIAdd<T>を実装していないクラスとなります. それ以外については, 両クラスは非常に似ているクラスとなっています.

C#
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);
}
F#
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()メソッドを考えます.

C#
class Program
{
    static T Sum<T>(T lhs, T rhs)
    {
        // 以下のように書ければベストですが, 実際は書けません.
        return lhs + rhs;
    }
}

上記のコードは, 以下のようなエラーが発生しています.
image.png

これを回避するために, IAdd<T>インターフェースAdd()メソッドを利用して, Sum()メソッドを実現しようとすると以下のようなコードを真っ先に思いつくかもしれません.

C#
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>インターフェース を実装しているとは限らないからです.
これを回避するために, 「インターフェース制約」という仕組みを利用します.

C#
class Program
{
    static T Sum<T>(T lhs, T rhs)
        where T : IAdd<T>   // T型に対して「IAdd<T>インターフェースを実装していないとダメ!」という制約を設けられる.
    {
        // IAdd<T>を実装していることが前提のため, Add()メソッドを利用することが可能に!!
        return lhs.Add(rhs);
    }
}
F#
let Sum<'T when 'T :> IAdd<'T>> (lhs:'T) (rhs:'T) = lhs.Add(rhs)

この仕組みを利用することによって, パラメータに渡せる型をある程度制限することが可能になっています.
また, インターフェース制約を利用することによって, インターフェースで宣言されているプロパティやメソッドを利用することが可能になります. 以下はそれを確認するためのスクリーンショットです.

  • インターフェース制約なしの場合

    当たり前ですが, Add()メソッドは候補に出てきません.
    image.png
    もちろん, Add()メソッドを利用してもエラーになります.
    image.png

  • インターフェース制約ありの場合

    Add()メソッドが候補として表示されるようになります.
    image.png

これは, 「1. プロパティ/メソッドの実装を強制できる」で紹介したように, プロパティやメソッドの実装を強制できるからこそ, この制約が成立するわけですね.
サンプルコードは以下のようになります.

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 money1 = new Money(500);
        var money2 = new Money(2_000);
        var sum = Sum(money1, money2);
        Console.WriteLine($"amount= {sum.Amount}");
        // output:
        //      amount= 2500
    }
}
F#
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#
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);
    }
}

image.png

  • F#
F#
// 以下のコードはエラーに...
Sum (Number 500.) (Number 1_500.)
|> (fun m -> printfn "amount= %A" m.Amount)

image.png

Add()メソッドを実装していてもエラーとなってしまいました.
これは, 当然といえば当然で, インターフェース制約 という名前からもわかるとおり, インターフェースを実装しているかどうかが重要なわけです. C#やF#のように強い静的型付け言語においては, さまざまな場面において型によって解決をはかります. その機能のひとつとして, インターフェース制約があるわけですね.

3. 単体テストが可能に!

これは, ジェネリクスのインターフェース制約を利用することによって, 単体テストを作成することが非常に容易となります.
特に, DBやネットワーク通信など, 外部とやりとりするような箇所についての単体テストで威力を発揮します. もちろん, 通常の単体テストを作成する上でも非常に役立ちます.

これについては part2 で詳しく紹介しようと思います.

余談

ここまで長い文章をお読みいただき, ありがとうございました.
part2 は近日中に公開したいと思いますが, いつになるかはわかりませんので, あしからず....

もし, この記事をお読みいただいて, 単体テストのくだりに興味をもっていただいた方につきましては, part2 までお待ちいただくか, Youtube Liveの方で直接ご質問いただければと思います.
Youtube Liveはこちらからチャンネル登録していただければ幸いです.

また, この記事ではF#についてもご紹介してみました.
もし, F#に少しでも興味がわいたかたがいらっしゃいましたら, Youtube Liveの方にお越しいただければと思います.
私自身, F#のリファレンスサイトを現在作成中なので, そちらを参考にしていただいても問題ございません.
ぜひ, 来ていただければなと思います.

今回の記事の参考になりそうなページは以下になります.
|> F# | クラス
|> F# | インターフェース
|> F# | パイプラインと関数合成

以上です.

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9