Cover Image for Javaの継承をGo言語で実装してみる
Javaの継承をGo言語で実装してみる

image

ども、k69 です。

JavaプログラマがGo言語を触ってみた(とりあえず触ってみる編)に続きGo言語のお勉強。

Javaのクラス設計と同じようにUMLで設計したところ、Go言語では「継承(extends)」がないので共通処理(抽象メソッド)をどうやって実現すべきかイメージできなかった。。。

じゃ、「簡単なJavaのクラス設計をGo言語で実装してみよう!」と
ウニウニしたのがこの記事です。

対象読者

  • Go言語のソースファイルの作成単位に悩んでいる人
  • Javaの継承(extends)を使った共通処理(抽象メソッド)をGo言語でどうやって実装するか悩んでいる人
  • Go言語の初心者

前提条件

  • Java言語の知識がある人

はじめに

Javaの継承(extends)をつかった簡単なサンプルをGo言語に置き換えてみます。
「動物」を継承するクラス「イヌ」と「ネコ」に「鳴く」メソッドを実装してみます。

1. クラス図

厳格なUMLではないですが、こんな感じでしょうか。

image

3. Go言語で同じものを実現しようとすると・・・

以下、悩みました(;´Д`)

  1. Go言語のソースファイルはどう分割すればいいの?(クラスの概念ないし…)
  • GitHubで競合しないようにソースファイルを細かく分けたい。
  • 動物の種類(サルとかキジとか)が増えたら、ソースファイルを増やす設計にしたい。
  1. Go言語では継承(extends)の概念がないので共通処理(抽象メソッド)をどうやって実現すればいいの?

これらの悩みを自分なりに考えてみました。

4. 【悩みその1】Go言語のソースファイルをどう分割すればいいの?(クラスの概念ないし…)

Javaのソースファイル構成とGo言語のソースファイル構成を比較して考えてみました。

4-1. Javaのソースファイル構成

Javaプログラマが上記のクラス図を見てソースファイル構成を決めると、大抵1クラス1ファイルにすると思います。

ファイル構成
└─src
    ├─animal                         // javaのパッケージ単位フォルダ
    │      Animal.java               // javaのクラス単位
    │      Cat.java
    │      Dog.java
    │
    └─main
            Main.java

4-2. Go言語のソースファイル構成

Javaのソースファイル構成と同じ構成です。今回はあえて同じにしています。
あえての理由は、"「悩みその1」まとめ"で。

ファイル構成
└─src
    ├─animal                         // goのパッケージ単位フォルダ
    │      Animal.go                 // goにはクラスの概念がない
    │      Cat.go
    │      Dog.go
    │
    └─main
            Main.go

Go言語にはクラスの概念がないので、3つ(Animal.go, Cat.go, Dog.go)のソースファイルを分割する必要はありません。(ただし、パッケージは同じでないとダメ)

たとえば、次のように1ファイルにまとめるても文法的にはOK。(あくまで文法的にはです。。。)

ファイル構成
└─src
    ├─animal 
    │      Animal_Cat_Dog.go   // 1つのソースファイルにまとめても文法的にOK
    │
    └─main
            Main.go

4-3. 「悩みその1」まとめ

「Go言語のソースファイル分割をどうすればいいの?(クラスの概念ないし…)」に対する自分なりの答えは以下となりました。

  • Go言語のソースファイルはGitHubで管理しやすい(競合しない細かい)単位にする。
  • 似て非なる機能(今回の例だと、動物の種類(サルとかキジとか))が増えることをあらかじめわかっていれば、その機能単位でソースファイルを作成する。
  • Go言語には継承がないので、パッケージフォルダに基底となるソースファイル(今回の例だと”Animal.go”)を1つ作成する。

※ ベストなやり方を模索しているので皆さんコメントお願いします。(^O^)/

5. 【悩みその2】 Go言語では継承(extends)の概念がないので共通処理(抽象メソッド)をどうやって実現すればいいの?

5-1. 共通処理(抽象メソッド)

まずはJavaのソースを見てみましょう。
動物は鳴く(bark)という振舞いを抽象メソッドで実装してます。

Animal.java
package animal;

public abstract class Animal {
    public abstract void bark();
}

そして、Go言語のソース。
Go言語は、抽象メソッドはありませんので、代わりにインタフェースで鳴く(bark)という振舞いを実装しています。

**「動物という抽象的な概念(クラスに代わるもの)はない」**です。。
※ ファイル名のAnimalはどこにも影響ありません。

Animal.go
package animal

type Barker interface {
	Bark(string)
}

あと、先頭が大文字になるとpublicスコープになるみたいです。

5-2. 具象オブジェクト(Cat)

こちらもJavaソースから。
Catクラスはネコの鳴き声(voice)を持ち、
ネコが鳴く(bark)振舞いを持っています。

Cat.java
package animal;

public class Cat extends Animal {
    private String voice = "にゃーにゃー";

    public void bark() {
        System.out.println(this.voice);
    }
}

次にGo言語。

Go言語はクラスの概念がないので、ネコを構造体(struct)で定義し、ネコ型のデータ型(type)として宣言します。しかし、データ型はJavaクラスのように値("voice=にゃーにゃー")を持つことができません。

ここで登場するのが、**Javaコンストラクタ風のメソッドNewCat()**です。このメソッドでデータ型(Cat)のインスタンスを作成する時に値(”にゃーにゃー”)を持たせます。

そして、Animal.goで定義したインタフェース宣言Bark()を実装します。ここでポイントとなるのが、レシーバーです。
 **レシーバーとは「データ型に対してメソッド定義されたもの」**です。ここではデータ型(type Cat struct)のメソッド(Bark())ですね。

Cat.go
package animal

import (
	"fmt"
)

type Cat struct {    // ネコを構造体(struct)で定義
	voice string
}

func NewCat() *Cat {  // Javaコンストラクタ風のメソッド
	return &Cat{"にゃーにゃー"} 
}

func (c Cat) Bark(){  // レシーバー
	fmt.Println(c.voice)
}

5-3. 具象オブジェクト(Dog)

Cat と同じなので省略。

5-4. Mainオブジェクト

最後にMainです。こちらもJavaから。
Cat,Dogの具象オブジェクトのインスタンスを作成し、
インタフェース(Animal)で定義したメソッド(bark)を呼び出しています。

Main.java
package main;

import animal.Cat;
import animal.Dog;
import animal.Animal;

public class Main {
    public static void main(String[] args) {
       Animal c = new Cat();
       Animal d = new Dog();

       c.bark();
       d.bark();
    }
}

次にGo言語。
 具象オブジェクト(Cat や Dog)からインタフェース(Baker)に変換するところも、基本的にJavaと同じで分かりやすいですね。

Main.go
package main

import (
	"animal"
)

func main(){
	var c animal.Barker = animal.NewCat()
	var d animal.Barker = animal.NewDog()

	c.Bark()
	d.Bark()
}

まとめ

同じ設計をGo言語とJava言語で実装してみることで、Java言語の設計(クラス設計)をGo言語に適用することはできそうです。ただし、実装するときには言語仕様の違いがあるので、Java言語のクラス設計をGo言語に適用するには、今回のように実装パターンを覚える必要がありそうです。

Java言語は継承(extends)や実装(implements)によりクラス間に制約を与え、それぞれの役割を明確にすることができるので、大人数(大規模)で実装するのに適しているかもしれません。一方、Go言語は制約がゆるいのでオブジェクト間の依存度が低いため、頻繁に改修されるようなアプリケーションに向いていると感じました。

参考URL

© 2022 k69

本サイトを通じて、たくさんの人がプログラミングに興味を持ち、 これがキッカケでモノづくりの楽しさに触れることができれば幸せです!