ども、k69 です。
JavaプログラマがGo言語を触ってみた(とりあえず触ってみる編)に続きGo言語のお勉強。
Javaのクラス設計と同じようにUMLで設計したところ、Go言語では「継承(extends)」がないので共通処理(抽象メソッド)をどうやって実現すべきかイメージできなかった。。。
じゃ、「簡単なJavaのクラス設計をGo言語で実装してみよう!」と
ウニウニしたのがこの記事です。
対象読者
- Go言語のソースファイルの作成単位に悩んでいる人
- Javaの継承(extends)を使った共通処理(抽象メソッド)をGo言語でどうやって実装するか悩んでいる人
- Go言語の初心者
前提条件
- Java言語の知識がある人
はじめに
Javaの継承(extends)をつかった簡単なサンプルをGo言語に置き換えてみます。
「動物」を継承するクラス「イヌ」と「ネコ」に「鳴く」メソッドを実装してみます。
1. クラス図
厳格なUMLではないですが、こんな感じでしょうか。
3. Go言語で同じものを実現しようとすると・・・
以下、悩みました(;´Д`)
- Go言語のソースファイルはどう分割すればいいの?(クラスの概念ないし…)
- GitHubで競合しないようにソースファイルを細かく分けたい。
- 動物の種類(サルとかキジとか)が増えたら、ソースファイルを増やす設計にしたい。
- 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)という振舞いを抽象メソッドで実装してます。
package animal;
public abstract class Animal {
public abstract void bark();
}
そして、Go言語のソース。
Go言語は、抽象メソッドはありませんので、代わりにインタフェースで鳴く(bark)という振舞いを実装しています。
**「動物という抽象的な概念(クラスに代わるもの)はない」**です。。
※ ファイル名のAnimalはどこにも影響ありません。
package animal
type Barker interface {
Bark(string)
}
あと、先頭が大文字になるとpublicスコープになるみたいです。
5-2. 具象オブジェクト(Cat)
こちらもJavaソースから。
Catクラスはネコの鳴き声(voice)を持ち、
ネコが鳴く(bark)振舞いを持っています。
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())ですね。
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)を呼び出しています。
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と同じで分かりやすいですね。
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
- Goはオブジェクト指向言語だろうか?
http://postd.cc/is-go-object-oriented/ - Go言語での構造体実装パターン
http://blog.monochromegane.com/blog/2014/03/23/struct-implementaion-patterns-in-golang/ - Go 言語の値レシーバとポインタレシーバ
https://skatsuta.github.io/2015/12/29/value-receiver-pointer-receiver/