Shimane.go#03 Lightning Talk
Presented by Spiegel, 2020
C/C++, Java など制御系言語が得意
ただし,現在は無期休業でその日暮らし中
Go 言語は趣味の範囲内で
gpgpdump
OpenPGP packet visualizer
pa-api
Client Side API for Amazon PA-APIv5
「オブジェクト」を定義し
オブジェクト間の関係とメッセージングを
記述すること
「関係」のひとつが
「汎化」は is-a 関係とも呼ばれる
Serval is a “Friends”.
Raccoon is a “Friends”.
Fennec is a “Friends”.
package io
// Reader is the interface
// that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
Interface 型は
「実現(Realization)」の一種で
「ふるまい」のみを定義する
type Human struct { ... }
type Student struct {
Human // embedded field
specialty string
}
埋め込みフィールドは
「委譲(Delegation)」として機能する
「委譲」では
メソッドのオーバーライドはできない
このように
Go で「実現」や「委譲」は記述できるが
たとえば Go では
Factory Method パターンを構成できない
(親メンバをオーバーライドできないため)
「継承」によるオーバーライドではなく
具象化したインスタンスを注入する
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 型や埋め込みフィールドを使って
記述しながら分類・類型化を進めていける
つまり
C/C++ や Java など
「継承」を主軸にした言語では
抽象 → 具象
の順で記述するため
本来の人間の思考とは逆向きになる
C/C++ や Java の時代に
このギャップがあまり問題にならなかったのは
これらの言語が成立した時代が
「ウォーターフォール型」全盛期だったから
(憶測)
「ウォーターフォール型」開発では
要求定義 → 設計 → プログラミング
の各フェーズが完了しないと次に進めないので
プログラミングを開始している時点で
設計を完了している
ので安心して 抽象 → 具象 へと書き進められる
(建前)
しかし実際には
要求定義で全ての要求を過不足なく
出し尽くすのは難しい
しかも要求は常に変化する
「ウォーターフォール型」開発に於いて
要求の「抜け」や「変化」は
高いコストで跳ね返ってくる
もちろん,このやり方は
書き直しやリファクタリングが
頻繁に発生する
しかし
実は!
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())
}
以上,ありがとうございました
ネタ元(自ブログより)
Powered by
Hugo and reveal-hugo