JavaプログラムをGoに移植するためのテクニック――継承と例外

99b72ba4c7dd4da957edb3e619a6d71f?s=47 MakKi
May 18, 2019

 JavaプログラムをGoに移植するためのテクニック――継承と例外

Go Conference 2019 Spring

99b72ba4c7dd4da957edb3e619a6d71f?s=128

MakKi

May 18, 2019
Tweet

Transcript

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

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

  3. 自己紹介 • 牧内大輔 • MakKi / @makki_d • 所属:KLab株式会社 ◦

    エンジニアリングマネージャー ◦ オンライン対戦の中継サーバを Goで書いたり • gozxing ◦ https://github.com/makiuchi-d/gozxing ◦ 1D/2Dバーコードリーダーライブラリ ◦ JavaのZXingをGoに移植
  4. 会社紹介 KLab(くらぶ)株式会社 • モバイルオンラインゲームの開発運用 • 今日は休日出勤扱い Go言語の活用事例 • オンライン対戦同期サーバ •

    MMORPGのゲームサーバ • その他細かいツール類やslackbotなど
  5. なぜJavaをGoに移植するのか

  6. ソフトウェア資産を活かしたい • Goは比較的新しい言語 ◦ 2009年11月10日に発表 • JavaやC++などのソフトウェア資産をGoでも利用したい ◦ Goの標準ライブラリはかなり充実している ◦

    その範疇外のライブラリも世の中にはたくさんある • PureなGoで書かれている価値 ◦ クロスプラットフォーム対応のしやすさなど • Go向けに設計し直すのは大変 ◦ 言語構造、思想の違いが大きい ◦ できることなら設計や構造をそのまま利用したい
  7. 諸注意 この発表は、Javaの構造をそのままGoに移植するためのテクニックの紹介です。 元コードの構造をそのまま残すため、Goでは推奨されない書き方が出てきます。 コード断片を表示しますが、見にくかったらごめんなさい。 Javaを題材としていますが、C++やC#のように同様のクラスシステムと 例外機構を持つ言語にも適用できると思います。 もっと良い書き方があれば教えてください。

  8. クラスと継承

  9. 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()
  10. 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()
  11. 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()
  12. 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()
  13. 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
  14. 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() // ワンワン
  15. 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() } }
  16. 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 }
  17. 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() } } ... 略 ...
  18. Java: 多態 • 派生クラスは親クラスの部分型 ◦ 親クラスとして扱える ◦ 親クラスとしての振る舞いを持つ ◦ 親クラス型の変数に代入可能

    Dog dog; dog = new HotDog(2, 20); dog.Bark(); dog = new AmericanDog(3); dog.Bark();
  19. Go: interfaceによる多態 • interfaceを満たせばその型 ◦ その型振る舞いを持つ ◦ その型の変数に代入可能 ◦ 構造体埋め込みで自動的に満たせる

    • 別途interfaceを定義 • Javaより型制約がゆるい ◦ 移植する上では問題ない type IDog interface { Bark() } var dog IDog dog = NewHotDog(2, 20) dog.Bark() dog = NewAmericanDog(3) dog.Bark()
  20. クラスと継承のまとめ • クラスを構造体とレシーバ関数で表現 • 構造体の埋め込みで継承を真似る • 明示的な仮想関数テーブルでオーバーライドを実現 ◦ 関数フィールドを持つ方法 ◦

    インターフェイスをフィールドに持つ方法 • interfaceを使って多態を実現
  21. 例外

  22. 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){ // 何か処理 }
  23. 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 { // 何か処理 }
  24. Java: 例外種別による処理分け • 継承による階層構造 • catchで捕捉する例外型を指定 ◦ 捕捉しないものは更に上に伝播 try{ doSomething();

    } catch(IOException e){ // 何か処理 } catch(RuntimeException e){ // 何か処理 } Exception IOException RuntimeException EOFException SocketException
  25. 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
  26. 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: // 何か処理 }
  27. 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) { // 何か処理 } }
  28. 例外まとめ • interface継承による例外階層の模倣 ◦ Javaと同じ継承関係にできる • 型アサーションや型スイッチによる例外種別判定 ◦ 複数catchがある場合型スイッチが便利 ◦

    ラップされると判定できない • xerrors.As による例外種別判定 ◦ ラップされる場合はこちらを使う
  29. おしまい 質問やもっと良い書き方の提案などあればお願いします。