Shimane.go#03 Lightning Talk

継承できないなら
注入すればいいじゃない!

Presented by Spiegel, 2020

Licensed under  

C/C++, Java など制御系言語が得意

ただし,現在は無期休業でその日暮らし中

Go 言語は趣味の範囲内で

趣味のプロダクト

gpgpdump
OpenPGP packet visualizer

pa-api
Client Side API for Amazon PA-APIv5

では,本編へ

オブジェクト指向とは

「オブジェクト」を定義し

オブジェクト間の関係とメッセージングを
記述すること

「関係」のひとつが

汎化(Generalization)

「汎化」は is-a 関係とも呼ばれる

Generalization

Serval is a “Friends”.

Raccoon is a “Friends”.

Fennec is a “Friends”.

Go では「汎化」の記述に
ふたつの手段がある

Interface 型

埋め込みフィールド

Interface 型

package io

// Reader is the interface
// that wraps the basic Read method.
type Reader interface {
    Read(p []byte) (n int, err error)
}

Interface 型は
「実現(Realization)」の一種で

Realization

「ふるまい」のみを定義する

埋め込みフィールド

type Human struct { ... }

type Student struct {
    Human // embedded field
    specialty string
}

埋め込みフィールドは
「委譲(Delegation)」として機能する

Delegation

「委譲」では
メソッドのオーバーライドはできない

このように
Go で「実現」や「委譲」は記述できるが

「継承(Inheritance)」は
記述できない

たとえば Go では
Factory Method パターンを構成できない

Factory Method Pattern

(親メンバをオーバーライドできないため)

「継承」がないなら
どうすればいいの
???

「継承」できないなら
「注入」すればいいじゃない!

「継承」によるオーバーライドではなく

Dependency Injection

具象化したインスタンスを注入する

Go プログラマは息をするように

依存オブジェクトを注入
(Dependency Injection)

する

「依存の注入」の最たるものが
error ハンドリング

// The error built-in interface type
type error interface {
    Error() string
}

Error() メソッドを有するオブジェクトは
全て error 型として機能する。

つまり

あらゆるエラーは Error() メソッドによって
error 型に集約できる

file, err := os.Open(path)
if err != nil {
    fmt.Fprintln(os.Stderr, err)
    return err
}
defer file.Close()

別の使い方としては
errors.Unwrap() 関数のように

func Unwrap(err error) error {
    u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return u.Unwrap()
}

必要に応じて必要なメソッド だけ
その場で抽出することもできる

(他の機能については知らないフリをする)

もう一例
(自作プロダクトの例)

コマンドライン・インタフェースではデータの入出力に標準入出力を使うことが多いが

ビジネスロジックの中で標準入出力を直接使うとテストし辛くなる

そこで以下のパッケージを定義し

package rwi

type RWI struct {
    reader      io.Reader
    writer      io.Writer
    errorWriter io.Writer
}

標準入出力を io.Reader, io.Writer
抽象化した上で
コンテキスト情報として渡す

func Execute(ui *rwi.RWI, args []string) exitcode.ExitCode {
    /* main logic */
    return exitcode.Normal
}

これで main() 関数はこんな感じになる

func main() {
    Execute(
        rwi.New(
            rwi.WithReader(os.Stdin),
            rwi.WithWriter(os.Stdout),
            rwi.WithErrorWriter(os.Stderr),
        ),
        os.Args[1:],
    ).Exit()
}

テスト・コードについては以下のように書ける

outBuf := &bytes.Buffer{}
outErrBuf := &bytes.Buffer{}
res := Execute(
    rwi.New(
        rwi.WithWriter(outBuf),
        rwi.WithErrorWriter(outErrBuf)
    ),
    []string{"-v"},
)
...

テスト条件を切り分けるため
インタフェース部分を抽象化することは
割と使われる手法だが

「依存の注入」を使えばテスト条件の切り分けが
比較的簡単にできる

そもそも何故「汎化」関係が
要求されるのか?

オブジェクト指向の要求定義では
あらゆる「オブジェクト」を列挙し
分類・類型化する

分類・類型化のためには
「汎化」関係は必須の要件

でも

要求定義に於いて
列挙することなく
分類・類型化する人はいない

たとえば

最初に「猫」を定義してから
机器猫,猫耳メイド,サーバルキャット…
と派生するのではなく

まず机器猫,猫耳メイド,サーバルキャット…
と列挙して

これ全部『にゃ〜ん』って鳴くやん!
と類型化して はじめて

「『にゃ〜ん』と鳴く猫」を定義できる

Go なら具体例の列挙からはじめて
Interface 型や埋め込みフィールドを使って
記述しながら分類・類型化を進めていける

つまり

Go によるプログラミングは
人間の思考に近い

C/C++ や Java など
「継承」を主軸にした言語では

抽象 → 具象

の順で記述するため
本来の人間の思考とは逆向きになる

C/C++ や Java の時代に
このギャップがあまり問題にならなかったのは
これらの言語が成立した時代が
「ウォーターフォール型」全盛期だったから

(憶測)

「ウォーターフォール型」開発では

要求定義 → 設計 → プログラミング

の各フェーズが完了しないと次に進めないので
プログラミングを開始している時点で
設計を完了している

ので安心して 抽象 → 具象 へと書き進められる
(建前)

しかし実際には
要求定義で全ての要求を過不足なく
出し尽くすのは難しい

しかも要求は常に変化する

「ウォーターフォール型」開発に於いて
要求の「抜け」や「変化」は
高いコストで跳ね返ってくる

そこで提案

Go プログラマは
要求定義から参加して
積極的にコードを書くべし

もちろん,このやり方は
書き直しやリファクタリングが
頻繁に発生する

しかし

安心してください

Go
リファクタリングに厚い言語

実は!

Interface 型や埋め込みフィールドは
リファクタリングに於いて
もっとも威力を発揮する

以下は埋め込みフィールドを使って
既存パッケージを再定義する例

type Time struct { time.Time }

func (t *Time) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    var v string
    d.DecodeElement(&v, &start)
    tm, _ := time.Parse(time.RFC3339, v)
    *t = Time{tm}
    return nil
}

さらに type alias のような
リファクタリング専用機能まである

type Cat struct{}

func (c Cat) Say() string {return "たーのしー!"}

type Serval = Cat

func main() {
    fmt.Println((Serval{}).Say())
}

プログラミングは
書くば書くほど読めば読むほど
理解が深まり上達します

積極的に書き直しましょう

以上,ありがとうございました

ネタ元(自ブログより)

slide.Baldanders.info

Powered by

Hugo and reveal-hugo