インターフェース分離の原則を考える
前置き
クリーンアーキテクチャに則った個人開発のサービスを運用しているmaruです。
今回はインターフェース分離の原則について書いていこうと思います。
こちらの記事のテスト可能の追記は1週間くらい待ってください。🙇
インターフェース分離の原則とは
SOLIDの原則の"I"に当たる部分で、原題は Interface Segregation Principle となります。
インターフェースの利用者にとって不要なメソッドへの依存を強制してはいけないという原則です。
つまり簡単に言ってしまえば 不要なメソッドが存在する状況を作るな!! 😡 とおっしゃるわけです。
DRYとの共存
少し話が脱線するように思われますが、インターフェース分離を妨げる要因の一つにDRY原則への陶酔があるのでお話します。
実際私もそれに陥りかけ、師匠に指摘されて気づきました。
DRY原則とは
Don't Repeat Yourself
同じ意味や機能を持つ情報を複数の場所に重複して置くことをなるべく避けるべきとする考え方。
共存
ボブおじさんは書籍においてDRYに決して否定的ではありません。しかし、行き過ぎたDRY偏重主義が私たちを苦しめると説いています。
彼はクリーンアーキテクチャの書籍において重複(DRY)についてこのように述べています。
皆重複を排除したがるけれど、その重複は 本物の重複 ですか?
本物の重複 : あるインスタンスに変更があればそのインスタンスのすべての複製にも同じ変更を反映しなければならない。
偽物の重複 : 明らかに重複していたコードが時間とともに異なる真価をとげて、数年後には全く違うものになっている。
つまり、安易に「この処理とこの処理は似ているから共通化してしまおう!!」ということをするな!!とおっしゃっています。
実際のコードで考える
極々簡単なコードを用いて説明していきます。
悪い例
ダメな例です。まずは全文。
package main
import "fmt"
// creatureAction基底インターフェース
type creatureAction interface {
Breathe() // 呼吸
Photosynthesis() // 光合成
Eat() // 食う
Run() // 走る
Speak() // しゃべる
Swim() // 泳ぐ
}
type creature struct {
spiecies string
creatureAction // creatureActionを満たす
}
func (c *creature) Breathe() {
fmt.Printf("%s: 酸素うまし!!\n", c.spiecies)
}
func (c *creature) Photosynthesis() {
fmt.Printf("%s: 二酸化炭素もうまし!!\n", c.spiecies)
}
func (c *creature) Eat() {
fmt.Printf("%s: メシウマ!!\n", c.spiecies)
}
func (c *creature) Run() {
fmt.Printf("%s: 止まるんじゃねえぞ!!\n", c.spiecies)
}
func (c *creature) Speak() {
fmt.Printf("%s: ああ、それってハネクリボー?\n", c.spiecies)
}
func (c *creature) Swim() {
fmt.Printf("%s: なついあつは泳ぐに限る!!\n", c.spiecies)
}
func main() {
human := &creature{spiecies: "ヒト"}
fmt.Printf("==== %s action ===\n", human.spiecies)
human.Breathe()
human.Eat()
human.Run()
human.Speak()
human.Swim()
fmt.Printf("==== %s action over ===\n\n\n", human.spiecies)
plant := &creature{spiecies: "植物"}
fmt.Printf("==== %s action ===\n", plant.spiecies)
plant.Breathe()
plant.Photosynthesis()
fmt.Printf("==== %s action over ===\n\n\n", plant.spiecies)
fish := &creature{spiecies: "魚"}
fmt.Printf("==== %s action ===\n", fish.spiecies)
fish.Breathe()
fish.Eat()
fish.Swim()
fmt.Printf("==== %s action over ===\n\n\n", fish.spiecies)
}
==== ヒト action ===
ヒト: 酸素うまし!!
ヒト: メシウマ!!
ヒト: 止まるんじゃねえぞ!!
ヒト: ああ、それってハネクリボー?
ヒト: なついあつは泳ぐに限る!!
==== ヒト action over ===
==== 植物 action ===
植物: 酸素うまし!!
植物: 二酸化炭素もうまし!!
==== 植物 action over ===
==== 魚 action ===
魚: 酸素うまし!!
魚: メシウマ!!
魚: なついあつは泳ぐに限る!!
==== 魚 action over ===
ここではcreatureAction基底インターフェースを以下のようにしています。
どうでしょうか?このようにすれば全てのcreatureの行動をこの一つのインターフェースで済ませられます。 コードの量少ない!!DRY最高!!
// creatureAction基底インターフェース
type creatureAction interface {
Breathe() // 呼吸
Photosynthesis() // 光合成
Eat() // 食う
Run() // 走る
Speak() // しゃべる
Swim() // 泳ぐ
}
では、このチームに光合成について知らない新人がジョインしたとしましょう。
彼、彼女はこう思うのです。
「あ、humanとfishも光合成できるじゃん!!光合成させちゃえ!!」
// 前半省略
func main() {
human := &creature{spiecies: "ヒト"}
fmt.Printf("==== %s action ===\n", human.spiecies)
human.Breathe()
human.Photosynthesis()
human.Eat()
human.Run()
human.Speak()
human.Swim()
fmt.Printf("==== %s action over ===\n\n\n", human.spiecies)
plant := &creature{spiecies: "植物"}
fmt.Printf("==== %s action ===\n", plant.spiecies)
plant.Breathe()
plant.Photosynthesis()
fmt.Printf("==== %s action over ===\n\n\n", plant.spiecies)
fish := &creature{spiecies: "魚"}
fmt.Printf("==== %s action ===\n", fish.spiecies)
fish.Breathe()
fish.Photosynthesis()
fish.Eat()
fish.Swim()
fmt.Printf("==== %s action over ===\n\n\n", fish.spiecies)
}
==== ヒト action ===
ヒト: 酸素うまし!!
ヒト: 二酸化炭素もうまし!!
ヒト: メシウマ!!
ヒト: 止まるんじゃねえぞ!!
ヒト: ああ、それってハネクリボー?
ヒト: なついあつは泳ぐに限る!!
==== ヒト action over ===
==== 植物 action ===
植物: 酸素うまし!!
植物: 二酸化炭素もうまし!!
==== 植物 action over ===
==== 魚 action ===
魚: 酸素うまし!!
魚: 二酸化炭素もうまし!!
魚: メシウマ!!
魚: なついあつは泳ぐに限る!!
==== 魚 action over ===
あっという間に、正しく動かないプログラムの完成です。植物にとっては二酸化炭素は美味しいかもしれませんがhumanとfishにとっては二酸化炭素はセクシーではありません。
その職場の人達にとってはhumanとfishが光合成しないのは常識かもしれませんが、この新人の場合はそうでなかった。
インターフェースを分離していないことは属人化を助長するとも考えられないでしょうか?逆に言えばインターフェースを分離すれば属人化を和らげることができます。
基底インターフェースを用いた良い例
2つ例を紹介します。
悪い例と比べて冗長に感じる人もいるかもしれませんが、どちらの方法でも基底インターフェースを用いつつ不要なメソッドへの依存をなくすことができます。
メソッドの中身の記述量が多い場合は②の方法がおすすめです。
①最小限の基底インターフェースを用いた良い例
小さく作り、拡張する
最小限の基底インターフェースを埋め込み(embedding)する例
package main
import "fmt"
// creature基底インターフェース
type creatureAction interface {
Breathe() // 呼吸
}
// animal基底インターフェース
type animalAction interface {
Eat() // 食う
}
// ヒトインターフェース
type humanAction interface {
Run() // 走る
Speak() // しゃべる
Swim() // 泳ぐ
}
// 植物インターフェース
type plantAction interface {
Photosynthesis() // 光合成
}
// 魚インターフェース
type fishAction interface {
Swim() // 泳ぐ
}
// creature 基底タイプ
type creature struct {
spiecies string
creatureAction
}
type plant struct {
*creature
plantAction
}
type animal struct {
*creature
animalAction
}
type human struct {
*animal
humanAction
}
type fish struct {
*animal
fishAction
}
type cat struct {
*animal
catAction
}
func (c *creature) Breathe() {
fmt.Printf("%s: 酸素うまし!!\n", c.spiecies)
}
func (p *plant) Photosynthesis() {
fmt.Printf("%s: 二酸化炭素もうまし!!\n", p.spiecies)
}
func (a *animal) Eat() {
fmt.Printf("%s: メシウマ!!\n", a.spiecies)
}
func (h *human) Run() {
fmt.Printf("%s: 止まるんじゃねえぞ!!\n", h.spiecies)
}
func (h *human) Speak() {
fmt.Printf("%s: ああ、それってハネクリボー?\n", h.spiecies)
}
func (h *human) Swim() {
fmt.Printf("%s: なついあつは泳ぐに限る!!\n", h.spiecies)
}
func (f *fish) Swim() {
fmt.Printf("%s: なついあつは泳ぐに限る!!\n", f.spiecies)
}
func (c *cat) Run() {
fmt.Printf("%s: 止まるんじゃねえぞ!!\n", c.spiecies)
}
func main() {
human := &human{animal: &animal{creature: &creature{spiecies: "ヒト"}}}
fmt.Printf("==== %s action ===\n", human.spiecies)
human.Breathe()
human.Eat()
human.Run()
human.Speak()
human.Swim()
fmt.Printf("==== %s action over ===\n\n\n", human.spiecies)
plant := &plant{creature: &creature{spiecies: "植物"}}
fmt.Printf("==== %s action ===\n", plant.spiecies)
plant.Breathe()
plant.Photosynthesis()
fmt.Printf("==== %s action over ===\n\n\n", plant.spiecies)
fish := &fish{animal: &animal{creature: &creature{spiecies: "魚"}}}
fmt.Printf("==== %s action ===\n", fish.spiecies)
fish.Breathe()
fish.Eat()
fish.Swim()
fmt.Printf("==== %s action over ===\n\n\n", fish.spiecies)
cat := &cat{animal: &animal{creature: &creature{spiecies: "猫"}}}
fmt.Printf("==== %s action ===\n", cat.spiecies)
cat.Breathe()
cat.Eat()
cat.Run()
fmt.Printf("==== %s action over ===\n\n\n", cat.spiecies)
}
Run()やSwim()に見た目上の重複はありますが、このコードではfishがRunすることもcatがSwimすることもありません(トラ🐅のように泳げる猫もいますが、猫の皆さんお許しください🙇)。
実際
fish.Photosynthesis()をコードに加えると
fish.Photosynthesis undefined (type *fish has no field or method Photosynthesis)
と出ます。これはfishのPhotosynthesisへの依存が強制されていないことを示します。無事目的通りインターフェースを分離できました!!
②大きな基底インターフェースから必要な分だけ切り出す例
大きく作り、絞り込む
私はクリーンアーキテクチャでcontrollerからusecaseを介して、必要なrepositoryの処理を呼び出す際にはこちらを使用しています。
リポジトリを大きめに作っておいて(大きめとは言えどアクターごとにはちゃんと分ける)、usecaseで絞る感じです。
package main
import "fmt"
type creatureAction interface {
Breathe() // 呼吸
Photosynthesis() // 光合成
Eat() // 食う
Run() // 走る
Speak() // しゃべる
Swim() // 泳ぐ
}
type creature struct {
spiecies string
creatureAction
}
func (c *creature) Breathe() {
fmt.Printf("%s: 酸素うまし!!\n", c.spiecies)
}
func (c *creature) Photosynthesis() {
fmt.Printf("%s: 二酸化炭素もうまし!!\n", c.spiecies)
}
func (c *creature) Eat() {
fmt.Printf("%s: メシウマ!!\n", c.spiecies)
}
func (c *creature) Run() {
fmt.Printf("%s: 止まるんじゃねえぞ!!\n", c.spiecies)
}
func (c *creature) Speak() {
fmt.Printf("%s: ああ、それってハネクリボー?\n", c.spiecies)
}
func (c *creature) Swim() {
fmt.Printf("%s: なついあつは泳ぐに限る!!\n", c.spiecies)
}
type plant struct {
creature creature
plantAction
}
type plantAction interface {
Breathe() // 呼吸
Photosynthesis() // 光合成
}
func (p *plant) Breathe() {
p.creature.Breathe()
}
func (p *plant) Photosynthesis() {
p.creature.Photosynthesis()
}
type human struct {
creature creature
humanAction
}
type humanAction interface {
Breathe() // 呼吸
Eat() // 食う
Run() // 走る
Speak() // しゃべる
Swim() // 泳ぐ
}
func (h * human) Breathe() {
h.creature.Breathe()
}
func (h *human) Eat() {
h.creature.Eat()
}
func (h *human) Run() {
h.creature.Run()
}
func (h *human) Speak() {
h.creature.Speak()
}
func (h *human) Swim() {
h.creature.Swim()
}
type fish struct {
creature creature
fishAction
}
type fishAction interface {
Breathe() // 呼吸
Eat() // 食う
Swim() // 泳ぐ
}
func (f *fish) Breathe() {
f.creature.Breathe()
}
func (f *fish) Eat() {
f.creature.Eat()
}
func (f *fish) Swim() {
f.creature.Swim()
}
type cat struct {
creature creature
catAction
}
type catAction interface {
Breathe() // 呼吸
Eat() // 食う
Run() // 走る
}
func (c *cat) Breathe() {
c.creature.Breathe()
}
func (c *cat) Eat() {
c.creature.Eat()
}
func (c *cat) Run() {
c.creature.Run()
}
func main() {
human := &human{}
human.creature.spiecies = "ヒト"
fmt.Printf("==== %s action ===\n", human.creature.spiecies)
human.Breathe()
human.Eat()
human.Run()
human.Speak()
human.Swim()
fmt.Printf("==== %s action over ===\n\n\n", human.creature.spiecies)
plant := &plant{}
plant.creature.spiecies = "植物"
fmt.Printf("==== %s action ===\n", plant.creature.spiecies)
plant.Breathe()
plant.Photosynthesis()
fmt.Printf("==== %s action over ===\n\n\n", plant.creature.spiecies)
fish := &fish{}
fish.creature.spiecies = "魚"
fmt.Printf("==== %s action ===\n", fish.creature.spiecies)
fish.Breathe()
fish.Eat()
fish.Swim()
fmt.Printf("==== %s action over ===\n\n\n", fish.creature.spiecies)
cat := &cat{}
cat.creature.spiecies = "猫"
fmt.Printf("==== %s action ===\n", cat.creature.spiecies)
cat.Breathe()
cat.Eat()
cat.Run()
fmt.Printf("==== %s action over ===\n\n\n", cat.creature.spiecies)
}
まとめ
ポイントは以下のようなかんじではないでしょうか。
- 基底〇〇を作るのは構わないけれど、インターフェース分離の原則もお忘れなく。
- 本物の重複かどうかを見極めましょう。
これを守りDRYを適用するだけで結構コードがセクシーになって、保全性も上がると思います。
Discussion