はじめに
「オブジェクト指向のインターフェースは知っているが,重要性や具体的な使い所がわからない」という方に向け,私なりにインターフェースの重要性について説明します。
インターフェースの意義
インターフェースはコードの可読性を高め、将来のメソッド実装を保証することで、変更しやすいコードベースの作成に貢献します.
以下2点の具体的なメリットについて解説していきます.
- 条件分岐(if文, switch文)の削減による可読性の向上
- 将来のメソッド実装を保証
if文やswitch文の削減による可読性の向上
コードが増える場合,if文やswitch文の削減が重要になってきます.
ソフトウェアは,様々な場所で状態や設定に応じた処理の分岐が発生します.
しかし,コードは分岐が増えるほど,複雑で理解しづらくなります.
そこで,インターフェースを使えば,if文やswitch文を書かずに処理を分岐できます.
インターフェース型の変数に,異なるクラスのインスタンスを代入することで,異なる処理を呼び出すことができます.
例として,ポケモンの「わざ」を発動する処理について考えてみましょう.
今回実装する「わざ」は次の3つです.
- ひのこ
- タイプ: ほのおタイプ
- 威力:40
- 追加効果: 10%の確率でやけど状態にする
- あわ
- タイプ: みずタイプ
- 威力:40
- 追加効果: なし
- すいとる
- タイプ: くさタイプ
- 威力:20
- 追加効果: 与えたダメージ分HPを回復する
今回の主題は「追加効果の処理をどう分岐するか」です.
まずはSwitch文で追加効果について分岐して,「わざ」を取り扱うクラス(Move
)クラスを実装してみます.
以下の折りたたみブロックに実装例を示します.
Switch文で実装した例
package main
import (
"fmt"
"math/rand"
"time"
)
type Move struct {
Name string
Type string
Power int
Additional string
}
func (m Move) execute() {
fmt.Printf("%s (%s) with power %d is activated.\n", m.Name, m.Type, m.Power)
switch m.Additional {
case "burn":
// 乱数生成
rand.Seed(time.Now().UnixNano())
if rand.Intn(100) < 10 { // 10% の確率
fmt.Println("The target is burned!")
} else {
fmt.Println("No additional effect.")
}
case "drain":
healedAmount := m.Power // 単純化のため,攻撃力と回復量を一緒にしている
fmt.Printf("The user recovers %d HP!\n", healedAmount)
case "none":
fmt.Println("No additional effect.")
default:
fmt.Println("Unknown additional effect.")
}
}
func main() {
moves := []Move{
{"ひのこ", "ほのおタイプ", 40, "burn"},
{"あわ", "みずタイプ", 40, "none"},
{"すいとる", "くさタイプ", 20, "drain"},
}
for _, move := range moves {
fmt.Println("Using move:", move.Name)
move.execute()
}
}
このコードは動作しますが,executeメソッドにswitch文の分岐があり,「ひのこ」の処理について知りたい人の目にも,「あわ」「すいとる」の処理も入ってきます.
「わざ」の数が増えるにつれて目に入る処理の数は増えていくでしょう.
こういった場合,インターフェースを使って可読性を向上できます.
では,追加効果インターフェース(EffectApplier
)とそれを実装したBurn
クラス,Drain
クラス,None
クラスを実装してみましょう.
以下の折りたたみブロックにインターフェースを使用した実装例を示します.
インターフェースで実装した例
package main
import (
"fmt"
"math/rand"
"time"
)
// 追加効果インターフェース
type EffectApplier interface {
Activate()
}
// やけど
type Burn struct{}
func (b Burn) Activate() {
rand.Seed(time.Now().UnixNano())
if rand.Intn(100) < 10 { // 10%でやけど状態にする
fmt.Println("The target is burned!")
} else {
fmt.Println("No additional effect.")
}
}
// HP吸収
type Drain struct {
Amount int
}
func (d Drain) Activate() {
fmt.Printf("The user recovers %d HP!\n", d.Amount)
}
// 何もしない
type None struct{}
func (n None) Activate() {
fmt.Println("No additional effect.")
}
// 「わざ」クラス
type Move struct {
Name string
Type string
Power int
Effect EffectApplier
}
// 「わざ」の発動
func (m Move) execute() {
fmt.Printf("%s (%s) with power %d is activated.\n", m.Name, m.Type, m.Power)
m.Effect.Activate()
}
func main() {
moves := []Move{
{"ひのこ", "ほのおタイプ", 40, Burn{}},
{"あわ", "みずタイプ", 40, None{}},
{"すいとる", "くさタイプ", 20, Drain{Amount: 20}},
}
for _, move := range moves {
fmt.Println("Using move:", move.Name)
move.execute()
}
}
インターフェースを使用することで,分岐が減って,execute
メソッドの見通しがよくなりました.
記事の都合上,全てのクラス,インターフェースを一箇所に記述しているためコード量は増えています.
しかし,各クラスを別ファイルに分ければ,一つの「わざ」の処理を理解するために読むコード量は減ります.
インターフェースの使用はコードの単純化と可読性を向上させ,ソフトウェアの変更を容易にし,作業効率の向上とコスト削減に寄与します.
将来のメソッド実装を保証
インターフェースを使うことで,将来のメソッド実装を保証できます.
ポケモンの「わざ」に「10まんボルト」が追加された場合について考えてみましょう.
10まんボルトの仕様は以下のようなものです.
- 10まんボルト
- タイプ: でんきタイプ
- 威力:90
- 追加効果: 10%の確率で相手をまひ状態にする
実装担当者が追加効果の実装を忘れた場合に,インターフェースを使用した実装はエラーを発生させることができます.
インターフェースを使用した実装では,EffectApplier
の実装(Activate
メソッドの実装)を忘れるとコンパイルエラーが発生し,コンパイル時に実装ミスが検出されます.
一方,switch文を使用すると,default処理が実行されてエラーが表示されないため,実装ミスが検出されにくいです.
このように,インターフェースはバグを予防する効果があり,ビジネスのリスクを削減できます.
さらなる勉強におすすめな書籍
インターフェースの重要性についてより詳しくなるために,おすすめの書籍を最後に紹介します.
この記事もこちらの書籍の受け売りな部分があるので,この記事に興味を持っていただけた方なら,楽しく学べるでしょう.
良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方
私の記事よりもifやswitchで分岐する実装の悪い点について丁寧に説明しており,インターフェースの重要性がより詳しく記載されています.
さらに,ここで述べた以外にも変更が容易なコードベースに貢献するためのスキルにも触れられています.
設計の本は理解が難しいものが多い中で,初学者にもおすすめの一冊です.