Open6

GoのInterfaceについて

Hina🐣Hina🐣

インターフェースとは

ある機能を持っていることを保証する宣言
→「中身に関係なく、インターフェースで定義された関数を持っていればOK」という抽象的な型

インターフェースの必要性
◇ソフトウェア柔軟性のため

  • 別の実装に切り替えることが簡単
  • 実装を隠蔽して、使う側から見たらシンプルになる
  • テストでモックが使用可能になる
    拡張性、保守性、テスト容易性が確保される

クラスとの関係性

インターフェースは「クラスに対してこういう機能を持て!!」という約束事をする存在

  • クラス:データとふるまい(メソッド)を持つ具体的な実装
  • インターフェース:ある振る舞いを定義した抽象的な型(中身はもたない)
Hina🐣Hina🐣

GolangでのInterface実装

package main

import "fmt"

type Eater interface {
	Putin() //口に入れる
	Chew()  //噛む
}

type Human struct {
	Height int
}

type Dog struct {
	Kind string
}

// 人間用のインターフェースを実装
func (h Human) Putin() {
	fmt.Println("箸で食べる")
}

func (h Human) Chew() {
	fmt.Println("歯でしっかり噛む")
}

// 犬用のインターフェースを実装
func (d Dog) Putin() {
	fmt.Println("床からそのまま食べる")
}

func (d Dog) Chew() {
	fmt.Println("人間と同じように歯で食べる")
}

// インターフェースが引数になる、食べるメソッド
func EatAll(e Eater) {
	e.Putin()
	e.Chew()
}

func main() {
	man := Human{Height: 170}
	hina := Dog{Kind: "Cavaria"}
	fmt.Println("<人間が食べる>")
	EatAll(man)
	fmt.Println("<犬が食べる>")
	EatAll(hina)
}

上記はGolangでInterfaceを実装した例
Golangでは暗黙的にInterfaceを実装できる。
つまり

func (h Human) Putin() {
	fmt.Println("箸で食べる")
}

func (h Human) Chew() {
	fmt.Println("歯でしっかり噛む")
}

この二つの関数が実装された時点で
Human構造体はEaterインターフェースを実装されるとGoのコンパイラは判断をする。

ポイント
Interfaceはあくまでであり、振る舞いの定義をしているだけ。
→**「Interfaceを引数にとる関数を定義して使えるようにする」必要がある**

// インターフェースが引数になる、食べるメソッド
func EatAll(e Eater) {
	e.Putin()
	e.Chew()
}
Hina🐣Hina🐣

Interfaceを利用しない場合

package main

import "fmt"

type Human struct {
   Height int
}

type Dog struct {
   Kind string
}

// 人間用のメソッド
func (h Human) Putin() {
   fmt.Println("箸で食べる")
}

func (h Human) Chew() {
   fmt.Println("歯でしっかり噛む")
}

// 犬用のメソッド
func (d Dog) Putin() {
   fmt.Println("床からそのまま食べる")
}

func (d Dog) Chew() {
   fmt.Println("人間と同じように歯で食べる")
}

// インターフェースなしで、各種の処理を個別に書く
func EatHuman(h Human) {
   h.Putin()
   h.Chew()
}

func EatDog(d Dog) {
   d.Putin()
   d.Chew()
}

func main() {
   var man Human = Human{Height: 170}
   var hina Dog = Dog{Kind: "Cavaria"}

   fmt.Println("<人間が食べる>")
   EatHuman(man)

   fmt.Println("<犬が食べる>")
   EatDog(hina)
}

上記のようになる
変更箇所としては以下のようだ

// インターフェースなしで、各種の処理を個別に書く
func EatHuman(h Human) {
	h.Putin()
	h.Chew()
}

func EatDog(d Dog) {
	d.Putin()
	d.Chew()
}

Interfaceが定義されていないことによって、犬が食べる振る舞いと、人間が食べる振る舞いを別物として扱わなければいけない!!

→インターフェースを使うことで
「誰が食べるか」ではなく「どう食べるか」に着目して処理の共通化ができる

Hina🐣Hina🐣

JavaでのInterface実装

Eater.java
public interface Eater {
    void putIn(); // 食べ物を口に入れる
    void chew();  // 噛む
}
Human.java
public class Human implements Eater {
    @Override
    public void putIn() {
        System.out.println("箸で食べる");
    }

    @Override
    public void chew() {
        System.out.println("歯でしっかり噛む");
    }
}
Dog.java
public class Dog implements Eater {
    @Override
    public void putIn() {
        System.out.println("床からそのまま食べる");
    }

    @Override
    public void chew() {
        System.out.println("人間と同じように歯で食べる");
    }
}

EatingService.java
public class EatingService {
    public static void letEat(Eater e) {
        e.putIn();
        e.chew();
    }
}
main.java
public class Main {
    public static void main(String[] args) {
        Eater man = new Human();
        Eater dog = new Dog();

        System.out.println("<人間が食べる>");
        EatingService.letEat(man);

        System.out.println("<犬が食べる>");
        EatingService.letEat(dog);
    }
}
Hina🐣Hina🐣

Interface注意点

Interfaceで定義されている関数をその構造体(クラス)がすべて実装していても、実際にその関数を使う必要はない

Golangの場合

例えば

type Eater interface {
    PutIn()
    Chew()
    Swallow()
}

type Human struct{}

func (Human) PutIn()  { fmt.Println("箸で食べる") }
func (Human) Chew()   { fmt.Println("しっかり噛む") }
func (Human) Swallow(){ fmt.Println("飲み込む") }

func LetEat(e Eater) {
    e.PutIn()
}

疑問

「あれ? Chew() や Swallow() を使ってないのに、なぜそれも実装しないといけないの?」

✅ これは「設計の視点の違い」
構造体側:インターフェースを満たす= 契約を守る責任がある
よって、定義されたすべてのメソッドを持たなければならない。

関数側:インターフェースに含まれるメソッドのうち、必要なものだけを使えばよい。
→ 未使用なメソッドがあっても問題ではない。

Javaでも同様

public interface Eater {
    void putIn();
    void chew();
    void swallow();
}

public class Human implements Eater {
    @Override
    public void putIn() {
        System.out.println("箸で食べる");
    }

    @Override
    public void chew() {
        System.out.println("しっかり噛む");
    }

    @Override
    public void swallow() {
        System.out.println("飲み込む");
    }
}

public class EatingService {
    public static void letPutIn(Eater e) {
        e.putIn();  // chew() や swallow() は使ってない
    }
}
Hina🐣Hina🐣

ポリモーフィズムとは

同じインターフェース(共通の型)を使って、複数の異なる実装を同じように扱えること
→ある関数が異なる構造体を引数にとっても、同じように動く仕組み

type Eater interface {
	Putin() //口に入れる
}

type Human struct {}
type Dog struct {}

// 人間用のインターフェースを実装
func (h Human) Putin() {
	fmt.Println("箸で食べる")
}

// 犬用のインターフェースを実装
func (d Dog) Putin() {
	fmt.Println("床からそのまま食べる")
}

// インターフェースが引数になる、食べるメソッド
func EatAll(e Eater) {
	e.Putin() //←これがポリモーフィズム!!
}
func main(){
    EatAll(Human{})  //箸で食べる
    EatAll(Dog{})  //床からそのまま食べる
}
  • EatAll関数はPutInインターフェースしか知らない
  • HumanDogもPutInインターフェースを満たしている
  • 「違う型」でも「同じように」扱える=ポリモーフィズム