🐱

インターフェース分離の原則を考える

11 min read

前置き

クリーンアーキテクチャに則った個人開発のサービスを運用しているmaruです。

今回はインターフェース分離の原則について書いていこうと思います。

https://zenn.dev/maru44/articles/b9e07e91a0ea77
こちらの記事のテスト可能の追記は1週間くらい待ってください。🙇

インターフェース分離の原則とは

SOLIDの原則の"I"に当たる部分で、原題は Interface Segregation Principle となります。

インターフェースの利用者にとって不要なメソッドへの依存を強制してはいけないという原則です。
つまり簡単に言ってしまえば 不要なメソッドが存在する状況を作るな!! 😡 とおっしゃるわけです。

DRYとの共存

少し話が脱線するように思われますが、インターフェース分離を妨げる要因の一つにDRY原則への陶酔があるのでお話します。

実際私もそれに陥りかけ、師匠に指摘されて気づきました。

DRY原則とは

Don't Repeat Yourself
同じ意味や機能を持つ情報を複数の場所に重複して置くことをなるべく避けるべきとする考え方。

共存

ボブおじさんは書籍においてDRYに決して否定的ではありません。しかし、行き過ぎたDRY偏重主義が私たちを苦しめると説いています。
彼はクリーンアーキテクチャの書籍において重複(DRY)についてこのように述べています。

皆重複を排除したがるけれど、その重複は 本物の重複 ですか?
本物の重複 : あるインスタンスに変更があればそのインスタンスのすべての複製にも同じ変更を反映しなければならない。
偽物の重複 : 明らかに重複していたコードが時間とともに異なる真価をとげて、数年後には全く違うものになっている。

つまり、安易に「この処理とこの処理は似ているから共通化してしまおう!!」ということをするな!!とおっしゃっています。

実際のコードで考える

極々簡単なコードを用いて説明していきます。

悪い例

ダメな例です。まずは全文。

bad.go
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も光合成できるじゃん!!光合成させちゃえ!!」

bad.go
// 前半省略

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)する例

good.go
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で絞る感じです。

good2.go
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)
}

まとめ

ポイントは以下のようなかんじではないでしょうか。

  1. 基底〇〇を作るのは構わないけれど、インターフェース分離の原則もお忘れなく。
  2. 本物の重複かどうかを見極めましょう。

これを守りDRYを適用するだけで結構コードがセクシーになって、保全性も上がると思います。

Discussion

ログインするとコメントできます