やる気がストロングZERO

やる気のストロングスタイル

読み解きづらいコード

自分が「読み解きづらいな」と思ったコードに対して、なぜ読み解きづらいと思ったのかを考察し、
また、そうならないようにどうしたいかを考えたので書いてみる。

個人的な考えだし、エンジニアレベルとか状況とかで変わると思うし、僕が見えてない事情とかもあると思うけど、一旦そういうのは無視して書いてる。

フックベースで処理が組まれていて、フックの種類が多くネストが深い

イベント駆動みたいに「なんらかをフックして次の処理が実行される」みたいな機能をふんだんに使って処理を組まれていると、次なにが実行されるのかが全然わからない。

たとえばActive Recordは以下のコールバックがある。

before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit/after_rollback

before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
after_commit/after_rollback

before_destroy
around_destroy
after_destroy
after_commit/after_rollback

これらを全て確認しないと処理を追えない。
実行順序も意識しておく必要もある。

さらにそのコールバック処理の中で別のActive Recordが使われていたりすると、そのコールバックも把握して追っていかないといけない。。

これらのコールバックを駆使して組まれた処理は

someObject.save()って実行すると上手く処理されるけど、どういう処理が行われているのかよくわからん。処理を追うのも大変。。」

という感じになる。

対策:
フックに過剰に頼らない設計にする。(フックに向いている処理ってのはあると思う)

何が実行されるのかを外部が制御してる

「実行メソッド名が外部から渡ってきてて、そのメソッドを実行する」みたいなやつ。
実際に外部からデータもらわないと何が実行されるかわからん。

大体は「内部処理を外からも汎用的に使えるようにした」みたいなやつがこういう感じになってる気がする。

対策:
外部が内部の実行メソッド名を知っているという状態がそもそもおかしい。
何を実行するのかは内部で制御するようにする。

メソッドだけ見ても何をしているのかよくわからない

こんな感じのやつ。

calculator = New Calculator(user)
calculator.execute()
price = calculator.price

priceがどういうデータからどういう計算をされた値なのか全然わからん。
ユーザー情報を渡してなんらかの計算をしているっぽい雰囲気しかわからん。
executeメソッドの中身を見ると、データベースから色々データを取得して、なんらかの計算をするロジックが長々と書かれていたりする。

対策:
引数と返り値を使って、なんのデータがどういうデータになるのか解るように表現する。
不必要にに広範囲のデータを渡さないようにする。

単体で動かせない

コードリーディングで挙動を読み解けない場合、実行してみて動きを見たいが依存が多く動かせない。
巨大なダミーデータの作成が必要になって、どういうデータが必要なのかを理解するためにコードリーディングが必要になって、読み解けなくて、、

対策:
依存をできるだけ減らしていつでもどこでも単体で動かせるようにする。

実現したいことに対して実現するための構成要素がなんか多い

1つのことを実現するために5個くらいclassが存在したり、5個くらいテーブルがあって絡み合ってる。 その思想を読み解けず、なんのためにこれだけの要素があるのかわからないので処理が追いにくい。

対策:
それを実現するのに一番自然に思い浮かぶロジックをそのまま表現できるようなコードにする。
今必要ではない念の為の機能のために不要にコードを太らせないようにする。(使われてない機能だから余計読み解きにくい。。)

Goのエラーハンドリングについて

したいこと

なにかエラーが出た時に、ログから

  • 「なにが発生したのか」
  • 「どこで発生したのか?」

がわかるようにしたい。

Go標準のerrorはスタックトレース情報を持ってない

errors.New("エラーメッセージ")

これだとスタックトレース情報がない。

エラーが出た時下記のようにして上位レイヤーでエラーログ出力する感じになると思うが、 これだと「どこで発生したのが?」が追えない。

// レイヤーの下の方でエラーがあったとする。
_, err := someFunc()
if err != nil {
  return err // 呼び出し元に返される
}

// 上位レイヤーでキャッチしてログ出力
err := underLayer()
if err != nil {
  logger.Error(err) //loggerが出力ファイル位置を吐き出すようにしてたとしても、あくまでこの位置なのでエラーの発生箇所がわからない。
}

なので、
https://github.com/pkg/errors
を使うことにした。(errorと同じインターフェースで使えて、スタックトレース情報を付加してくれる。)

使い方の指針

自分で作るエラーの場合

標準のerrorsの使い方と同じ。

import "github.com/pkg/errors"

err := errors.New("エラーメッセージ")

// こうやるとスタックトレース情報がとれる
fmt.Printf("%+v", err)

goの既存関数とかLibraryの関数が返してくるエラーの場合

既存関数(僕らが触れない部分)が返してくるエラーは標準のerrorsなのでスタックトレース情報を持ってない。
だから、下記の様にスタックトレース情報を付加してやる。

import "github.com/pkg/errors"

err := someFunc()
if err != nil {
  return errors.WithStack(err) // 呼び出し元に返される
}

Goでdecimalを使う時に考えたこと(floatの精度について)

Goで金額計算するのでdecimal型を探したが、基本ライブラリには無いようなのでどうしようか悩んでいた。

基本ライブラリにmath.bigパッケージがあり、Floatの精度を自由に決められるようなものがあったのでそれで行けるかと思ってたけど(不必要に外部ライブラリを使いたくなかった)考え方が根本から間違ってたのでまとめる。

(結局はdecimal型を提供してくれるライブラリを採用した。 decimal - GoDoc

参考:
料率計算における小数の扱いについて
なぜBigDecimalを使わなければならないのか | Java好き

float型で精度をたっぷり用意すれば金額計算でも使える?

「float型は誤差が出る」というのは知ってたので金額計算は使えないという認識はあった。

でも基本ライブラリにdecimalが無いのでどうしようか悩んでる時、ふと
今回扱うのはせいぜい少数第2位くらいまでなので、じゃあ少数第10位くらいまでの「精度」を用意してやれば使えるんじゃね?

みたいに思った。

漠然とこんなイメージ

0.1 + 0.7 = 0.8000000012
で、少数第二位くらいで切り捨てればいいじゃん
答え:0.80

情報系の専門学校や大学を出てたら当然の話なのかもですけど、、
すみません。完全に間違ってました。

floatの精度とは?

floatでは10進数で表せない数値が存在する。
例えば0.01は2進数だと循環してしまうので、メモリに保持するにはどこかで切り捨てなければならない。
このとき「どこまでデータを保持して切り捨てるか」がfloatで言うところの精度となる。

だからどれだけ精度をあげても0.01は決して0.01としては扱えず、ごくわずかの誤差が入ってくる。

色んな所で切り捨てが起こり誤差は膨らむ一方。正確な計算は出来ない。

f1 = 0.1
f2 = 0.7
f3 = f1 + f2

f3 == 0.8 // false 

人の感覚的にはf3は0.8なのでtrueになるべきだが、 0.1と0.7の定数定義の時点で「切り捨て」が発生し、f3には2度の「切り捨て」が含まれた結果になっている。 なので0.8とは「切り捨て」の事情が異なる分だけ差が出ているのでfalseとなる。

最初の思惑「少数第二位くらいで切り捨てればいいじゃん」をやろうとすると、常にどこで「切り捨て」が発生するのかを意識して自分で切り捨て(もしくは切り上げ)を行い続けなくてはならず(他にも何を考慮すればいいのかさえよくわからない)完全に間違った策であることを理解した。

そもそもfloat型ってのは大きい数値を扱えるかわりに精度に関しては犠牲にするっていう型なのでキッチリした計算に使ってはいけないものだと言うことでした。

decimalはどうやって計算してんの?

0.01という数値は1(int)と小数点位置-2(int)をデータとして持って扱うので誤差が出ない。

【Go】gorpを使ってDDDのRepositoryを書いた

goを使ったシステムにて、DBアクセス周りでgorpを使うことにしたので色々調査してDDDで言うところのrepositoryを書いた。

gorp
GitHub - go-gorp/gorp: Go Relational Persistence - an ORM-ish library for Go

gorpを選んだ理由

データ取得は「SQL文を直接書く」というところが気に入った。
(insert,updateは構造体を渡せばやってくれる。)

僕はActiveRecord的なクエリビルダがあまり好きではない。

JOIN等を含んだちょっと複雑な取得条件になると、クエリビルダで表現するにはどうすればいいのかわからず調べなければならない事が多い。
ライブラリごとに方言があり例えばActiveRecordのクエリビルダをマスターしても、Djangoではその知識は使えないので別途学ぶ必要がある。
特に[or条件]は実装が難しいのか、どのライブラリも直感的ではないインターフェースになっている感じがする。

また保守時に「最終的にどういうSQLが発行されるのか」を追わなければならないシーンも多い。

対して「SQL文を直接書けばよい」であればすぐ書けるし、どういうSQLが実行されるのかもコードからすぐわかる。
最初からSQL文を使いたいと思うようになってしまった。

DDDのrepositoryをgorpを使って書いたらこんな感じになった。

gorpの初期化関数

// gorp初期化処理
func OpenDb() (*gorp.DbMap, error) {
   dbUserName := "DB_USERNAME"
   dbPassWord := "DB_PASSWORD"
   dbHost := "DB_HOST"
   dbPort := "DB_PORT"
   dbDbName := "DB_DBNAME"
   db, err := sql.Open("mysql", fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?parseTime=true", dbUserName, dbPassWord, dbHost, dbPort, dbDbName))
   if err != nil {
      return nil, err
   }

   dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}}

   dbmap.AddTableWithName(entities.User{}, "user").SetKeys(true, "Id")

   dbmap.CreateTablesIfNotExists()
   dbmap.TraceOn("[gorp]", log.New(os.Stdout, "myApp:", log.Lmicroseconds))

   return dbmap, nil
}

Userエンティティ

type User struct {
   Id         int64     `db:"id, primarykey, autoincrement"`
   UserName   string    `db:"username, notnull"`
   CreateTime time.Time `db:"create_time, notnull"`
   UpdateTime time.Time `db:"update_time, notnull"`
}

repository

type UserDb struct {}

// コンストラクタ
func NewUserDb() *UserDb {
   return &UserDb{}
}

// User構造体を受け取ってuserテーブルに保存する
func (r *UserDb) Save(user *User, executor gorp.SqlExecutor) error {
   user.CreateTime = time.Now()
   user.UpdateTime = time.Now()
   err := executor.Insert(user)
   if err != nil {
      return err
   }
   return nil
}

// idでuserデータを取得
func (r *UserDb) GetById(id int64, executor gorp.SqlExecutor) *User {
   user := &User{}
   err := executor.SelectOne(user, "select * from `user` where id = ?", id)
   if err != nil {
      return err
   }
   return user
}

保存処理

func someFunc() error {
   db, err := OpenDb()
   if err != nil {
      return err // 成否チェック
   }
   defer db.Db.Close() // 関数終了時にClose処理するために記載
   userRepository := NewUserDb()

   user1 := &User{UserName: "テスト太郎"}
   err = userRepository.Save(user1, db) // dbコネクションを渡して保存。ここはトランザクションの範囲外
   if err != nil {
       return err // 成否チェック
   }

   tx, err := db.Begin() // トランザクション開始
   if err != nil {
      return err // 成否チェック
   }
   defer tx.Rollback() // コミットされていれば実行されても何もされない(内部的には「既にトランザクションが閉じています」的なエラーがでてる)。コミットされてなければRollbackされる。

   user2 := &User{UserName: "テスト次郎"}
   err = userRepository.Save(user2, tx) // トランザクションを渡して保存
   if err != nil {
      return err
   }
   tx.Commit() // 問題なければコミット
   return nil
}

気になった点

トランザクション分離レベルが指定できない

gorpはトランザクション開始時にトランザクション分離レベルを指定することができない。
DB側のデフォルト設定が適用されることになる。
※内部的には設定を渡せる部分があるが、かならずnilが渡されている。

楽観的同時実行制御の機能があるので問題ないのかもしれない。
https://github.com/go-gorp/gorp#optimistic-locking

rollbackの書き方

gorpというよりgo言語の問題かもしれないが、例外機能が無いので「例外をキャッチしたらrollback」というような書き方ができない。

色々悩んだ結果、上記「保存処理」のコードのように

   tx, err := db.Begin() // トランザクション開始
   if err != nil {
      return err // 成否チェック
   }
   defer tx.Rollback() // コミットされていれば実行されても何もされない(内部的には「既にトランザクションが閉じています」的なエラーがでてる)。コミットされてなければRollbackされる。

と書いた。

gorpのRollback()はトランザクションがcloseしているとsql.ErrTxDoneを返してくるだけで何もしない。
だから、Commitされていなければ(途中でエラーが出たとか、Commitし忘れていたとか)Rollbackされるし、
Commitされていればsql.ErrTxDoneを返して何もされない。

以下疑問が浮かんだが問題なさそうだった。

・ 「Rollbackが失敗していた場合どうなる?」

Rollbackが失敗したところで、Commitしていなければ変更は反映されないので問題なさそう。
※となると明示的にRollback処理書かなくてもいいのかな。。?

・ 「Rollbackが失敗し、次のトランザクションが開始されCommitされた場合、RollbackされるべきデータがDBへ反映されてしまわないか?」

使うトランザクションオブジェクトが異なるので影響しない。問題なさそう。

【Go】インターフェースから具象型への型アサーション

インターフェースに依存させてコードを書いてると、テストで具象型として扱いたいケースが出てきたので方法を調べた。

参考)
A Tour of Go

value := <インターフェース型の変数>.(<具象型>)
value, ok := <インターフェース型の変数>.(<具象型>) // この場合okには成否が入る。

呼び方がいまいちわかってないが、

  • [具象型 => 別の具象型(intからfloatとか)]:キャスト
  • [インターフェース => 具象型]:型アサーション

という感じなのかな?

サンプルケース

Human構造体は言葉を覚えるMemoriseメソッドを持っている。

type Human struct {
}
func (h *Human) Memorise(word string) {
   // 覚える処理
}

本番運用時はDBに言葉を記録するが、テスト時はインメモリに保存したいので、
以下のようにPersistenceインターフェースを用意して、DIで切り替えられるようにした。

type Human struct {
   persistence Persistence
}
func (h *Human) Memorise(word string) {
   h.persistence.Save(word)
}

// インターフェース
type Persistence interface {
   Save(word string)
}


// Dbに保存するPersistence
type DbPersistence struct {
   // なんかdbに保存するためのプロパティなど
}
func (p *DbPersistence) Save(word string) {
   // dbに保存するための処理
}

// memoryに保存するPersistence テスト用
type MemoryPersistence struct {
   memory []string
}
func (p *MemoryPersistence) Save(word string) {
   p.memory = append(p.memory, word)
}

Memoriseテストの実行時、言葉を覚えているかをチェックするため以下のようにmemoryの内部を確認したい。
こんな感じで型アサーションする。

func Test_SomeTest(t *testing.T) {
   masao := Human{
      persistence: &MemoryPersistence{},
   }
   masao.Memorise("人に厳しく自分に甘く")

   // persisitenceはPersistenceインターフェース型だからmemoryにアクセスできない。コンパイルエラーになる
   if masao.persistence.memory[0] != "人に厳しく自分に甘く"{ 
    t.Errorf("正しく覚えてない")
   }

   // こうやって型アサーションしてMemoryPersistence型として扱えばmemoryにアクセスできる。
   memoryPersistence := masao.persistence.(*MemoryPersistence)
   if memoryPersistence.memory[0] != "人に厳しく自分に甘く" {
      t.Errorf("正しく覚えてない")
   }
}

docker-compose.ymlをDRYに書く

参考) docker-compose で複数環境を構築するときの設定をなるべく DRY に書く - ikasama over technology

ここに書かれてる「修羅の道」をやってしまってた。

DRYに書くには以下の様にするらしい。

  • 共通docker-compose.yml(これだけで基本動く様にする)
  • 差分環境docker-compose.yml(共通部分の変更したい部分だけのyml。上書きされる)
$ docker-compose -f 共通docker-compose.yml -f 差分環境docker-compose.yml -p 環境名 up -d

最終的にどんな設定になるのかは、 docker-compose config コマンドを使うと見れるとのこと。

$ docker-compose -f 共通docker-compose.yml -f 差分環境docker-compose.yml -p 環境名 config

-pオプション

docker-compose upするとできるコンテナのプレフィックス名を指定できる感じ。
共通環境と差分環境の両方をupしたい場合など、これをそれぞれ別名で指定しないと、衝突して後勝ちになる。

activeresource-responseを入れたら既存挙動が変わってバグった

経緯

activeresourceでデータ取得するロジックがあった。

user = User.find(1)
data = user.some_data # apiアクセスを実行してデータを取得

dataがnilだったら〜〜という処理があった

# data == nilのとき、resultには"データはありませんでした"が入る想定。
result = data || "データありませんでした"

ある日、httpヘッダ情報が必要になったがactiveresourceでは取得できないのでググって見つかったgem(activeresource-response)を入れて対応した。

user = User.find(1)
some_header = user.http_response["some_header"]

すると、既存ロジックの挙動が変わった。

user = User.find(1)
data = user.some_data # apiアクセスを実行してデータを取得

# data == nilのとき、resultにはnilが入るようになってしまった。
result = data || "データありませんでした"

原因

返却値dataのclassが変わっていた。
(多分返却データにヘッダ情報を取得するメソッドを追加するため。)

gemを入れる前

user = User.find(1)
data = user.some_data
data.class # NilClass
data == nil # true
data.nil? # true
data # nil
result = data || "データがありませんでした" # resultには"データがありませんでした"が入る

gemを入れた後

user = User.find(1)
data = user.some_data
data.class # SimpleDelegatorになってた
data == nil # true
data.nil? # false
data # nil
result = data || "データがありませんでした" # resultにはnilが入る

まとめ

既存オブジェクトに対して知らないところで処理を追加するようなgemはやっぱ怖いと思った。