はじめに
Zennに投稿されていた『オレの書くGoは間違っていた』という記事を読みました。
この記事で得た知見は「引数はinterfaceで受け入れて、戻り値は具体的な型(例えばstruct)で返却する」というのがGo流という話です。
せっかくの機会なので、「interfaceってなんだっけ?」と考えてみることにしました。
元記事に触発されて書き始めたものの、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があるので会話が成り立つ
わたし「あ~、雨降ってるやん~、洗濯物干してるのに~」
だれか「あるあるだよね〜」
- この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
はLaundry
interfaceを満たすんだなって分かります。 -
なので結論として戻り値として用いるのはおかしい!・・・、と言いたかったのですが、interfaceで返却するのが必ずしも悪いとも言い切れませんでした。というのもinterfaceで返して致命的にダメな理由もない気がしてきたのです。例えば今回の
NewHumanRepository
だと構造体の型で返してもinterfaceで返しても特に何も変わりません。 -
シャツを洗濯物とも洋服とも捉えられるのがinterfaceの強みですが、特にDDDで言えば、
IHumanRepository
はHumanRepository
のための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
}
参考文献
- 『オレの書くGoは間違っていた』
-
上記記事に関するツイート
- mattnさんによる上記記事へのコメントです。非常に参考になります。