Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

会社紹介 KLab(くらぶ)株式会社 ● 今日は休日出勤扱い(旅費・宿泊費も会社もち) ● モバイルオンラインゲームの開発運用 ● 福岡オフィスあります(KBCビル) ● KLab福岡Meetup Twitter:@KLab_KFM Go言語の活用事例 ● オンライン対戦同期サーバ ● MMORPGのゲームサーバ ● インフラ管理系ツール ● その他細かいツール類やslackbotなど

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

今日のお話 ● Java製1D/2Dバーコードライブラリ「ZXing」をGoに移植しました ○ https://github.com/makiuchi-d/gozxing ● 元のJavaコードとGoのコードを見比べながら 実際に使っているテクニックをいくつか紹介します ○ クラスと継承 ■ メソッドオーバーライド ○ 例外 ■ 例外型の階層構造 ■ 例外種別判定

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

クラスと継承

Slide 9

Slide 9 text

Goに移植するときのポイント ● 構造体とレシーバ関数でクラスを表現 ● 構造体埋め込みで継承っぽい動作 ● Goではメソッドオーバーライドができない

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Java: OneDReader, Code128Readerの実装 public abstract class OneDReader implements Reader { @Override public Result decode(BinaryBitmap image, 略) throws NotFoundException, FormatException { ... Result result = decodeRow(rowNumber, row, hints); ... } public abstract Result decodeRow(int rowNumber, BitArray row, Map hints) throws NotFoundException, ChecksumException, FormatException; } public final class Code128Reader extends OneDReader { @Override public Result decodeRow(int rowNumber, BitArray row, Map hints) throws NotFoundException, ChecksumException, FormatException { ... } }

Slide 12

Slide 12 text

Java: OneDReader, Code128Readerの実装 public abstract class OneDReader implements Reader { @Override public Result decode(BinaryBitmap image, 略) throws NotFoundException, FormatException { ... Result result = decodeRow(rowNumber, row, hints); ... } public abstract Result decodeRow(int rowNumber, BitArray row, Map hints) throws NotFoundException, ChecksumException, FormatException; } public final class Code128Reader extends OneDReader { @Override public Result decodeRow(int rowNumber, BitArray row, Map hints) throws NotFoundException, ChecksumException, FormatException { ... } } Readerインターフェイスの実装 派生クラスのdecodeRow()呼び出し 派生クラスでdeocdeRowの実装を要求 派生クラスでのdecodeRowの実装 派生クラス

Slide 13

Slide 13 text

Go: OneDReader, Code128Readerの実装 type RowDecoder interface { DecodeRow(rowNumber int, row *gozxing.BitArray, 略) (*gozxing.Result, error) } type OneDReader struct { RowDecoder } func NewOneDReader(rowDecoder RowDecoder) *OneDReader { return &OneDReader{rowDecoder} } func (this *OneDReader) Decode(image *gozxing.BinaryBitmap, 略) (*gozxing.Result, error) { ... result, e := this.DecodeRow(rowNumber, row, hints) ... }

Slide 14

Slide 14 text

Go: OneDReader, Code128Readerの実装 type RowDecoder interface { DecodeRow(rowNumber int, row *gozxing.BitArray, 略) (*gozxing.Result, error) } type OneDReader struct { RowDecoder } func NewOneDReader(rowDecoder RowDecoder) *OneDReader { return &OneDReader{rowDecoder} } func (this *OneDReader) Decode(image *gozxing.BinaryBitmap, 略) (*gozxing.Result, error) { ... result, e := this.DecodeRow(rowNumber, row, hints) ... } Readerインターフェイスの実装 埋め込んだRowDecoderのDecodeRowの呼び出し オーバーライドするメソッドを集めた インターフェイス(仮想関数テーブル) 仮想関数テーブルの埋め込み 初期化関数で実装を要求

Slide 15

Slide 15 text

Go: OneDReader, Code128Readerの実装 type RowDecoder interface { DecodeRow(rowNumber int, row *gozxing.BitArray, 略) (*gozxing.Result, error) } type OneDReader struct { RowDecoder } func NewOneDReader(rowDecoder RowDecoder) *OneDReader { return &OneDReader{rowDecoder} } func (this *OneDReader) Decode(image *gozxing.BinaryBitmap, 略) (*gozxing.Result, error) { ... result, e := this.DecodeRow(rowNumber, row, hints) ... } Readerインターフェイスの実装 埋め込んだRowDecoderのDecodeRowの呼び出し オーバーライドするメソッドを集めた インターフェイス(仮想関数テーブル) 仮想関数テーブルの埋め込み 初期化関数で実装を要求 type code128Reader struct { *OneDReader } func NewCode128Reader() gozxing.Reader { this := &code128Reader{} this.OneDReader = NewOneDReader(this) return this } func (*code128Reader) DecodeRow(略) (*gozxing.Result, error) { ... }

Slide 16

Slide 16 text

Go: OneDReader, Code128Readerの実装 type RowDecoder interface { DecodeRow(rowNumber int, row *gozxing.BitArray, 略) (*gozxing.Result, error) } type OneDReader struct { RowDecoder } func NewOneDReader(rowDecoder RowDecoder) *OneDReader { return &OneDReader{rowDecoder} } func (this *OneDReader) Decode(image *gozxing.BinaryBitmap, 略) (*gozxing.Result, error) { ... result, e := this.DecodeRow(rowNumber, row, hints) ... } Readerインターフェイスの実装 埋め込んだRowDecoderのDecodeRowの呼び出し オーバーライドするメソッドを集めた インターフェイス(仮想関数テーブル) 仮想関数テーブルの埋め込み 初期化関数で実装を要求 type code128Reader struct { *OneDReader } func NewCode128Reader() gozxing.Reader { this := &code128Reader{} this.OneDReader = NewOneDReader(this) return this } func (*code128Reader) DecodeRow(略) (*gozxing.Result, error) { ... } 継承元を埋め込み 仮想関数テーブルとして自身を渡す 派生クラスのDecodeRow実装 派生クラス

Slide 17

Slide 17 text

メソッドオーバーライドのテクニック ● 継承元構造体 ○ 仮想関数テーブルとなるインターフェイスを持つ ■ 派生構造体のメソッドを呼び出せる ○ 初期化関数で実装を要求できる ● 派生構造体 ○ 継承元構造体を埋め込む ■ 継承元のフィールドやメソッドを利用できる ○ 継承元構造体の初期化関数に自身を渡す ■ メソッドオーバーライド

Slide 18

Slide 18 text

例外

Slide 19

Slide 19 text

JavaからGoへの書き換え try { alignmentPattern = findAlignmentInRegion(moduleSize, estAlignmentX, estAlignmentY, i); break; } catch (NotFoundException e) { // try next round } alignmentPattern, e = this.findAlignmentInRegion(moduleSize, estAlignmentX, estAlignmentY, i) if e == nil { break } if _, ok := e.(gozxing.NotFoundException); !ok { return nil, e }

Slide 20

Slide 20 text

JavaからGoへの書き換え try { alignmentPattern = findAlignmentInRegion(moduleSize, estAlignmentX, estAlignmentY, i); break; } catch (NotFoundException e) { // try next round } alignmentPattern, e = this.findAlignmentInRegion(moduleSize, estAlignmentX, estAlignmentY, i) if e == nil { break } if _, ok := e.(gozxing.NotFoundException); !ok { return nil, e } NotFoundExceptionだけ握りつぶす(それ以外は送出) NotFoundException以外を送出 例外非発生でbreak (for文の中) nilチェックしてOKならbreak 戻り値にerrorを追加

Slide 21

Slide 21 text

JavaからGoへの書き換え try { ... } catch (FormatException | ChecksumException e) { // 特定の例外種別専用の処理 ... } ... switch e.(type) { case nil: // break case gozxing.FormatException, gozxing.ChecksumException: // 特定の例外種別専用の処理 default: return nil, e }

Slide 22

Slide 22 text

JavaからGoへの書き換え try { ... } catch (FormatException | ChecksumException e) { // 特定の例外種別専用の処理 ... } ... switch e.(type) { case nil: // break case gozxing.FormatException, gozxing.ChecksumException: // 特定の例外種別専用の処理 default: return nil, e } 2種類の例外だけ捕捉して特別処理 2種類の例外だけ捕捉して特別処理 他の例外は明示的に送出 例外非発生なら明示的になにもしない

Slide 23

Slide 23 text

Goに移植するときのポイント ● 例外はthrowではなく戻り値として送出する ○ error型 ○ 例外を投げうるメソッドにはすべて戻り値を足す ○ 呼び出し側でnilチェック ● 型による例外種別判定 ○ 型アサーション、タイプスイッチ ○ 元のJavaの設計そのまま ● 例外の階層的なグルーピング ○ 継承による型の階層関係

Slide 24

Slide 24 text

interface継承による例外型の階層関係の模倣 ● Javaでの継承関係と同じにする ○ errorを継承してerrorとしても扱う ● 実体はerrorを埋め込んだ構造体 type Exception interface { error exception() } type ReaderException interface { Exception readerException() } type NotFoundException interface { ReaderException notFoundException() } type notFoundException struct { error } func (_ notFoundException) exception() {} func (_ notFoundException) readerException() {} func (_ notFoundException) notFoundException(){} Exception ReaderException WriterException NotFoundException FormatException

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

まとめ

Slide 28

Slide 28 text

今日紹介したテクニック ● クラスと継承 ○ メソッドオーバーライドの模倣 ■ 仮想関数テーブルインターフェイス ● 例外機構 ○ 例外型の階層関係の模倣 ■ インターフェイス継承 ○ 型による例外種別判定 ■ 型アサーションやタイプスイッチ ■ xerrors.As関数

Slide 29

Slide 29 text

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