Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

MakKi
May 18, 2019

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

Go Conference 2019 Spring

MakKi

May 18, 2019
Tweet

More Decks by MakKi

Other Decks in Programming

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. おしまい 質問やもっと良い書き方の提案などあればお願いします。