追記
以下の点を修正と追記を行いました。
2018/11/13
- 矢印の矢の形を間違えていたため図の修正
- 参考文献を記載
はじめに
久川善法です。
過去に自分が行った実装を振り返ったときにもっと良い方法があったと思うことがあります。
早速質問です。こんなことを考えたことはありませんか?
継承しようと思ったけど、一部の継承先にだけは間違った継承をさせてしまっている。
当時(半年前)の私は以下の解決策を思いつきました。
- オーバーライドを使って個別の処理は揉み消す
- 継承の階層を深くして解決する
- Has-aの関係だからとりあえずinterfaceで解決する
結果的にこの全てを織り交ぜて対応しましたが、今になって思うと微妙でした。
では今ならどうするのか?
Compositionを使った方法で解決します。
それではなぜ当時の私の解決策が微妙なのか、今の解決策に至るまでの血と涙の結晶を説明していきます。
こんな問題にぶち当たった
Birdを継承したPenguinを追加したいけど、Penguinって飛べないじゃん。。。
Badな解決策1:オーバーライドを使った方法
OwlとPigeonは飛べるからPenguinだけ個別にオーバーライドして動作を変えちゃおう!
code
package main
import (
"fmt"
)
func main() {
p := &Penguin{}
p.Greet()
p.Fly()
}
/* 継承で言うところの親になる鳥 */
type Bird struct{}
func (b *Bird) Greet() {
fmt.Println("hello qiita")
}
func (b *Bird) Fly() {
fmt.Println("I can fly")
}
/* フクロウ */
type Owl struct {
Bird
}
/* ハト */
type Pigeon struct {
Bird
}
/* ペンギン */
type Penguinstruct {
Bird
}
// Birdを継承しているが、飛ぶことは出来ないためオーバーライドして動作を書き換えている
func (p *Penguin) Fly() {
fmt.Println("I can't fly")
}
オーバーライドを使った解決策の問題
- Penguin以外の飛べない動物を追加するたびにオーバーライドする必要があり継承先でコードが重複
- 使う人が継承先の動物の振る舞いを全て知る必要がある
- 親の修正に伴って、子も修正する必要があり、修正による影響が大きい
- etc..
キリがないですね。
Badな解決策2:継承の階層を深くした方法
Birdには全てに共通するGreetメソッドのみを持たせて、Flyメソッドを持ったBirdFlyとBirdNotFlyを継承させよう!
継承の階層を深くして解決しよう!
code
package main
import (
"fmt"
)
func main() {
b := &Owl{}
b.Greet()
b.Fly()
}
/* 継承で言うところの親になる鳥 */
type Bird struct{}
func (b *Bird) Greet() {
fmt.Println("hello qiita")
}
type BirdFly struct{}
func (b *BirdFly) Fly() {
fmt.Println("I can fly")
}
type BirdNotFly struct{}
func (b *BirdNotFly) Fly() {
fmt.Println("I can't fly")
}
/* フクロウ */
type Owl struct {
Bird
BirdFly
}
/* ハト */
type Pigeon struct {
Bird
BirdFly
}
/* ペンギン */
type Penguin struct {
Bird
BirdNotFly
}
継承の階層を深くした方法の問題
- 継承させまくることで、ガッチガチのオブジェクトが出来上がる(変更が困難)
- 機能を追加するたびに継承の階層を深くなっていく可能性がある
- etc..
オーバーライドする方法よりはマシですが、多用するのはよくなさそうですね。
Badな解決策3:interfaceを使った方法
そうだ!飛べるって振る舞いはHas-aの関係だからinterfaceを実装しよう!
code
package main
import (
"fmt"
)
func main() {
var bird Flyable
bird = &Penguin{}
bird.(*Penguin).Greet()
bird.Fly()
}
/* 継承で言うところの親になる鳥 */
type Bird struct {
}
func (b *Bird) Greet() {
fmt.Println("hello qiita")
}
type Flyable interface {
Fly()
}
/* フクロウ */
type Owl struct {
Bird
}
func (o *Owl) Fly() {
fmt.Println("I can fly")
}
/* ハト */
type Pigeon struct {
Bird
}
func (o *Pigeon) Fly() {
fmt.Println("I can fly")
}
/* ペンギン */
type Penguin struct {
Bird
}
func (o *Penguin) Fly() {
fmt.Println("I can't fly")
}
※当時はJavaだったためそれに似せて書いています。そのため歪なコードになっています。
interfaceを使った方法の問題
- 個別に振る舞いを実装することになるためコードが重複
- 修正による影響が膨大
- etc..
コードの重複量が多い上、修正の影響が多く出てしまう問題が解決されていない。
Goodな解決策:Compositionを使った方法
処理を切り替えるだけなんだからStrategyパターンを採用できそう!
※コンポジションとは、あるオブジェクトに別のオブジェクトを取り込んで扱うこと
code
package main
import (
"fmt"
)
func main() {
flyable := &NotFlying{}
p := &Penguin{Bird{flyable}}
p.Greet()
p.Fly()
}
/* 継承で言うところの親になる鳥 */
type Bird struct {
Flyable
}
func (b *Bird) Greet() {
fmt.Println("hello qiita")
}
func (b *Bird) Fly() {
b.Flyable.Fly()
}
type Flyable interface {
Fly()
}
/* フクロウ */
type Owl struct {
Bird
}
/* ハト */
type Pigeon struct {
Bird
}
/* ペンギン */
type Penguin struct {
Bird
}
/* 飛べる鳥用のFly */
type Flying struct{}
func (o *Flying) Fly() {
fmt.Println("I can fly")
}
/* 飛べない鳥用のFly */
type NotFlying struct{}
func (o *NotFlying) Fly() {
fmt.Println("I can't fly")
}
問題は解決
Compositionを使った方法では以下の問題が解決できました。
- FlyとNotFlyのコードの重複
- FlyとNotFly以外の行動があれば、その振る舞いだけ追加すれば良いだけなので変更に強い
- BirdがInterfaceに依存しているため、簡単に処理を切り替える事が出来る
- etc..
最後に
今回はCompositonを使って過去の問題を解決してみました。
当時はこういった解決方法が思いつきませんでしたが、デザインパターンの勉強を始めてから良い方法を思いつく様になってきました。
今回のケースでいうとStrategyパターンを採用しています。
この記事を書く事で、より継承やインタフェースの使い方がイメージできるようになりました。
学んで行く中でもっと良い方法が見つかるのでとても楽しいです。
※より良い方法や間違っている箇所があればコメントにてご指摘お願いします!
参考文献
以下の書籍を参考にさせていただいております。
- アジャイルソフトウェア開発の奥義
- Java言語で学ぶデザインパターン入門
- Head Firstデザインパターン
補足
- 図を無理にコードで表現しているので、概念の理解には図を参考にしてください。
- Golangは継承がないので構造体を埋め込む形で継承を表現しています。
- 当時はJavaで実装していましたが、今回は勉強中であるGolangで実装をしています。
- Compositon以外を否定するといった内容ではありません。今回のケースはCompositionを使うことが一番の良い解決策だったのでその紹介です。