Slide 1

Slide 1 text

Javaプログラムを Goに移植するためのテクニック ――継承と例外 Go Conference 2019 Spring 牧内大輔 @makki_d

Slide 2

Slide 2 text

念の為アンケート クラスと例外を持った言語に全く触れたことのない人 ● Javaの他、C++、C#、Ruby、Pythonなどどれでも

Slide 3

Slide 3 text

自己紹介 ● 牧内大輔 ● MakKi / @makki_d ● 所属:KLab株式会社 ○ エンジニアリングマネージャー ○ オンライン対戦の中継サーバを Goで書いたり ● gozxing ○ https://github.com/makiuchi-d/gozxing ○ 1D/2Dバーコードリーダーライブラリ ○ JavaのZXingをGoに移植

Slide 4

Slide 4 text

会社紹介 KLab(くらぶ)株式会社 ● モバイルオンラインゲームの開発運用 ● 今日は休日出勤扱い Go言語の活用事例 ● オンライン対戦同期サーバ ● MMORPGのゲームサーバ ● その他細かいツール類やslackbotなど

Slide 5

Slide 5 text

なぜJavaをGoに移植するのか

Slide 6

Slide 6 text

ソフトウェア資産を活かしたい ● Goは比較的新しい言語 ○ 2009年11月10日に発表 ● JavaやC++などのソフトウェア資産をGoでも利用したい ○ Goの標準ライブラリはかなり充実している ○ その範疇外のライブラリも世の中にはたくさんある ● PureなGoで書かれている価値 ○ クロスプラットフォーム対応のしやすさなど ● Go向けに設計し直すのは大変 ○ 言語構造、思想の違いが大きい ○ できることなら設計や構造をそのまま利用したい

Slide 7

Slide 7 text

諸注意 この発表は、Javaの構造をそのままGoに移植するためのテクニックの紹介です。 元コードの構造をそのまま残すため、Goでは推奨されない書き方が出てきます。 コード断片を表示しますが、見にくかったらごめんなさい。 Javaを題材としていますが、C++やC#のように同様のクラスシステムと 例外機構を持つ言語にも適用できると思います。 もっと良い書き方があれば教えてください。

Slide 8

Slide 8 text

クラスと継承

Slide 9

Slide 9 text

Java: クラスの定義 ● クラスは型 ● データと振る舞いを記述 ○ データ=フィールド ○ 振る舞い=メソッド ● コンストラクタで初期化 class Dog { public int Age; public Dog(int age) { Age = age; } public void Bark() { System.out.print("ワン"); } } ... Dog dog = new Dog(3); dog.Bark()

Slide 10

Slide 10 text

Go: 構造体とレシーバ関数 ● フィールドをまとめた構造体 ○ 型として定義 ● 初期化関数 ○ コンストラクタの代わり ○ 慣例的にNewで始める ● メソッドはレシーバ関数 ○ ポインタレシーバ ○ オブジェクトに対する副作用 type Dog struct { Age int } func NewDog(age int) *Dog { return &Dog{age} } func (dog *Dog) Bark() { fmt.Print("ワン") } ... dog := NewDog(3) dog.Bark()

Slide 11

Slide 11 text

Java: 継承による拡張 ● 派生クラス ○ フィールド、メソッドを継承 ○ 独自のフィールド、メソッドを追加 class HotDog extends Dog { public int Temperature; public HotDog(int age, int temp) : Dog(age) { Temperature = temp; } public void Breath() { System.out.print("ハッハッ"); } } ... HotDog dog = new HotDog(3, 25); dog.Bark()

Slide 12

Slide 12 text

Go: 構造体の埋め込み ● 親となる構造体を埋め込む ○ 無名のフィールド ○ 親のフィールドやメソッドが使える ■ あたかも派生構造体のもの ● 独自のフィールド、メソッド ○ 普通に追加できる type HotDog struct { *Dog Temperature int } func NewHotDog(age, temp int) *HotDog { return &HotDog{NewDog(age), temp} } func (dog *HotDog) Breath() { fmt.Print("ハッハッ") } ... dog := NewHotDog(3, 25) dog.Bark()

Slide 13

Slide 13 text

Java: メソッドのオーバーライド ● 派生クラスでオーバーライド ○ 親クラスの挙動を一部変更する ○ abstractメソッドの場合もある ■ 派生クラスでの実装を期待 class Dog { ... 略 ... public void BarkTwice() { Bark(); Bark(); } } class AmericanDog extends Dog { public void Bark() { System.out.print("Bow"); } } ... AmericanDog dog = new AmericanDog(3); dog.BarkTwice() // BowBow

Slide 14

Slide 14 text

Go: 埋め込みだけではオーバーライドできない ● レシーバの型は元の型のまま ○ 埋め込まれた型のレシーバが呼ばれる ○ 親クラスからは自身のメソッドになる BarkTwiceの中のdogは*Dogなので *DogのBarkが呼ばれる C++で言う仮想関数テーブルが必要 func (dog *Dog) BarkTwice() { dog.Bark() dog.Bark() } type AmericanDog struct { *Dog } ... 略 ... func (dog *AmericanDog) Bark() { fmt.Print("Bow"); } ... dog := NewAmericanDog(3); dog.BarkTwice() // ワンワン

Slide 15

Slide 15 text

Go: 仮想関数をフィールドに持つ ● Goでは関数も変数に入れられる ○ 関数変数をフィールドとして持てる ● 派生クラスで上書きできる ● 親クラスは関数フィールドを呼ぶ func NewDog(age int) *Dog { dog := &Dog{age} dog.VtBark = dog.Bark return dog } func NewAmericanDog(age int) *AmericanDog { dog := &AmericanDog{NewDog(age)} dog.VtBark = dog.Bark return dog } func (dog *AmericanDog) Bark() { fmt.Print("Bow"); } ... dog := NewAmericanDog(3); dog.BarkTwice() // BowBow type Dog struct { ... 略 ... VtBark func() } func (dog *Dog) BarkTwice() { dog.VtBark() dog.VtBark() } }

Slide 16

Slide 16 text

Go: 仮想関数をフィールドに持つ 関数変数で持つ場合の問題点 ● 仮想関数の数だけ代入が増える ● 全部実装されている保証がない ● 代入忘れが発生しうる interfaceとして持ってはどうか ● 代入が増えない ● 実装されていることが保証される type TrainedDog struct { *Dog VtOte func() VtOkawari func() VtOsuwari func() } func NewTrainedDog() *TrainedDog { dog := &TrainedDog{Dog()} dog.VtBark = dog.Bark dog.VtOte = dog.Ote dog.VtOkawari = dog.Okawari dog.VtOsuwari = dog.Osuwari return dog }

Slide 17

Slide 17 text

Go: interfaceによる仮想関数テーブル ● type AmericanDog struct { *Dog } func NewAmericanDog(age int) *AmericanDog { dog := &AmericanDog{NewDog(age)} dog.Vt = dog return dog } func (dog *AmericanDog) Bark() { fmt.Print("Bow"); } ... dog := NewAmericanDog(3); dog.BarkTwice() // BowBow type DogVT interface { Bark() } type Dog struct { Vt DogVT Age int } func NewDog(age int) *Dog { dog := &Dog{Age: age} dog.Vt = dog return dog } func (dog *Dog) BarkTwice() { dog.Vt.Bark() dog.Vt.Bark() } } ... 略 ...

Slide 18

Slide 18 text

Java: 多態 ● 派生クラスは親クラスの部分型 ○ 親クラスとして扱える ○ 親クラスとしての振る舞いを持つ ○ 親クラス型の変数に代入可能 Dog dog; dog = new HotDog(2, 20); dog.Bark(); dog = new AmericanDog(3); dog.Bark();

Slide 19

Slide 19 text

Go: interfaceによる多態 ● interfaceを満たせばその型 ○ その型振る舞いを持つ ○ その型の変数に代入可能 ○ 構造体埋め込みで自動的に満たせる ● 別途interfaceを定義 ● Javaより型制約がゆるい ○ 移植する上では問題ない type IDog interface { Bark() } var dog IDog dog = NewHotDog(2, 20) dog.Bark() dog = NewAmericanDog(3) dog.Bark()

Slide 20

Slide 20 text

クラスと継承のまとめ ● クラスを構造体とレシーバ関数で表現 ● 構造体の埋め込みで継承を真似る ● 明示的な仮想関数テーブルでオーバーライドを実現 ○ 関数フィールドを持つ方法 ○ インターフェイスをフィールドに持つ方法 ● interfaceを使って多態を実現

Slide 21

Slide 21 text

例外

Slide 22

Slide 22 text

Java: 例外機構 ● throwで例外送出 ● try-catchで捕捉 int div(int a, int b) { if (b == 0) { throw new Exception("zero division") } return a / b; } ... try{ avg = div(sum, count); } catch(Exception e){ // 何か処理 }

Slide 23

Slide 23 text

Go: errorを返す ● 例外機構は無い ● 明示的にerrorを返す ○ 正常終了ならnil ● 複数戻り値を利用 ○ 慣例的にerrorは最後 例外を返しうるメソッドすべての 戻り値にerrorを追加していく func div(a, b int) (int, error) { if b == 0 { return 0, errors.New("zero division") } return a / b, nil } ... avg, err = div(sum, count) if err != nil { // 何か処理 }

Slide 24

Slide 24 text

Java: 例外種別による処理分け ● 継承による階層構造 ● catchで捕捉する例外型を指定 ○ 捕捉しないものは更に上に伝播 try{ doSomething(); } catch(IOException e){ // 何か処理 } catch(RuntimeException e){ // 何か処理 } Exception IOException RuntimeException EOFException SocketException

Slide 25

Slide 25 text

Go: interface継承による例外階層 ● Javaでの継承関係と同じ ○ errorを継承すればerrorとして返せる ● 実体はerrorを埋め込んだ構造体 type Exception interface { error exception() } type IOException interface { Exception ioException() } type EOFException interface { IOException eofException() } type eofException struct { error } func (_ eofException) exception() {} func (_ eofException) ioException() {} func (_ eofException) eofException() {} Exception IOException RuntimeException EOFException SocketException

Slide 26

Slide 26 text

Go: 例外種別の判定 ● 型アサーションによる判定 ● 型スイッチによる判定 ○ 複数catchがある場合に便利 欠点 ● ラップされると型が変わる ○ xerrorsのErrorfの ": %w" ○ 型で判定できなくなる func DoSomething() error { return eofException{ errors.New("エラー"), } } ... err := DoSomething() if err != nil { if _, ok := err.(IOException); ok { // 何か処理 } } switch err.(type) { case nil: break case EOFException, SocketException: // 何か処理 }

Slide 27

Slide 27 text

Go: xerrors.Asによる判定 ● xerrors.Asの実装 ○ 次の条件に合うまで Unwrap() ■ targetの型に代入可能 ■ err.As(target) がtrue ● 代入可能かを利用した判定 ○ targetにはinterface型も渡せる ○ interface継承の例外でも使える ● Asメソッドによる判定 ○ 今回は省略 ○ 例外の型ごとに柔軟に設定できる func DoSomething() error { return eofException{ errors.New("エラー"), } } func DoSomething2() error { err := DoSomething() return xerrors.Errorf("エラー2: %w", err) } ... err := DoSomething2() if err != nil { var exception IOException if xerrors.As(err, &exception) { // 何か処理 } }

Slide 28

Slide 28 text

例外まとめ ● interface継承による例外階層の模倣 ○ Javaと同じ継承関係にできる ● 型アサーションや型スイッチによる例外種別判定 ○ 複数catchがある場合型スイッチが便利 ○ ラップされると判定できない ● xerrors.As による例外種別判定 ○ ラップされる場合はこちらを使う

Slide 29

Slide 29 text

おしまい 質問やもっと良い書き方の提案などあればお願いします。