はじめに
Go
にはPreemptive interface Anti-Pattern in Go
という考え方があります。
私はこちらの記事を読み初めて知りました。
ただ、原文を読んだだけでは理解が難しかったので自分なりに手を動かして自分の言葉で整理してみたのが本記事の内容となります。
英語力に自信がなく、自動翻訳の力を借りています。また、間違っている可能性もあるため真の情報としては原文のご確認をお願いします。
(本記事の内容が間違っていたらご指摘のほどお願いします。)
TL;DR
-
Preemptive Interface
という実装パターンがあります。 -
Java
のようなinterface
を明示的に継承させる必要がある言語では有効となります。 -
Go
ではinterface
は暗黙的に継承されるのでそこまで有効ではありません。
Preemptive Interface
とは?
そもそもPreemptive Interface
とはどのような実装パターンなのでしょうか?
強引な訳かもしれませんが、日本語では「先走ったインターフェイス」と言えるでしょうか。
実際にinterface
が必要かどうか判明する前にあらかじめinterface
を実装するパターンとなります。
噛み砕いた表現をすると、「とりあえずinterface
用意しておいた方が無難だよね」という感じでしょうか。
以下のような実装がPreemptive Interface
となります。
type IUser interface {
Hello() error
}
type User struct {}
func NewUser() IUser {
return User{}
}
func (u User) Hello() error {
return nil
}
これがGo
においてはアンチパターンとなる場合があるのだということです。
どういうことなのでしょうか?
なぜとりあえずinterface
を用意しておきたくなるのか
実際にJava
とGo
のコードを実装してとりあえずinterfae
を用意しておきたくなる理由を紐解いてみます。
以下のような機能を有した実装がすでにあったと仮定します。
-
bool
を返すCanAction()
というAuth
型があります。 -
Auth
型を引数に持つTakeAction(a Auth)
というメソッドがあります。
このTakeAction(a Auth)
関数の引数をinterface
にして汎用性を高めたくなりました。
修正の目的としてはテストを実施する際にMockが使いたくなったとかになるでしょうか。
では実際の実装を用いてJava
とGo
を比較していきます。
なるべく比較しやすいようにJava
とGo
が似たような実装になるようにしています。
原文とは異なる実装を用いていること、ご了承ください。
Java
まずはJava
から見ていきます。
Sample実装(修正前)
Auth.java
package auth;
public class Auth {
public boolean canAction() {
return true;
}
}
Logic.java
package logic;
import auth.Auth;
public class Logic {
public void takeAction(Auth auth) {
if (auth.canAction()) {
System.out.println("take action");
}
}
}
Main.java
import auth.Auth;
import logic.Logic;
public class Main {
public static void main(String[] args) {
final var auth = new Auth();
final var logic = new Logic();
logic.takeAction(auth);
}
}
Sample実装(修正後)
Auth.java
package auth;
import iauth.IAuth;
public class Auth implements IAuth {
public boolean canAction() {
return true;
}
}
IAuth.java
package iauth;
public interface IAuth {
public boolean canAction();
}
Logic.java
package logic;
import iauth.IAuth;
public class Logic {
public void takeAction(IAuth auth) {
if (auth.canAction()) {
System.out.println("take action");
}
}
}
Main.java
import auth.Auth;
import logic.Logic;
public class Main {
public static void main(String[] args) {
final var auth = new Auth();
final var logic = new Logic();
logic.takeAction(auth);
}
}
変更点
-
IAuth
を追加 -
takeAction
の引数をAuth
からIAuth
に変更 -
Auth
にimplement Auth
を追加
Go
続いてGo
の実装を見ていきます。
Sample実装(修正前)
auth.go
package auth
type Auth struct{}
func NewAuth() Auth {
return Auth{}
}
func (a Auth) CanAction() bool {
return true
}
logic.go
package logic
import (
"log"
"preemptive-interface/auth"
)
type Logic struct{}
func NewLogic() Logic {
return Logic{}
}
func (l Logic) TakeAction(auth auth.Auth) {
if auth.CanAction() {
log.Println("take action")
}
}
main.go
package main
import (
"preemptive-interface/auth"
"preemptive-interface/logic"
)
func main() {
a := auth.NewAuth()
l := logic.NewLogic()
l.TakeAction(a)
}
Sample実装(修正後)
auth.go
package auth
type Auth struct{}
func NewAuth() Auth {
return Auth{}
}
func (a Auth) CanAction() bool {
return true
}
iauth.go
package iauth
type IAuth interface {
CanAction() bool
}
logic.go
package logic
import (
"log"
"preemptive-interface/iauth"
)
type Logic struct{}
func NewLogic() Logic {
return Logic{}
}
func (l Logic) TakeAction(auth iauth.IAuth) {
if auth.CanAction() {
log.Println("take action")
}
}
main.go
package main
import (
"preemptive-interface/auth"
"preemptive-interface/logic"
)
func main() {
a := auth.NewAuth()
l := logic.NewLogic()
l.TakeAction(a)
}
変更点
-
IAuth
を追加 -
takeAction
の引数をAuth
からIAuth
に変更
Java
とGo
を比較
一見すると言語仕様に応じてJava
ではimplements
を用いて明示的に継承しているが、Go
では暗黙的に継承(ダックタイピング)であるかくらいの違いしかありません。
これだけだとそこまで修正に対してコストが変わらないように感じます。
しかし、ある状況下だとJava
ではこの修正が簡単にはいかないことがあります。
私(あなた) がLogic
の実装者でAuth
を 別の方 が実装しているとします。(私(あなた)が簡単にAuth
を修正できない状況です。)
このときLogic
を上記で実装したように修正した場合、Auth
実装者に「implements
を用いて継承するようにしてください」と連絡/同意をする必要があります。
でないとビルドが通らなくなります。(Main.java
でビルドエラーが発生し破壊的変更となります。)
このような事態がJava
では発生するため、あらかじめinterface
を用意しておくPreemptive Interface
が有効と言えます。
一方、Go
の場合はというと暗黙的な継承(ダックタイピング)となるためLogic
実装者が一方的に上記のような修正を行なうことができます。(main.go
のビルドは落ちず、後方互換性を保つことができ破壊的変更にはなりません。)
そのため、Go
ではPreemptive Interface
はアンチパターンですよ。ということになるのですね。
≒ そんなことをしておかなくてもあとで簡単に変更ができます。不要なものはなるべく作らない。YAGNI原則ですね。
おわりに
原文の終盤ではPreemptive Interface
を用いるとメソッド数が増加する傾向にあると言及されています。
クリーンアーキテクチャにおけるUsecase
とRepository
の関係性などを思い浮かべるとよいでしょうか。
一般的にinterface
はシンプルである(メソッドが少ない)方が有利な点が多いかと思います。
シンプルになったりDIがしやすくなったりといったメリットが挙げられるでしょうか。
Go
の標準パッケージでもinterface
のメソッドは1つないし2つが一般的なコードとなっています。
こちらはGo
でのAccept Interfaces, return structs
という考え方を整理することでもう少し理解しやすくなる内容かと思います。
(Accept Interfaces, return structs
は日本語での記事も多数あるのでそちらを参照ください。機会があれば私も整理するつもりです。)
実際にJava
とGo
を比較しPreemptive Interface
を確認したことでGo
らしさを確認できたと思います。
一方でプロダクトコード開発において今回のようにstruct
が引数に与えられていてそこから修正するというシチュエーションに遭遇するのか?という疑問も持ちました。
比較的Mockを利用したテストを想定してコーディングするために最初からPreemptive Interface
で実装することも多いかと思います。
Go
らしさとプロダクトコード開発のバランスについてはまだまだ理解できていないので今後もいろいろと整理していきたいです。