LoginSignup
11
7

More than 5 years have passed since last update.

Goのstructでオブジェクト指向言語と同じようにやってハマること

Posted at

オブジェクト指向言語では、以下のように親クラスの関数が子クラスでオーバーライドされることを期待する実装ができます。(Swiftで書いています)

class Human {
    func weapon() -> String {
        return "拳"
    }

    func attack() {
        print(weapon() + "で攻撃した")
    }
}

class Fighter: Human {
    override func weapon() -> String {
        return "グレートソード"
    }
}

関数呼び出し

var human: Human

print("== Human ==")
human = Human()
print("武器は" + human.weapon())
human.attack()
print("")

print("== Fighter ==")
// Human型に代入
human = Fighter()
print("武器は" + human.weapon())
human.attack()
print("")

実行結果

== Human ==
武器は拳
拳で攻撃した

== Fighter ==
武器はグレートソード
グレートソードで攻撃した

親クラスHumanのattack()は、子クラスでweapon()が変更されることを期待している。(正しい武器で攻撃メッセージが表示されることを期待)

structの失敗例

Goで同じ動作を期待して、ハマった人は少なくないと思います。以下はGoで失敗する例です。

//
// Human
//
type Human struct {
}

func (self *Human) Weapon() string {
    return "拳"
}

func (self *Human) Attack() {
    fmt.Println(self.Weapon() + "で攻撃した")
}

//
// Fighter
//
type Fighter struct {
    Human
}

func (self *Fighter) Weapon() string {
    return "グレートソード"
}

//
// Paladin
//
type Paladin struct {
    Human
}

func (self *Paladin) Weapon() string {
    return "エクスカリバー"
}

関数呼び出し

fmt.Println("== Human ==")
human := Human{}
fmt.Println("武器は" + human.Weapon())
human.Attack()
fmt.Println("")

fmt.Println("== Fighter ==")
fighter := Fighter{}
fmt.Println("武器は" + fighter.Weapon())
fighter.Attack()
fmt.Println("")

fmt.Println("== Paladin ==")
paladin := Paladin{}
fmt.Println("武器は" + paladin.Weapon())
paladin.Attack()
fmt.Println("")

実行結果

== Human ==
武器は拳
拳で攻撃した

== Fighter ==
武器はグレートソード
拳で攻撃した

== Paladin ==
武器はエクスカリバー
拳で攻撃した

main()からのWeapon()の呼び出しは、問題なくそれぞれのstruct関数で実装したとおりの文字列を返していますが、Attack()からのWeapon()呼び出しは、FighterもPaladinもHumanのWeapon()を呼び出しています。

Goではオブジェクト指向の継承という概念は存在しません。以下の記述は、FighterがHumanを継承をしているのではなく、あくまでFighterがHumanを埋め込んでいるだけです。

type Fighter struct {
    Human
}

そこに親子関係のようなものはありません。つまり、以下のようなことです。

fighter := Fighter{}
// FighterにAttack()はないから、HumanのAttack()を呼び出している
fighter.Attack()
// こう書くのと同じ。上はそれを省略的に書いているようなもの
fighter.Human.Attack()

HumanのAttack()で呼び出すself.Weapon()のselfは常にHumanです。だから、パラディンも拳で攻撃するのです・・・

interfaceでも失敗する例

interfaceを介しても結果は同じです。

//
// Player interface
//
type Player interface {
    Weapon() string
    Attack()
}

Human,Fighter,Paladinのstruct実装は先ほどと同じとします。これらはWeapon()、Attack()を呼び出せるため、Player interfaceを実装していることになります。

そして、Player interfaceを介して、Weapon()、Attack()を呼び出します。

var player Player

fmt.Println("== Human -> Player ==")
player = &Human{}
fmt.Println("武器は" + player.Weapon())
player.Attack()
fmt.Println("")

fmt.Println("== Fighter -> Player ==")
player = &Fighter{}
fmt.Println("武器は" + player.Weapon())
player.Attack()
fmt.Println("")

fmt.Println("== Paladin -> Player ==")
player = &Paladin{}
fmt.Println("武器は" + player.Weapon())
player.Attack()
fmt.Println("")

これも先ほどと同じように、Attack()からのWeapon()呼び出しは、どれもHumanのそれを呼び出します。

== Human -> Player ==
武器は拳
拳で攻撃した

== Fighter -> Player ==
武器はグレートソード
拳で攻撃した

== Paladin -> Player ==
武器はエクスカリバー
拳で攻撃した

structの埋め込みをやめる

Fighter、PaladinはHumanの詰め込みをやめて、Attack()を各々実装するように変えてみます。

//
// Player interface
//
type Player interface {
    Weapon() string
    Attack()
}

//
// Human
//
type Human struct {
}

func (self *Human) Weapon() string {
    return "拳"
}

func (self *Human) Attack() {
    fmt.Println(self.Weapon() + "で攻撃した")
}

//
// Fighter
//
type Fighter struct {
}

func (self *Fighter) Weapon() string {
    return "グレートソード"
}

func (self *Fighter) Attack() {
    fmt.Println(self.Weapon() + "で攻撃した")
}

//
// Paladin
//
type Paladin struct {
}

func (self *Paladin) Weapon() string {
    return "エクスカリバー"
}

func (self *Paladin) Attack() {
    fmt.Println(self.Weapon() + "で攻撃した")
}

これで先ほどと同じようにWeapon()、Attack()を呼び出してみると、以下のように期待動作になります。

== Human ==
武器は拳
拳で攻撃した

== Fighter ==
武器はグレートソード
グレートソードで攻撃した

== Paladin ==
武器はエクスカリバー
エクスカリバーで攻撃した

この例では逐一Attack()を実装してもたいしたボリュームはないですが、実践で同様の対処をすると、似たようなコードをたくさん書くはめになりかねません。

Dependency Injectionな例

では、どうするのがいいかというと、Attack()をHuman、Fighter、Paladinに実装せず、外に追い出します。ここではBattle structに追い出しています。

//
// Battle
//
type Battle struct {
    player Player
}

func (self *Battle) Attack() {
    fmt.Println(self.player.Weapon() + "で攻撃した")
}

もしくはstructの関数ではなく、単なる関数にもできるでしょう。

func Attack(player Player) {
    fmt.Println(player.Weapon() + "で攻撃した")
}

そして以下のように呼び出します。(ここではBattle structを使います)

var battle Battle
fmt.Println("== Human -> Battle ==")
battle = Battle{
    player: &Human{},
}
battle.Attack()
fmt.Println("")

fmt.Println("== Fighter -> Battle ==")
battle = Battle{
    player: &Fighter{},
}
battle.Attack()
fmt.Println("")

fmt.Println("== Paladin -> Battle ==")
battle = Battle{
    player: &Paladin{},
}
battle.Attack()
fmt.Println("")

実行結果

== Human -> Battle ==
拳で攻撃した

== Fighter -> Battle ==
グレートソードで攻撃した

== Paladin -> Battle ==
エクスカリバーで攻撃した

うまくいきました。これは実践でも使える方法です。Attack()が1つに集約されているので、実装ボリュームが増えても、複数のstructで重複実装をする必要はありません。

単にGoは継承がないから苦肉の策としてこうするのではなく、structどうしを疎結合にするというメリットもあります。いわゆるDI(Dependency Injection)と呼ばれるこの設計については、以下の記事が参考になると思います(コードの例はPHPです)。

初学者でも5分で理解できるようにDI(Dependency Injection)を説明してみた

継承について改めて考える

そもそも、親クラスの関数Aから関数Bを呼び出し、関数Bは子クラスでオーバーライドされることを親クラスが期待する。こういった実装は、それが機能するオブジェクト指向言語であれ、親クラス側で想定外のことが起きるデメリットもあります。言語によっては関数にfinalキーワードをつけて、望まないオーバーライドを禁止できるものもありますが、付け焼き刃な言語仕様のような気もします・・・

ちなみに以下の記事に、継承は除外されているのがベストという見出しで、Javaの生みの親の人の話があります。

Goはオブジェクト指向言語だろうか?

英文の元記事
Is Go An Object Oriented Language?

「もう一度最初からJavaを作り直すとしたら、どこを変更したいですか?」 答えは「クラスを除外するでしょうね」というものでした。笑いが静まった後、彼が説明したのは、本当の問題はクラス自体ではなく実装継承(extendsの関係)なのだということでした。インターフェースによる継承(implementsの関係)のほうが望ましいのです。できる限り実装継承は避けたほうがよいでしょう。

まさにGoはその思想を形にした言語と言えそうです。この記事を書いた人は、Goは継承を使わないオブジェクト指向プログラミング言語であると言っています。継承やポリモーフィズムは、オブジェクト指向で必須とは限らないという見方ですね。

Goは慣れ親しんできたオブジェクト指向言語とは勝手が違い、同じように設計できない苦労もありますが、改めて設計について考えさせられる面白さもあります。

ハマらないように設計で意識すること

今回書いたようなハマりに陥らないために、以下のことを意識すると良いかなと思います。

  • structの関数の中でstruct自身の別の関数を呼ぶのを避ける
  • 継承的な動作を期待するstructの埋め込みは避ける
  • typeが異なるstructを同じように扱いたい時は、interfaceを宣言し、structはinterfaceの関数を実装する
  • そのinterfaceを受け取る関数を作り、interfaceを介して異なるtypeのstructで共通する処理実装をする(struct自身の関数で実装しない&DI)

あと調べたら、私のこの記事と似た内容の記事もありましたね(過去に読んだ記憶もある)。ちょっと切り口が違うしDIの話はないので重複ではないはず・・・

オブジェクト指向言語としてGolangをやろうとするとハマること

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7