$30 off During Our Annual Pro Sale. View Details »

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

MakKi
July 13, 2019

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

Go Conference'19 Summer in Fukuoka

MakKi

July 13, 2019
Tweet

More Decks by MakKi

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. クラスと継承

    View Slide

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

    View Slide

  10. メソッドオーバーライドできない
    ● レシーバの型は元の型のまま
    ○ 埋め込まれた型のレシーバが呼ばれる
    ○ 親クラスからは自身のメソッドになる
    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()

    View Slide

  11. 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 {
    ...
    }
    }

    View Slide

  12. 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の実装
    派生クラス

    View Slide

  13. 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)
    ...
    }

    View Slide

  14. 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の呼び出し
    オーバーライドするメソッドを集めた
    インターフェイス(仮想関数テーブル)
    仮想関数テーブルの埋め込み
    初期化関数で実装を要求

    View Slide

  15. 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) {
    ...
    }

    View Slide

  16. 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実装
    派生クラス

    View Slide

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

    View Slide

  18. 例外

    View Slide

  19. 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
    }

    View Slide

  20. 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を追加

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  24. 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

    View Slide

  25. 型による例外種別判定
    ● 型アサーションによる判定
    ● 型スイッチによる判定
    ○ 複数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:
    // 何か処理
    }

    View Slide

  26. 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) {
    // 何か処理
    }
    }

    View Slide

  27. まとめ

    View Slide

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

    View Slide

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

    View Slide