IO、並び替え
IO and sorting
私たちの製品の所有者は、サーバーが再起動されたときにソフトウェアがスコアを失うことにいくらか混乱しています。これ は、ストアの実装がインメモリであるためです。彼女はまた、「
/league
」エンドポイントが勝ちの数で順序付けられたプレーヤーを返す必要があると解釈しなかったことに不満を持っています!// server.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
// PlayerStore stores score information about players
type PlayerStore interface {
GetPlayerScore(name string) int
RecordWin(name string)
GetLeague() []Player
}
// Player stores a name with a number of wins
type Player struct {
Name string
Wins int
}
// PlayerServer is a HTTP interface for player information
type PlayerServer struct {
store PlayerStore
http.Handler
}
const jsonContentType = "application/json"
// NewPlayerServer creates a PlayerServer with routing configured
func NewPlayerServer(store PlayerStore) *PlayerServer {
p := new(PlayerServer)
p.store = store
router := http.NewServeMux()
router.Handle("/league", http.HandlerFunc(p.leagueHandler))
router.Handle("/players/", http.HandlerFunc(p.playersHandler))
p.Handler = router
return p
}
func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", jsonContentType)
json.NewEncoder(w).Encode(p.store.GetLeague())
}
func (p *PlayerServer) playersHandler(w http.ResponseWriter, r *http.Request) {
player := strings.TrimPrefix(r.URL.Path, "/players/")
switch r.Method {
case http.MethodPost:
p.processWin(w, player)
case http.MethodGet:
p.showScore(w, player)
}
}
func (p *PlayerServer) showScore(w http.ResponseWriter, player string) {
score := p.store.GetPlayerScore(player)
if score == 0 {
w.WriteHeader(http.StatusNotFound)
}
fmt.Fprint(w, score)
}
func (p *PlayerServer) processWin(w http.ResponseWriter, player string) {
p.store.RecordWin(player)
w.WriteHeader(http.StatusAccepted)
}
// in_memory_player_store.go
package main
func NewInMemoryPlayerStore() *InMemoryPlayerStore {
return &InMemoryPlayerStore{map[string]int{}}
}
type InMemoryPlayerStore struct {
store map[string]int
}
func (i *InMemoryPlayerStore) GetLeague() []Player {
var league []Player
for name, wins := range i.store {
league = append(league, Player{name, wins})
}
return league
}
func (i *InMemoryPlayerStore) RecordWin(name string) {
i.store[name]++
}
func (i *InMemoryPlayerStore) GetPlayerScore(name string) int {
return i.store[name]
}
// main.go
package main
import (
"log"
"net/http"
)
func main() {
server := NewPlayerServer(NewInMemoryPlayerStore())
log.Fatal(http.ListenAndServe(":5000", server))
}
章の上部にあるリンクで対応するテストを見つけることができます。
これを使用できるデータベースは数十ありますが、ここでは非常にシンプルなアプローチを採用します。このアプリケーションのデータをJSONとしてファイルに保存します。
これにより、データの移植性が非常に高くなり、実装が比較的簡単になります。
特にうまくスケーリングしませんが、これはプロトタイプなので、今のところは問題ありません。私たちの状況が変化し、それが適切でなくなった場合、私たちが使用した
PlayerStore
抽象化のため、それを別のものと交換するのは簡単です。新しいストアを開発するときに統合テストに合格し続けるために、今のところ
InMemoryPlayerStore
を保持します。新しい実装が統合テストに合格するのに十分であると確信したら、それを入れ替えてから、InMemoryPlayerStore
を削除します。これで、データの読み取り(
io.Reader
)、データの書き込み(io.Writer
)、および標準ライブラリを使用してこれらの関数をテストすることなく標準ライブラリをテストする方法について、標準ライブラリに関連するインターフェースに精通しているはずです。実際のファイルを使用する必要があります。この作業を完了するには、
PlayerStore
を実装する必要があるため、実装する必要のあるメソッドを呼び出すストアのテストを記述します。GetLeague
から始めます。//file_system_store_test.go
func TestFileSystemStore(t *testing.T) {
t.Run("league from a reader", func(t *testing.T) {
database := strings.NewReader(`[
{"Name": "Cleo", "Wins": 10},
{"Name": "Chris", "Wins": 33}]`)
store := FileSystemPlayerStore{database}
got := store.GetLeague()
want := []Player{
{"Cleo", 10},
{"Chris", 33},
}
assertLeague(t, got, want)
})
}
私たちは
Reader
を返すstrings.NewReader
を使用しています。 これは、FileSystemPlayerStore
がデータを読み取るために使用するものです。main
では、Reader
でもあるファイルを開きます。# github.com/quii/learn-go-with-tests/io/v1
./file_system_store_test.go:15:12: undefined: FileSystemPlayerStore
新しいファイルで
FileSystemPlayerStore
を定義しましょう//file_system_store.go
type FileSystemPlayerStore struct{}
再試行
# github.com/quii/learn-go-with-tests/io/v1
./file_system_store_test.go:15:28: too many values in struct initializer
./file_system_store_test.go:17:15: store.GetLeague undefined (type FileSystemPlayerStore has no field or method GetLeague)
Reader
を渡していますが、予期しておらず、まだGetLeague
が定義されていないため、問題があります。//file_system_store.go
type FileSystemPlayerStore struct {
database io.Reader
}
func (f *FileSystemPlayerStore) GetLeague() []Player {
return nil
}
もう一回試してみる...
=== RUN TestFileSystemStore//league_from_a_reader
--- FAIL: TestFileSystemStore//league_from_a_reader (0.00s)
file_system_store_test.go:24: got [] want [{Cleo 10} {Chris 33}]
以前にリーダーからJSONを読み取りました
//file_system_store.go
func (f *FileSystemPlayerStore) GetLeague() []Player {
var league []Player
json.NewDecoder(f.database).Decode(&league)
return league
}
テストは成功するはずです。
これは以前に行ったことです! サーバーのテストコードは、応答からJSONをデコードする必要がありました。
この関数をDRYにしてみましょう。
league.go
と呼ばれる新しいファイルを作成し、これを中に入れます。//league.go
func NewLeague(rdr io.Reader) ([]Player, error) {
var league []Player
err := json.NewDecoder(rdr).Decode(&league)
if err != nil {
err = fmt.Errorf("problem parsing league, %v", err)
}
return league, err
}
これを実装と、
server_test.go
のテストヘルパーgetLeagueFromResponse
で呼び出します//file_system_store.go
func (f *FileSystemPlayerStore) GetLeague() []Player {
league, _ := NewLeague(f.database)
return league
}
構文解析エラーに対処するための戦略はまだありませんが、続けましょう。
私たちの実装に欠陥があります。まず、
io.Reader
の定義を思い出してみましょう。type Reader interface {
Read(p []byte) (n int, err error)
}
私たちのファイルを使用すると、最後までバイト単位で読み取ることを想像できます。 もう一度「読み取り
Read
」しようとするとどうなりますか?現在のテストの最後に以下を追加します。
//file_system_store_test.go
// read again
got := store.GetLeague()
assertLeague(t, got, want)
これは成功させたいのですが、テストを実行すると成功しません。
問題は、
Reader
が最後に到達したため、これ以上読むものがないことです。最初に戻るように指示する方法が必要です。type ReadSeeker interface {
Reader
Seeker
}
埋め込みを覚えていますか?これは、
Reader
と[Seeker
](https://golang.org/pkg/io/#Seeker)で構成されるインターフェースです。type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
これはいいですね、代わりにこのインターフェイスを取るように
FileSystemPlayerStore
を変更できますか?//file_system_store.go
type FileSystemPlayerStore struct {
database io.ReadSeeker
}
func (f *FileSystemPlayerStore) GetLeague() []Player {
f.database.Seek(0, 0)
league, _ := NewLeague(f.database)
return league
}
テストを実行してみてください。テストは成功しました! テストで使用した
string.NewReader
は、ReadSeeker
も実装しているため、他の変更を行う必要はありませんでした。次に、
GetPlayerScore
を実装します。//file_system_store_test.go
t.Run("get player score", func(t *testing.T) {
database := strings.NewReader(`[
{"Name": "Cleo", "Wins": 10},
{"Name": "Chris", "Wins": 33}]`)
store := FileSystemPlayerStore{database}
got := store.GetPlayerScore("Chris")
want := 33
if got != want {
t.Errorf("got %d want %d", got, want)
}
})
./file_system_store_test.go:38:15: store.GetPlayerScore undefined (type FileSystemPlayerStore has no field or method GetPlayerScore)
テストをコンパイルするには、メソッドを新しい型に追加する必要があります。
//file_system_store.go
func (f *FileSystemPlayerStore) GetPlayerScore(name string) int {
return 0
}
これでコンパイルされ、テストは失敗します
=== RUN TestFileSystemStore/get_player_score
--- FAIL: TestFileSystemStore//get_player_score (0.00s)
file_system_store_test.go:43: got 0 want 33
リーグを繰り返してプレーヤーを見つけ、スコアを返すことができます。
//file_system_store.go
func (f *FileSystemPlayerStore) GetPlayerScore(name string) int {
var wins int
for _, player := range f.GetLeague() {
if player.Name == name {
wins = player.Wins
break
}
}
return wins
}
何十ものテストヘルパーリファクタリングを確認したので、これを機能させるためにこれをあなたにお任せします。
//file_system_store_test.go
t.Run("get player score", func(t *testing.T) {
database := strings.NewReader(`[
{"Name": "Cleo", "Wins": 10},
{"Name": "Chris", "Wins": 33}]`)
store := FileSystemPlayerStore{database}
got := store.GetPlayerScore("Chris")
want := 33
assertScoreEquals(t, got, want)
})
最後に、
RecordWin
でスコアの記録を開始する必要があります。私たちのアプローチは、書き込みに対してかなり近視眼的です。ファイル内のJSONの「行」を1つだけ更新することは簡単にはできません。すべての書き込みで、データベースの whole 新しい表現を保存する必要があります。
どうやって書くの?通常は
Writer
を使用しますが、すでにReadSeeker
があります。潜在的に2つの依存関係が存在する可能性がありますが、標準ライブラリにはすでにReadWriteSeeker
用のインターフェースがあり、ファイルで必要なすべてのことを実行できます。タイプを更新しましょう
type FileSystemPlayerStore struct {
database io.ReadWriteSeeker
}
コンパイルされるかどうかを確認します
./file_system_store_test.go:15:34: cannot use database (type *strings.Reader) as type io.ReadWriteSeeker in field value:
*strings.Reader does not implement io.ReadWriteSeeker (missing Write method)
./file_system_store_test.go:36:34: cannot use database (type *strings.Reader) as type io.ReadWriteSeeker in field value:
*strings.Reader does not implement io.ReadWriteSeeker (missing Write method)
strings.Reader
が ReadWriteSeeker
を実装しないことはそれほど驚くべきことではないので、何をしますか?2つの選択肢があります
- テストごとに一時ファイルを作成します。
*os.File
はReadWriteSeeker
を実装します。これの長所は、それが統合テストになり、実際にファイルシステムからの読み取りと書き込みを行っているため、非常に高い信頼性が得られることです。短所は、ユニットテストの方が速く、一般にシンプルであるためです。また、一時ファイルを作成し、テスト後にそれらが確実に削除されるように、さらに作業を行う必要があります。
ここでは特に間違った答えはないと思いますが、サードパーティのライブラリを使用することを選択することで、依存関係の管理について説明する必要があります。そのため、代わりにファイルを使用します。
テストを追加する前に、
strings.Reader
をos.File
で置き換えることにより、他のテストをコンパイルする必要があります。内部にいくつかのデータを含む一時ファイルを作成するヘルパー関数を作成しましょう
//file_system_store_test.go
func createTempFile(t testing.TB, initialData string) (io.ReadWriteSeeker, func()) {
t.Helper()
tmpfile, err := ioutil.TempFile("", "db")
if err != nil {
t.Fatalf("could not create temp file %v", err)
}
tmpfile.Write([]byte(initialData))
removeFile := func() {
tmpfile.Close()
os.Remove(tmpfile.Name())
}
return tmpfile, removeFile
}
ReadWriteSeeker
(ファイル)だけでなく、関数も返すことに気づくでしょう。 テストが終了したら、ファイルを確実に削除する必要があります。エラーが発生しやすく、読者の興味をそそる可能性があるため、ファイルの詳細をテストに漏らしたくない。removeFile
関数を返すことで、ヘルパーの詳細を処理でき、呼び出し側が実行する必要があるのはdefer cleanDatabase()
を実行することだけです。//file_system_store_test.go
func TestFileSystemStore(t *testing.T) {
t.Run("league from a reader", func(t *testing.T) {
database, cleanDatabase := createTempFile(t, `[
{"Name": "Cleo", "Wins": 10},
{"Name": "Chris", "Wins": 33}]`)
defer cleanDatabase()
store := FileSystemPlayerStore{database}
got := store.GetLeague()
want := []Player{
{"Cleo", 10},
{"Chris", 33},
}
assertLeague(t, got, want)
// read again
got = store.GetLeague()
assertLeague(t, got, want)
})
t.Run("get player score", func(t *testing.T) {
database, cleanDatabase := createTempFile(t, `[
{"Name": "Cleo", "Wins": 10},
{"Name": "Chris", "Wins": 33}]`)
defer cleanDatabase()
store := FileSystemPlayerStore{database}
got := store.GetPlayerScore("Chris")
want := 33
assertScoreEquals(t, got, want)
})
}
テストを実行すると、テストに成功するはずです。かなりの量の変更がありましたが、インターフェースの定義が完了したように感じられ、これから新しいテストを簡単に追加できるはずです。
既存のプレイヤーの勝利を記録する最初の反復を取得しましょう
//file_system_store_test.go
t.Run("store wins for existing players", func(t *testing.T) {
database, cleanDatabase := createTempFile(t, `[
{"Name": "Cleo", "Wins": 10},
{"Name": "Chris", "Wins": 33}]`)
defer cleanDatabase()
store := FileSystemPlayerStore{database}
store.RecordWin("Chris")
got := store.GetPlayerScore("Chris")
want := 34
assertScoreEquals(t, got, want)
})
./file_system_store_test.go:67:8: store.RecordWin undefined (type FileSystemPlayerStore has no field or method RecordWin)
新しいメソッドを追加する
//file_system_store.go
func (f *FileSystemPlayerStore) RecordWin(name string) {
}
=== RUN TestFileSystemStore/store_wins_for_existing_players
--- FAIL: TestFileSystemStore/store_wins_for_existing_players (0.00s)
file_system_store_test.go:71: got 33 want 34
私たちの実装は空なので、古いスコアが返されます。
//file_system_store.go
func (f *FileSystemPlayerStore) RecordWin(name string) {
league := f.GetLeague()
for i, player := range league {
if player.Name == name {
league[i].Wins++
}
}
f.database.Seek(0, 0)
json.NewEncoder(f.database).Encode(league)
}
なぜ私が「
player.Wins++
」ではなく「league[i].Wins++
」をしているのか、疑問に思われるかもしれません。スライス上で「範囲
range
」を指定すると、ループの現在のインデックス(この場合はi
)とそのインデックスにある要素の_copy_が返されます。コピーのWins
値を変更しても、繰り返し処理するleague
スライスには影響しません。そのため、league[i]
を実行して実際の値への参照を取得し、代わりにその値を変更する必要があります。テストを実行すると、テストは成 功するはずです。
GetPlayerScore
とRecordWin
では、名前でプレーヤーを見つけるために[] Player
を繰り返し処理しています。FileSystemStore
の内部でこの共通コードをリファクタリングすることもできますが、私には、これが新しいタイプに引き上げることができるおそらく有用なコードであると感じています。これまで「リーグ"League"
」での作業は常に[]Player
で行っていましたが、League
という新しいタイプを作成できます。これは、他の 開発者が理解しやすくなり、そのタイプに便利なメソッドをアタッチして使用できるようになります。league.go
内に以下を追加しますtype League []Player
func (l League) Find(name string) *Player {
for i, p := range l {
if p.Name == name {
return &l[i]
}
}
return nil
}
誰かが
League
を持っている場合、彼らは与えられたプレイヤーを簡単に見つけることができます。PlayerStore
インターフェイスを変更して、[]Player
ではなくLeague
を返すようにします。テストを再実行してみてください。インターフェイスを変更したためコンパイルの問題が発生しますが、修正は非常に簡単です。戻り値の型を []Player
からLeague
に変更するだけです。これにより、
file_system_store
のメソッドを簡略化できます。//file_system_store.go
func (f *FileSystemPlayerStore) GetPlayerScore(name string) int {
player := f.GetLeague().Find(name)
if player != nil {
return player.Wins
}
return 0
}
func (f *FileSystemPlayerStore) RecordWin(name string) {
league := f.GetLeague()
player := league.Find(name)
if player != nil {
player.Wins++
}
f.database.Seek(0, 0)
json.NewEncoder(f.database).Encode(league)
}
これはかなり見栄えがよく、
League
の他の便利な機能をリファクタリングできる方法を見つけることができます。新しいプレーヤーの勝利を記録するシナリオを処理する必要があります。
//file_system_store_test.go
t.Run("store wins for new players", func(t *testing.T) {
database, cleanDatabase := createTempFile(t, `[
{"Name": "Cleo", "Wins": 10},
{"Name": "Chris", "Wins": 33}]`)
defer cleanDatabase()
store := FileSystemPlayerStore{database}
store.RecordWin("Pepper")
got := store.GetPlayerScore("Pepper")
want := 1
assertScoreEquals(t, got, want)
})
=== RUN TestFileSystemStore/store_wins_for_new_players#01
--- FAIL: TestFileSystemStore/store_wins_for_new_players#01 (0.00s)
file_system_store_test.go:86: got 0 want 1
プレーヤーが見つからなかったために
Find
がnil
を返すシナリオを処理する必要があるだけです。//file_system_store.go
func (f *FileSystemPlayerStore) RecordWin(name string) {
league := f.GetLeague()
player := league.Find(name)
if player != nil {
player.Wins++
} else {
league = append(league, Player{name, 1})
}
f.database.Seek(0, 0)
json.NewEncoder(f.database).Encode(league)
}
ハッピーパスは問題なく見えたので、統合テストで新しい
Store
を使用してみることができます。これにより、ソフトウェアが動作するという確信が高まり、冗長なInMemoryPlayerStore
を削除できます。TestRecordingWinsAndRetrievingThem
で古いストアを置き換えます。//server_integration_test.go
database, cleanDatabase := createTempFile(t, "")
defer cleanDatabase()
store := &FileSystemPlayerStore{database}
テストを実行すると、テストに成功し、
InMemoryPlayerStore
を削除できます。 main.go
にコンパイルの問題が発生し、「実際の」コードで新しいストアを使用するようになります。//main.go
package main
import (
"log"
"net/http"
"os"
)
const dbFileName = "game.db.json"
func main() {
db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatalf("problem opening %s %v", dbFileName, err)
}
store := &FileSystemPlayerStore{db}
server := NewPlayerServer(store)
if err := http.ListenAndServe(":5000", server); err != nil {
log.Fatalf("could not listen on port 5000 %v", err)
}
}
- データベース用のファイルを作成します。
os.OpenFile
の2番目の引数では、ファイルを開くための権限を定義できます。この場合、O_RDWR
は読み取りと書き込みを行うことを意味し、os.O_CREATE
はファイルが存在しない場合にファイルを作成することを意味します。
プログラムを実行すると、再起動の間にデータがファイルに永続化されるようになりました。
誰かが
GetLeague()
またはGetPlayerScore()
を呼び出すたびに、ファイル全体を読み取ってJSONに解析します。FileSystemStore
はリーグの状態に完全に責任があるため、これを行う必要はありません。プログラムの起動時にファイルを読み取るだけで、データが変更されたときにファイルを更新するだけです。この初期化の一部を実行できるコンストラクターを作成し、代わりに読み取りで使用するためにリーグを
FileSystemStore
の値として保存できます。//file_system_store.go
type FileSystemPlayerStore struct {
database io.ReadWriteSeeker
league League
}
func NewFileSystemPlayerStore(database io.ReadWriteSeeker) *FileSystemPlayerStore {
database.Seek(0, 0)
league, _ := NewLeague(database)
return &FileSystemPlayerStore{
database: database,
league: league,
}
}
この方法では、ディスクから一度だけ読み取る必要があります。ディスクからリーグを取得するための以前の呼び出しをすべて置き換えて、代わりに
f.league
を使用できます。//file_system_store.go
func (f *FileSystemPlayerStore) GetLeague() League {
return f.league
}
func (f *FileSystemPlayerStore) GetPlayerScore(name string) int {
player := f.league.Find(name)
if player != nil {
return player.Wins
}
return 0
}
func (f *FileSystemPlayerStore) RecordWin(name string) {
player := f.league.Find(name)
if player != nil {
player.Wins++
} else {
f.league = append(f.league, Player{name, 1})
}
f.database.Seek(0, 0)
json.NewEncoder(f.database).Encode(f.league)
}
テストを実行しようとすると、
FileSystemPlayerStore
の初期化について不平を言うので、新しいコンストラクターを呼び出して修正するだけです。非常に厄介なバグを発生させる可能性があるファイルを処理する方法には、いくつかの単純な点があります。
RecordWin
を実行すると、ファイルの先頭にSeek
して新しいデータを書き込みますが、新しいデータが以前のデータよりも小さい場合はどうなるでしょうか。私たちの現在のケースでは、これは不可能です。スコアを編集または削除することはないため、データが大きくなるだけです。ただし、コードをこのようにしておくのは無責任です。削除シナリオが発生することは考えられないことではありません。
しかし、これをどのようにテストしますか?最初にコードをリファクタリングする必要があるので、書き込むデータの種類の懸念を書き込みから分離します。次に、それを個別にテストして、期待どおりに動作することを確認できます。
新しいタイプを作成して、「最初から書き始める」機能をカプセル化します。これを「テープ
Tape
」と呼びます。以下を使用して新しいファイルを作成します。//tape.go
package main
import "io"
type tape struct {
file io.ReadWriteSeeker
}
func (t *tape) Write(p []byte) (n int, err error) {
t.file.Seek(0, 0)
return t.file.Write(p)
}
ここでは、
Seek
部分をカプセル化しているため、ここではWrite
のみを実装していることに注意してください。つまり、FileSystemStore
は、代わりにWriter
への参照のみを持つことができます。//file_system_store.go
type FileSystemPlayerStore struct {
database io.Writer
league League
}
Tape
を使用するようにコンストラクタを更新します//file_system_store.go
func NewFileSystemPlayerStore(database io.ReadWriteSeeker) *FileSystemPlayerStore {
database.Seek(0, 0)
league, _ := NewLeague(database)
return &FileSystemPlayerStore{
database: &tape{database},
league: league,
}
}
最後に、
RecordWin
からSeek
呼び出しを削除することで、私たちが望んでいた驚くべき見返りを得ることができます。はい、あまり感じませんが、少なくとも他の種類の書き込みを行う場合、Write
を使用して必要な動作を実行できることを意味します。さらに、潜在的に問題のあるコードを個別にテストして修正できるようになります。ファイルの内容全体を元の内容よりも小さいもので更新するテストを書いてみましょう。
テストでは、コンテンツを含むファイルを作成し、
tape
を使用してファイルに書き込み、もう一度読み取って、ファイルの内容を確認します。tape_test.go
//tape_test.go
func TestTape_Write(t *testing.T) {
file, clean := createTempFile(t, "12345")
defer clean()
tape := &tape{file}
tape.Write([]byte("abc"))
file.Seek(0, 0)
newFileContents, _ := ioutil.ReadAll(file)
got := string(newFileContents)
want := "abc"
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
=== RUN TestTape_Write
--- FAIL: TestTape_Write (0.00s)
tape_test.go:23: got 'abc45' want 'abc'
思った通り!
必要なデータを書き込みますが、元のデータの残りを残します。
os.File
には、ファイルを効率的に空にできる切り捨て関数があります。これを呼び出して、必要なものを取得できます。tape
を次のように変更します。//tape.go
type tape struct {
file *os.File
}
func (t *tape) Write(p []byte) (n int, err error) {
t.file.Truncate(0)
t.file.Seek(0, 0)
return t.file.Write(p)
}
コンパイラーは、
io.ReadWriteSeeker
を想定している多くの場所で失敗しますが、*os.File
で送信しています。これらの問題は自分で修正できるはずですが、行き詰まった場合はソースコードを確認してください。それを取得したら、
TestTape_Write
テストはパスするはずです!RecordWin
には、json.NewEncoder(f.database).Encode(f.league)
という行があります。書き込むたびに新しいエンコーダを作成する必要はありません。コンストラクタでエンコーダを初期化して、代わりに使用できます。
タイプに「エンコーダー
Encoder
」への参照を保存し、コンストラクターで初期化します。//file_system_store.go
type FileSystemPlayerStore struct {
database *json.Encoder
league League
}
func NewFileSystemPlayerStore(file *os.File) *FileSystemPlayerStore {
file.Seek(0, 0)
league, _ := NewLeague(file)
return &FileSystemPlayerStore{
database: json.NewEncoder(&tape{file}),
league: league,
}
}
RecordWin
で使用します。func (f *FileSystemPlayerStore) RecordWin(name string) {
player := f.league.Find(name)
if player != nil {
player.Wins++
} else {
f.league = append(f.league, Player{name, 1})
}
f.database.Encode(f.league)
}