【Golang】interface是什么来着?
首先
我阅读了在Zenn上发布的一篇名为《我写的Go代码错了》的文章。
从这篇文章中我了解到,Go编程中一种流行的方式是将参数以interface的形式接收,而返回值则使用具体的类型(例如结构体)来返回。
趁着这个机会,我决定思考一下”接口是什么来着?”。
受到原文的启发,我开始撰写了这篇关于接口的思考性文章。
interface是什么意思?
-
- Goに限らず、(もっと言えばプログラミングに限らず)interfaceとは抽象化の手段の一つだと思っています。もうちょっと簡単に言えば、何かをふわっと捉えたい時に使うものです。1
例えば「洗濯物を干す」という日本語を考えてみましょう。
「洗濯物を干す」という日本語を使う時、干しているものが下着なのかシャツなのか上着なのかはどうでも良いことです。大事なことは、洗濯が完了していて濡れているので外に干す必要があるということです。
つまり「洗濯物」とは下着やシャツや上着を「洗濯が完了して濡れているので外に干す必要があるもの」とふわっと捉えるための言葉であり、これは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がない状態を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の発想で作ってるということです。今、ShirtとJacketがともに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は何かをふわっと捉えるためのものです。NewHumanRepositoryはhumanRepositoryを返却するためのメソッドであり、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を勝手に満たします。なので上記コードで行くと、isDriedClothesにLaundry型の値を渡すことが出来てしまいます。エディタでnsをホバーすると、Laundry型と出てきます。「ん?Laundry型なのに?」と可読性を落としそうです。あくまでShirt型としておいて、nsをホバーしたらShirt型と出てきてほしいわけです。「コンパイルエラー出ないってことはShirtはLaundryinterfaceを満たすんだなって分かります。
なので結論として戻り値として用いるのはおかしい!・・・、と言いたかったのですが、interfaceで返却するのが必ずしも悪いとも言い切れませんでした。というのもinterfaceで返して致命的にダメな理由もない気がしてきたのです。例えば今回のNewHumanRepositoryだと構造体の型で返してもinterfaceで返しても特に何も変わりません。
シャツを洗濯物とも洋服とも捉えられるのがinterfaceの強みですが、特にDDDで言えば、IHumanRepositoryはHumanRepositoryのためのinterfaceで、一対一の関係にあります。3ユースケース層とドメイン層から具体的なSQLの記述をインフラ層に切り離すためのinterfaceです。
// これ何がマズいんだろう?
func NewHumanRepository()IHumanRepository {
return HumanRepository{}
}
func NewHumanRepository()HumanRepository {
return HumanRepository{}
}
所以最终的结论是
接口是什么来着…?
如果你有这样的疑问,请继续阅读本文!!!
这个无关紧要的小故事真的很糟糕。
- 前述のとおり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
}
文献引用
-
- 『オレの書くGoは間違っていた』
上記記事に関するツイート
mattnさんによる上記記事へのコメントです。非常に参考になります。
也有传递模拟仓库到用例中进行单元测试的角色,所以它并不完全是一对一的。
出于想要表达这一点的一心,本文得以写成。