LoginSignup
18
10

More than 1 year has passed since last update.

【Golang】interfaceってなんだっけ?

Last updated at Posted at 2022-09-08

はじめに

Zennに投稿されていた『オレの書くGoは間違っていた』という記事を読みました。
この記事で得た知見は「引数はinterfaceで受け入れて、戻り値は具体的な型(例えばstruct)で返却する」というのがGo流という話です。
せっかくの機会なので、「interfaceってなんだっけ?」と考えてみることにしました。
元記事に触発されて書き始めたものの、interfaceについてほんわか考える記事です。

そもそもinterfaceってなに?

  • Goに限らず、(もっと言えばプログラミングに限らず)interfaceとは抽象化の手段の一つだと思っています。もうちょっと簡単に言えば、何かをふわっと捉えたい時に使うものです。1
  • 例えば「洗濯物を干す」という日本語を考えてみましょう。

sentakumono_niwa.png

  • 「洗濯物を干す」という日本語を使う時、干しているものが下着なのかシャツなのか上着なのかはどうでも良いことです。大事なことは、洗濯が完了していて濡れているので外に干す必要があるということです。

  • つまり「洗濯物」とは下着やシャツや上着を「洗濯が完了して濡れているので外に干す必要があるもの」とふわっと捉えるための言葉であり、これはinterfaceと言えそうです。

  • ここまでをGoのコードで確認してみましょう。

type Laundry interface {
    beDried(time int)
    getMoisture()int
}

type Shirt struct {
    // 水気という意味で使ってみた
    moisture int 
}

// 乾かすっていう意味での命名です
// 受動態を表したかったのですが、isDriedにするとbool返すように見えるので原形のbeにしてみました。
func(s *Shirt)beDried(time int) {
    // 100は適当。干せば干すほど乾く
    s.moisture -= time * 100
}

func(s Shirt)getMoisture()int {
  return s.moisture
}

type Jacket struct {
    moisture int 
}

func(j *Jacket)beDried(time int) {
    j.moisture -= time * 100
}

func(j Jacket)getMoisture()int {
  return j.moisture
}


// 乾いているかを判定するメソッドです。
// 引数がinterfaceの型になっているので、「洗濯物」であれば受け入れができる
func isDried(l Laundry)bool {
  return l.getMoisture() > 0
}

ではinterfaceがないとどうなる?

  • 先ほどの例でいえば「洗濯物」という言葉が使えないわけなので会話に非常に苦労します。
// interfaceがないために会話が成り立たない
わたし「あ~、雨降ってるやん~、パンツと靴下とシャツと上着干してるのに~」
だれか「へー、じゃあタオルは干してないんだね!」

// interfaceがあるので会話が成り立つ
わたし「あ~、雨降ってるやん~、洗濯物干してるのに~」
だれか「あるあるだよね〜」
  • このinterfaceがない状態をGoで表現してみましょう。2
type Shirt struct {
    moisture int 
}

func(s *Shirt)beDried(time int) {
    s.moisture -= time * 100
}

func(s Shirt)getMoisture()int {
  return s.moisture
}

type Jacket struct {
    moisture int 
}

func(j *Jacket)beDried(time int) {
    j.moisture -= time * 100
}

func(j Jacket)getMoisture()int {
  return j.moisture
}

// isDriedを構造体の数だけ書かないといけない・・・。
func isDriedShirt(s Shirt)bool {
  return s.moisture() > 0
}

func isDriedJacket(j Jacket)bool {
  return j.moisture() > 0
}
  • ここでコードだけ見てisDriedの引数をmoisutre渡すようにしたらええやんと思われた方もいるかも知れません。
type Shirt struct {
    moisture int 
}

func(s *Shirt)beDried(time int) {
    s.moisture -= time * 100
}

func(s Shirt)getMoisture()int {
  return s.moisture
}

type Jacket struct {
    moisture int 
}

func(j *Jacket)beDried(time int) {
    j.moisture -= time * 100
}

func(j Jacket)getMoisture()int {
  return j.moisture
}

// 複数書かなくて済んだ!
func isDried(moisture int)bool {
  return moisture > 0
}
  • これは一見問題ないように思えますが、色々問題点があります。
  • まず一点目は引数の名前こそmoistureですが、型はただのintです。つまりint型であれば何でも渡せてしまうわけで、型の保護を受けることが出来ていません。
  • 二点目はこれくらいの小規模なコードなら良いですが、コード量が膨らんでくると、isDriedメソッドがbeDriedメソッドを持っている構造体向けのものであるという意図が見えなくなってきます。「引数の名前はmoisture・・・?しかも型はただのint?何これ?」となるのが目に見えてますね。
  • 三点目は型の保護こそないが、interfaceの発想で作ってるということです。今、ShirtJacketがともにmoistureを持っていることに着目したが故に出てきたわけです。これは問題点というより、無意識の内にinterfaceを使ってしまっているということですね。

では本題に戻って「引数はinterfaceで受け入れて、戻り値は具体的な型(例えばstruct)で返却する」ってどういうことか

  • interfaceで受け入れるメリットはもう十分理解できたかと思います。問題は戻り値をinterfaceにするべきか具体的な型にするべきかということです。いきなり抽象度上がりますが、次のDDDを意識したコードを見てください。一緒くたに並べてますが、然るべき層に記述されていると考えてください。
// ドメイン層に定義してあるものとします。
type IHumanRepository interface {
    Create()
}

// インフラ層に記述してあるとする。
type HumanRepository struct {
}

func(hr HumanRepository)Create() {
    // 何かしらのCRUD処理
}

func NewHumanRepository()IHumanRepository {
    return HumanRepository{}
}

// ユースケース層(サービス層とかアプリケーション層とも言います)
type HumanUseCase struct {
    humanRepository IHumanRepository
}

func NewHumanUseCase(humanRepository IHumanRepository)HumanUseCase {
    return HumanUseCase{humanRepository: humanRepository}
}

// インターフェース層(プレゼンテーション層とも言います)
// 依存性注入(DI)をしています。
var humanRepository = NewHumanRepository()
var humanUseCase = NewHumanUseCase(humanRepository)
  • さらっとコードの説明をしますと、ドメイン層にリポジトリのinterfaceが用意されていて、ユースケース層ではそのinterfaceの型であるhumanRepositoryを受け取っています。これは良いことで、ふわっと引数のリポジトリを捉えることによって、例えばユースケース層のユニットテストを書きたい時、mock用のhumanRepositoryを受け取ったりできます。

  • 今回取り上げたいのは、NewHumanRepositoryの戻り値をIHumanRepositoryにするのはおかしくないかというところです。何故おかしいかと言えば、前述の通りinterfaceは何かをふわっと捉えるためのものです。NewHumanRepositoryhumanRepositoryを返却するためのメソッドであり、Createメソッドを持っている(=IHumanRepository型)構造体を返却するためのメソッドではないわけです。

  • 洗濯物の例えで行けば、シャツのファクトリメソッドがLaundry型で返ってきているのと同じことです。あくまでシャツは洗濯物として捉えることが出来るだけであって、洗濯物とイコールではありません。洋服と捉えることも出来ますし、繊維質の物体と捉えることだって出来ます。なので意味だけで考えれば戻り値をinterfaceにするべきでないように思えます。

package main

type Laundry interface {
	beDried(time int)
	getMoisture() int
}

type Clothes interface {
	beDried(time int)
	getMoisture() int
}

type Shirt struct {
	// 水気という意味で使ってみた
	moisture int
}

func (s *Shirt) beDried(time int) {
	s.moisture -= time * 100
}

func (s Shirt) getMoisture() int {
	return s.moisture
}

// シャツのファクトリメソッドの戻り値がinterfaceの型になっています
// シャツはLaundry型とするべきでしょうか?
func NewShirt() Laundry {
	// 新品のシャツが濡れていないと思うので、水気は0で初期化してみました
	return &Shirt{moisture: 0}
}

func isDriedLaundry(l Laundry) bool {
	return l.getMoisture() > 0
}

func isDriedClothes(c Clothes) bool {
	return c.getMoisture() > 0
}

func main() {
  ns := NewShirt()
  isDried := isDriedClothes(ns)
  // hogehoge...
}
  • ただGoは定義してあるメソッドを持っていればinterfaceを勝手に満たします。なので上記コードで行くと、isDriedClothesLaundry型の値を渡すことが出来てしまいます。エディタでnsをホバーすると、Laundry型と出てきます。「ん?Laundry型なのに?」と可読性を落としそうです。あくまでShirt型としておいて、nsをホバーしたらShirt型と出てきてほしいわけです。「コンパイルエラー出ないってことはShirtLaundryinterfaceを満たすんだなって分かります。

  • なので結論として戻り値として用いるのはおかしい!・・・、と言いたかったのですが、interfaceで返却するのが必ずしも悪いとも言い切れませんでした。というのもinterfaceで返して致命的にダメな理由もない気がしてきたのです。例えば今回のNewHumanRepositoryだと構造体の型で返してもinterfaceで返しても特に何も変わりません。

  • シャツを洗濯物とも洋服とも捉えられるのがinterfaceの強みですが、特にDDDで言えば、IHumanRepositoryHumanRepositoryのためのinterfaceで、一対一の関係にあります。3ユースケース層とドメイン層から具体的なSQLの記述をインフラ層に切り離すためのinterfaceです。

// これ何がマズいんだろう?
func NewHumanRepository()IHumanRepository {
    return HumanRepository{}
}

func NewHumanRepository()HumanRepository {
    return HumanRepository{}
}

ということで結論

interfaceってなんだっけ・・・?
そう思ったあなたはまたこの記事を上から読んでね!!!

無茶苦茶どうでもいい小話

  • 前述のとおりGoはinterfaceを勝手に満たしてしまうので、Laundryをイカ構造体が満たすのではという話でちょっと社内で盛り上がりました。4
type  Laundry interface {
    beDried(time int)
}

// Laundry-interfaceを満たしてしまっている
// Squidってイカってことです。
type Squid struct {
    // 旨味という意味で使ってみた
    flavorComponent int
}

// スルメになるメソッド
func(s *Squid)beDried(time int) {
    // 100はめっちゃ適当。干せば干すほど旨味が増す
    s.flavorComponent += time * 100
}

// Laundry-interfaceを満たしている。想定どおり。
type Shirt struct {
    // 水気という意味で使ってみた
    moisture int 
}

func(s *Shirt)beDried(time int) {
    // 相変わらず100は適当。干せば干すほど乾く
    s.moisture -= time * 100
}

参考文献

  1. もう少し一般的にinterfaceを捉えると「ふわっと捉える」というよりも「約束事」の方が近いかも知れません。(APIのinterfaceとかね)

  2. ぶっちゃけ構造体そのものを受け入れることでgetMoistureメソッド無くせるので、逆にメリット出てる気もしてしまいました。もうちょい良い例ありそう・・・。

  3. ユニットテスト用のmockのリポジトリをユースケースに渡すという役割もあるので完全に一対一ではないですけどね。

  4. これを書きたい一心で本記事は執筆されました。

18
10
0

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
18
10