1. インターフェースを使うメリット
現状、自身が認識しているものは以下の通り。
- 利用する側が必要なメソッドを明示化できる
- 利用する側、される側の依存関係を逆転できる
- 依存関係逆転の概念は、クリーンアーキテクチャ等で登場するもの。
- 個人的なイメージは、利用する側の立場でインターフェースを定義することで、利用される側へ用意してほしいメソッドの名前とシグネチャを実装上満たすべき制約として押し付ける、という形。
- クラス内の多くのメソッドで登場する同じような条件分岐がある場合、別クラス化することでキレイに書ける(クラスの根本から条件分岐するイメージ。ミノ駆動本で知った)
- テストコードでモックを利用できるようになる(特定の状況を作り出すのが難しい場合や、エラーハンドリングのテストとかやりやすくなる)
2. サンプルコード
以下の状況におけるサンプルコード(1. の例)
- あるAPIのレスポンスを生成する際に、ユーザーの名前と住所が必要
- ユーザー情報を扱うモデルUser構造体がある
- User構造体ではID, 名前, 住所の取得メソッド、名前と住所を更新するメソッドが公開メソッドとして用意されている
= APIのレスポンスとしては、IDの取得メソッドや更新メソッドが不要である
// IUserForResponse レスポンスのためのユーザー情報 インターフェース
interface IUserForResponse {
// Name 名前を取得
Name() string
// Address 住所を取得
Address() string
}
var _ IUserForResponse := &User{}
// Response レスポンス
type Response struct {
Name string
Address string
}
// CreateResponse Responseのファクトリ
func CreateResponse(userForRes IUserForResponse) *Response {
res := &Response{
Name: userForRes.Name()
Address: userForRes.Address()
}
return res
}
// User ユーザー情報
type User struct {
// ID
id string
// 名前
name string
// 住所
address string
}
// NewUser コンストラクタ
func NewUser(id string, name string, address string) (*User, error) {
user := &User{
id: id,
name: name,
address: address,
}
if err := user.validate(); err != nil {
// エラーハンドリング
}
return user, nil
}
// ID IDを取得
func (u *user) ID() string {
return u.ID
}
// Name 名前を取得
func (u *User) Name() string {
return u.name
}
// Address 住所を取得
func (u *User) Address() string {
return u.address
}
// Update 名前と住所を更新
func (u *User) Update(name string, address string) (*User, error) {
updatedUser, err := NewUser(u.ID(), name, address)
if err != nil {
// エラーハンドリング
}
return updatedUser, nil
}
// validate ユーザー情報のバリデーション
func (u *User) validate() error {
// バリデーションロジック
}
3. Goのインターフェースの詳細
3.1. インターフェースの実装は暗黙的である
インターフェースで定義されているメソッドが実装されている構造体が、インターフェースが満たされているとみなされる。
※Javaだとクラスに対してインターフェースを明示的に実装する必要があり、実装対象のクラスに「implements」というキーワードを記述しておく必要がある
サンプルコードだと、
Name() string
, Address() string
の実装を要求するIUserForResponse
インターフェースに対して、
Name() string
, Address()
含むメソッドが実装されているUser構造体は、IUserForResponse
インターフェースの実装を満たすことになる。
3.2. インターフェースにフィールドを定義できない
※Javaだと定数メンバを定義できる
3.3. interface{}
(空インターフェース.any
型)
この特別なインターフェース型の変数に、あらゆる型の値を格納できる
3.4. 型アサーションと型スイッチ
インターフェースから元の構造体の型へ変換して、その元の型独自の処理をさせたいときは、以下のように型アサーション or 型スイッチを使う。
型アサーションは通常のインターフェース型に対して使い、型スイッチはinterface{}型に対して使う らしい。
型アサーション
インターフェース型の変数に格納されている値を、元の構造体の型へ変換するときに使う。
var, ok := <インターフェースの変数>.(<キャスト対象の型>)
if !ok {
// 型変換失敗時の処理
}
型スイッチ
インターフェース型の変数に格納されている値の型を調べて、型ごとの処理を実行するために使う。
// 型を判断
switch <インターフェースの変数>.(type) {
case <型A>:
// 型Aである場合の処理
case <型B>:
// 型Bである場合の処理
...
default:
// 上で想定していない型である場合の処理
}
メモ:
interface{}
型に対して型スイッチで型判断するのは使うのはまぁそうだよね、とわかる。でも・・・普通のインターフェース型で元の型を判別しないといけないケースは、そもそもインターフェースのメリットぶち壊しじゃね?感があって、いまいちどういうケースで必要なのかよくわからない・・・今後、必要なケースに直面したらまとめることにする。
4. その他
4.1. 構造体がインターフェースを満たしているかをチェックする
Goではインターフェースの実装は暗黙的であるが、ブランク(_)を使った変数宣言を記述しておくことで、その構造体が特定のインターフェースを満たしていない場合にコンパイルエラーを吐かせることができ、これによって インターフェースの実装を想定している構造体を明示化 することができる。~~
var _ <インターフェース名> = &<インターフェース実装対象の構造体名>{}
4.2. インターフェースを満たす構造体のコンストラクタの返り値型
これがマジでググってもしっくりくる理由がわからなかったので、自分自身で出した結論ではあるが、 「インターフェース型を返すべき」 だと思う(現職のプロジェクトのコードでそうなってて、それが慣習的に正しいと仮定して考えて気づいた)。
というのも、まず前提として、 Goでの変数の初期化については、以下の慣習がある (書籍: 実用Go言語にも書いてある)。
- 型を明示化しないといけない場面: var <変数名> 型 = <格納値>
- 明示化する必要がない場面(短縮記法を使う): <変数名> := <格納値>
で、もうひとつの前提としてあるのが 「インターフェースを満たした構造体を呼び出し元で扱うときは、一旦インターフェース型の変数に入れる」 ということ。
この2つをあわせて考えると、インターフェースを満たす構造体のコンストラクタで返り値を構造体にしちゃうと、インターフェース型の変数の初期化時にvar <変数名> <インターフェース型> = <構造体の値>
って書かないといけなくなっちゃう。
現状の認識はむしろ逆。
構造体は、複数のインターフェースの実装である可能性があるため、コンストラクタで返すべき型は、構造体型。
そもそも利用する側でインターフェース型の変数を用意して代入して使うというケースに直面したことがない。利用する側のメソッド・関数の引数をインターフェース型にしておいて、実際にメソッド・関数にわたす値は構造体型で、という形。
参考
- 書籍: 実用Go言語