時間
Time
ポーカーについて多くのことを知る必要はありません。一定の時間間隔で、すべてのプレーヤーに着実に増加する「ブラインド(強制bet)」値を通知する必要があるということだけです。
私たちのアプリケーションは、ブラインドがいつ上がるべきか、そしてどれくらいあるべきかを追跡するのに役立ちます。
- 開始すると、何人のプレイヤーがプレイしているかを尋ねます。これは、「ブラインド」ベットが上がるまでの時間を決定します。
- 基本時間は5 分です。
- すべてのプレーヤーについて、1分が追加されます。
- 例:6人のプレーヤーは、ブラインドでは11分に相当します。
- ブラインドタイムが終了した後、ゲームはプレイヤーにブラインドベットの新しい金額を警告する必要があります。
- ブラインドは100チップから始まり、その後200、400、600、1000、2000となり、ゲームが終了するまで2倍になります(「ルース勝利」の以前の機能は、ゲームを終了するはずです)。
前の章では、
{name} wins
のコマンドを既に受け入れているコマンドラインアプリケーションを開始しました。これが現在のCLI
コードの外観ですが、開始する前に 他のコードについてもよく理解してください。type CLI struct {
playerStore PlayerStore
in *bufio.Scanner
}
func NewCLI(store PlayerStore, in io.Reader) *CLI {
return &CLI{
playerStore: store,
in: bufio.NewScanner(in),
}
}
func (cli *CLI) PlayPoker() {
userInput := cli.readLine()
cli.playerStore.RecordWin(extractWinner(userInput))
}
func extractWinner(userInput string) string {
return strings.Replace(userInput, " wins", "", 1)
}
func (cli *CLI) readLine() string {
cli.in.Scan()
return cli.in.Text()
}
プレーヤーの数に応じて特定の期間にブラインドベット値を印刷するようにプログラムをスケジュールできるようにしたいと考えています。
必要なことの範囲を制限するために、ここではプレーヤーの数を忘れて5人のプレーヤーがいると仮定し、ブラインドベットの新しい値が10分ごとに出力されるかどうかをテストします。
AfterFunc
は継続時間が経過するのを待ってから、独自のgoroutineでfを呼び出します。これは、Stopメソッドを使用して呼び出しをキャンセルするために使用できるTimer
を返します。
期間は、2つの瞬間間の経過時間をint64ナノ秒カウントとして表します。
タイムライブラリには、これらのナノ秒を乗算できるようにする定数がいくつかあります。これにより、これらの定数は、私たちが行うシナリオの種類に対して、より読みやすくなります。
5 * time.Second
「
PlayPoker
」を呼び出すと、すべてのブラインドアラートをスケジュールします。これをテストするのは少し難しいかもしれません。各期間が正しいブラインド量でスケジュールされていることを確認する必要がありますが、
time.AfterFunc
のシグネチャを見ると、2番目の引数は実行される関数です。 Goでは関数を比較できないため、送信された関数をテストすることはできません。そのため、実行に時間と印刷する量を必要とする、time.AfterFunc
の周りにある種のラッパーを書く必要があります。私たちはそれをスパイすることができます。スイートに新しいテストを追加する
t.Run("it schedules printing of blind values", func(t *testing.T) {
in := strings.NewReader("Chris wins\n")
playerStore := &poker.StubPlayerStore{}
blindAlerter := &SpyBlindAlerter{}
cli := poker.NewCLI(playerStore, in, blindAlerter)
cli.PlayPoker()
if len(blindAlerter.alerts) != 1 {
t.Fatal("expected a blind alert to be scheduled")
}
})
SpyBlindAlerter
を作成し、それをCLI
に挿入しよ うとしています。次に、 PlayPoker
を呼び出した後、アラートがスケジュールされていることを確認します。(最初に最も単純なシナリオを実行することを思い出して、次に反復します。)
ここに
SpyBlindAlerter
の定義がありますtype SpyBlindAlerter struct {
alerts []struct {
scheduledAt time.Duration
amount int
}
}
func (s *SpyBlindAlerter) ScheduleAlertAt(duration time.Duration, amount int) {
s.alerts = append(s.alerts, struct {
scheduledAt time.Duration
amount int
}{duration, amount})
}
./CLI_test.go:32:27: too many arguments in call to poker.NewCLI
have (*poker.StubPlayerStore, *strings.Reader, *SpyBlindAlerter)
want (poker.PlayerStore, io.Reader)
新しい引数を追加しましたが、コンパイラは不満を言っています。 厳密に言えば、最小限のコードは
NewCLI
が*SpyBlindAlerter
を受け入れるようにすることですが、少し騙して、依存関係をインターフェイスとして定義しましょう。type BlindAlerter interface {
ScheduleAlertAt(duration time.Duration, amount int)
}
そして、それをコンストラクタに追加します。
func NewCLI(store PlayerStore, in io.Reader, alerter BlindAlerter) *CLI
他のテストは
NewCLI
にBlindAlerter
が渡されていないので失敗します。BlindAlerter
をスパイすることは他のテストには関係ないので、テストファイルに追加します。var dummySpyAlerter = &SpyBlindAlerter{}
そして、コンパイルの問題を修正するために他のテストでそれを使ってください。 これを「ダミー
"dummy"
」と表記することで、テストの読者には重要ではないことが明らかになります。これでテストはコンパイルされ、新しいテストは失敗します。
=== RUN TestCLI
=== RUN TestCLI/it_schedules_printing_of_blind_values
--- FAIL: TestCLI (0.00s)
--- FAIL: TestCLI/it_schedules_printing_of_blind_values (0.00s)
CLI_test.go:38: expected a blind alert to be scheduled
これを
PlayPoker
メソッドで参照できるようにするためにBlindAlerter
をCLI
のフィールドとして追加する必要があります。type CLI struct {
playerStore PlayerStore
in *bufio.Scanner
alerter BlindAlerter
}
func NewCLI(store PlayerStore, in io.Reader, alerter BlindAlerter) *CLI {
return &CLI{
playerStore: store,
in: bufio.NewScanner(in),
alerter: alerter,
}
}
テストを通過させるために、
BlindAlerter
を好きなもので呼び出すことができます。func (cli *CLI) PlayPoker() {
cli.alerter.ScheduleAlertAt(5*time.Second, 100)
userInput := cli.readLine()
cli.playerStore.RecordWin(extractWinner(userInput))
}
次は、5人のプレイヤーのために、私たちが望むすべてのアラートのスケジュールを確認したいと思います。
t.Run("it schedules printing of blind values", func(t *testing.T) {
in := strings.NewReader("Chris wins\n")
playerStore := &poker.StubPlayerStore{}
blindAlerter := &SpyBlindAlerter{}
cli := poker.NewCLI(playerStore, in, blindAlerter)
cli.PlayPoker()
cases := []struct {
expectedScheduleTime time.Duration
expectedAmount int
}{
{0 * time.Second, 100},
{10 * time.Minute, 200},
{20 * time.Minute, 300},
{30 * time.Minute, 400},
{40 * time.Minute, 500},
{50 * time.Minute, 600},
{60 * time.Minute, 800},
{70 * time.Minute, 1000},
{80 * time.Minute, 2000},
{90 * time.Minute, 4000},
{100 * time.Minute, 8000},
}
for i, c := range cases {
t.Run(fmt.Sprintf("%d scheduled for %v", c.expectedAmount, c.expectedScheduleTime), func(t *testing.T) {
if len(blindAlerter.alerts) <= i {
t.Fatalf("alert %d was not scheduled %v", i, blindAlerter.alerts)
}
alert := blindAlerter.alerts[i]
amountGot := alert.amount
if amountGot != c.expectedAmount {
t.Errorf("got amount %d, want %d", amountGot, c.expectedAmount)
}
gotScheduledTime := alert.scheduledAt
if gotScheduledTime != c.expectedScheduleTime {
t.Errorf("got scheduled time of %v, want %v", gotScheduledTime, c.expectedScheduleTime)
}
})
}
})
テーブルベースのテストはここではうまく機能し、要件が何であるかを明確に示しています。テーブルを実行し、
SpyBlindAlerter
をチェックして、アラートが正しい値でスケジュールされているかどうかを確認します。こんな感じで失敗を重ねるといいですよ。
=== RUN TestCLI
--- FAIL: TestCLI (0.00s)
=== RUN TestCLI/it_schedules_printing_of_blind_values
--- FAIL: TestCLI/it_schedules_printing_of_blind_values (0.00s)
=== RUN TestCLI/it_schedules_printing_of_blind_values/100_scheduled_for_0s
--- FAIL: TestCLI/it_schedules_printing_of_blind_values/100_scheduled_for_0s (0.00s)
CLI_test.go:71: got scheduled time of 5s, want 0s
=== RUN TestCLI/it_schedules_printing_of_blind_values/200_scheduled_for_10m0s
--- FAIL: TestCLI/it_schedules_printing_of_blind_values/200_scheduled_for_10m0s (0.00s)
CLI_test.go:59: alert 1 was not scheduled [{5000000000 100}]
func (cli *CLI) PlayPoker() {
blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000}
blindTime := 0 * time.Second
for _, blind := range blinds {
cli.alerter.ScheduleAlertAt(blindTime, blind)
blindTime = blindTime + 10*time.Minute
}
userInput := cli.readLine()
cli.playerStore.RecordWin(extractWinner(userInput))
}
それは、私たちがすでに持っていたものよりもそれほど複雑ではありません。 私たちは今、
blinds
の配列を反復処理し、増加するblindTime
でスケジューラを呼び出しています「
PlayPoker
」を少し明確にするために、スケジュールされたアラートをメソッドにカプセル化できます。func (cli *CLI) PlayPoker() {
cli.scheduleBlindAlerts()
userInput := cli.readLine()
cli.playerStore.RecordWin(extractWinner(userInput))
}
func (cli *CLI) scheduleBlindAlerts() {
blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000}
blindTime := 0 * time.Second
for _, blind := range blinds {
cli.alerter.ScheduleAlertAt(blindTime, blind)
blindTime = blindTime + 10*time.Minute
}
}
最後に、テストは少し不格好に見えます。同じものを表す2つの匿名構造体、
ScheduledAlert
があります。それを新しい型にリファクタリングして、いくつかのヘルパーを比較してみましょう。type scheduledAlert struct {
at time.Duration
amount int
}
func (s scheduledAlert) String() string {
return fmt.Sprintf("%d chips at %v", s.amount, s.at)
}
type SpyBlindAlerter struct {
alerts []scheduledAlert
}
func (s *SpyBlindAlerter) ScheduleAlertAt(at time.Duration, amount int) {
s.alerts = append(s.alerts, scheduledAlert{at, amount})
}
タイプに
String()
メソッドを追加したので、テストが失敗した場合にうまく表示されます。新しいタイプを使用するようにテストを更新します。
t.Run("it schedules printing of blind values", func(t *testing.T) {
in := strings.NewReader("Chris wins\n")
playerStore := &poker.StubPlayerStore{}
blindAlerter := &SpyBlindAlerter{}
cli := poker.NewCLI(playerStore, in, blindAlerter)
cli.PlayPoker()
cases := []scheduledAlert{
{0 * time.Second, 100},
{10 * time.Minute, 200},
{20 * time.Minute, 300},
{30 * time.Minute, 400},
{40 * time.Minute, 500},
{50 * time.Minute, 600},
{60 * time.Minute, 800},
{70 * time.Minute, 1000},
{80 * time.Minute, 2000},
{90 * time.Minute, 4000},
{100 * time.Minute, 8000},
}
for i, want := range cases {
t.Run(fmt.Sprint(want), func(t *testing.T) {
if len(blindAlerter.alerts) <= i {
t.Fatalf("alert %d was not scheduled %v", i, blindAlerter.alerts)
}
got := blindAlerter.alerts[i]
assertScheduledAlert(t, got, want)
})
}
})
自分で「
assertScheduledAlert
」を実装します。ここではかなりの時間をかけてテストを作成しており、アプリケーションと統合しないことはややエッチです。これ以上の要件に取り掛かる前に、その問題に取り組みましょう。
ア プリを実行してみてください。アプリがコンパイルされず、「
NewCLI
」への引数が足りないという不満があります。アプリケーションで使用できる
BlindAlerter
の実装を作成してみましょう。BlindAlerter.go
を作成し、BlindAlerter
インターフェースを移動して、以下の新しいものを追加しますpackage poker
import (
"fmt"
"os"
"time"
)
type BlindAlerter interface {
ScheduleAlertAt(duration time.Duration, amount int)
}
type BlindAlerterFunc func(duration time.Duration, amount int)
func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int) {
a(duration, amount)
}
func StdOutAlerter(duration time.Duration, amount int) {
time.AfterFunc(duration, func() {
fmt.Fprintf(os.Stdout, "Blind is now %d\n", amount)
})
}
type は、
struct
だけでなく、インターフェイスを実装できることに注意してください。 1つの関数が定義されたインターフェースを公開するライブラリを作成する場合、MyInterfaceFunc
タイプも公開することは一般的な慣用法です。このタイプはあなたのインターフェースも実装する
func
になります。こうすることで、インターフェイスのユーザーは、関数だけでインターフェイスを実装することができます。 空のstruct
タイプを作成する必要はありません。次に、関数と同じシグネチャを持つ関数
StdOutAlerter
を作成し、time.AfterFunc
を使用して、それをos.Stdout
に出力するようにスケジュールします。この動作を確認するには、
NewCLI
を作成するmain
を更新します。poker.NewCLI(store, os.Stdin, poker.BlindAlerterFunc(poker.StdOutAlerter)).PlayPoker()
実行する前に、「
CLI
」の「blindTime
」の増分を10分ではなく10秒に変更すると、実際の動作を確認できます。10秒ごとに予想されるように、ブラインド値が出力されるはずです。 CLIに「
Shaun wins
」と入力すると、プログラムが予期したとおりに停止することに注意してください。ゲームは常に5人でプレイされるとは限らないため、ゲームを開始する前に、ユーザー数を入力するようユーザーに促す必要があります。
確認するために、StdOutに書き込まれた内容を記録するプレーヤーの数を入力するように求めています。 これを数回実行しました。
os.Stdout
はio.Writer
であることを知っているので、テストで依存性注入を使用してbytes.Buffer
を渡し、私たちのコー ドが何を書くか見てください。このテストでは、他の共同編集者についてはまだ気にしていないため、テストファイルでダミーを作成しました。
CLI
に4つの依存関係があることに少し注意する必要があります。 これは、多すぎる責任を持ち始めているように思われます。とりあえずそれと共存して、この新しい機能を追加するときにリファクタリングが出現するかどうか見てみましょう。var dummyBlindAlerter = &SpyBlindAlerter{}
var dummyPlayerStore = &poker.StubPlayerStore{}
var dummyStdIn = &bytes.Buffer{}
var dummyStdOut = &bytes.Buffer{}
これが新しいテストです。
t.Run("it prompts the user to enter the number of players", func(t *testing.T) {
stdout := &bytes.Buffer{}
cli := poker.NewCLI(dummyPlayerStore, dummyStdIn, stdout, dummyBlindAlerter)
cli.PlayPoker()
got := stdout.String()
want := "Please enter the number of players: "
if got != want {
t.Errorf("got %q, want %q", got, want)
}
})
main
でos.Stdout
になるものを渡し、何が書き込まれるかを確認します。./CLI_test.go:38:27: too many arguments in call to poker.NewCLI
have (*poker.StubPlayerStore, *bytes.Buffer, *bytes.Buffer, *SpyBlindAlerter)
want (poker.PlayerStore, io.Reader, poker.BlindAlerter)
新しい依存関係があるため、
NewCLI
を更新する必要がありますfunc NewCLI(store PlayerStore, in io.Reader, out io.Writer, alerter BlindAlerter) *CLI
その他のテストは、
NewCLI
に渡されるio.Writer
がないため、コンパイルに失敗します。他のテスト用に「
dummyStdout
」を追加します。新しいテストはそのように失敗す るはずです。
=== RUN TestCLI
--- FAIL: TestCLI (0.00s)
=== RUN TestCLI/it_prompts_the_user_to_enter_the_number_of_players
--- FAIL: TestCLI/it_prompts_the_user_to_enter_the_number_of_players (0.00s)
CLI_test.go:46: got '', want 'Please enter the number of players: '
FAIL
CLI
に新しい依存関係を追加して、PlayPoker
で参照できるようにする必要があります。type CLI struct {
playerStore PlayerStore
in *bufio.Scanner
out io.Writer
alerter BlindAlerter
}
func NewCLI(store PlayerStore, in io.Reader, out io.Writer, alerter BlindAlerter) *CLI {
return &CLI{
playerStore: store,
in: bufio.NewScanner(in),
out: out,
alerter: alerter,
}
}
最後に、ゲームの開始時にプロンプ トを書き込むことができます。
func (cli *CLI) PlayPoker() {
fmt.Fprint(cli.out, "Please enter the number of players: ")
cli.scheduleBlindAlerts()
userInput := cli.readLine()
cli.playerStore.RecordWin(extractWinner(userInput))
}
定数に抽出する 必要があるプロンプトの重複文字列があります。
const PlayerPrompt = "Please enter the number of players: "
テストコードと
CLI
の両方でこれを使用してください。次に、番号を送信して抽出する必要があります。 目的の効果があったかどうかを確認する唯一の方法は、スケジュールされたブラインドアラートを確認することです。
t.Run("it prompts the user to enter the number of players", func(t *testing.T) {
stdout := &bytes.Buffer{}
in := strings.NewReader("7\n")
blindAlerter := &SpyBlindAlerter{}
cli := poker.NewCLI(dummyPlayerStore, in, stdout, blindAlerter)
cli.PlayPoker()
got := stdout.String()
want := poker.PlayerPrompt
if got != want {
t.Errorf("got %q, want %q", got, want)
}
cases := []scheduledAlert{
{0 * time.Second, 100},
{12 * time.Minute, 200},
{24 * time.Minute, 300},
{36 * time.Minute, 400},
}
for i, want := range cases {
t.Run(fmt.Sprint(want), func(t *testing.T) {
if len(blindAlerter.alerts) <= i {
t.Fatalf("alert %d was not scheduled %v", i, blindAlerter.alerts)
}
got := blindAlerter.alerts[i]
assertScheduledAlert(t, got, want)
})
}
})
いたたたた!たくさんの変更。。
- StdInのダミーを削除し、代わりに、7を入力するユーザーを表すモックバージョンを送信します。
- また、ブラインドアラートでダミーを削除して、プレーヤー数がスケジュールに影響を与えていることを確認できるようにしました
- スケジュールされているアラートをテストします
テストはコンパイルされ、失敗するはずですが、5人のプレーヤーに基づいてゲームをハードコーディングしているため、スケジュールされた時間が間違っていると報告されます。
=== RUN TestCLI
--- FAIL: TestCLI (0.00s)
=== RUN TestCLI/it_prompts_the_user_to_enter_the_number_of_players
--- FAIL: TestCLI/it_prompts_the_user_to_enter_the_number_of_players (0.00s)
=== RUN TestCLI/it_prompts_the_user_to_enter_the_number_of_players/100_chips_at_0s
--- PASS: TestCLI/it_prompts_the_user_to_enter_the_number_of_players/100_chips_at_0s (0.00s)
=== RUN TestCLI/it_prompts_the_user_to_enter_the_number_of_players/200_chips_at_12m0s
この作業を行うために必要なすべての罪を自由に犯すことを忘れないでください。 作業用ソフトウェアができたら、作成しようとしている混乱のリファクタリングに取り掛かることができます。
func (cli *CLI) PlayPoker() {
fmt.Fprint(cli.out, PlayerPrompt)
numberOfPlayers, _ := strconv.Atoi(cli.readLine())
cli.scheduleBlindAlerts(numberOfPlayers)
userInput := cli.readLine()
cli.playerStore.RecordWin(extractWinner(userInput))
}
func (cli *CLI) scheduleBlindAlerts(numberOfPlayers int) {
blindIncrement := time.Duration(5+numberOfPlayers) * time.Minute
blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000}
blindTime := 0 * time.Second
for _, blind := range blinds {
cli.alerter.ScheduleAlertAt(blindTime, blind)
blindTime = blindTime + blindIncrement
}
}
numberOfPlayersInput
を文字列に読み込みます- ユーザーから入力を取得するために
cli.readLine()
を使用し、次にエラーシナリオを無視して、Atoi
を呼び出して整数に変換します。後でそのシナリオのテストを作成する必要があります。 - ここから、
scheduleBlindAlerts
を変更して、多数のプレーヤーを受け入れます。次に、ブラインド量を反復するときに「blindTime
」に追加するために使用する「blindIncrement
」時間を計算します
新しいテストは修正されましたが、システムが機能するのは、ユーザーが数字を入力してゲームを開始した場合に限られるためです。 ユーザー入力を変更してテストを修正する必要があります。 これにより、番号とそれに続く改行が追加されます(これは、現在のアプローチのさらに多くの欠陥を強調しています)。
これは少し恐ろしいことだと思いますか? テストを聞いてみましょう。
- いくつかのアラートをスケジュールしていることをテストするために、4つの異なる依存関係を設定しました。システムに thing の依存関係がたくさんある場合、それは多すぎることを意味します。視覚的には、テストが雑然としていることがわかります。
- 私には、ユーザー入力の読み取りと、実行したいビジネスロジックとの間をより明確に抽象化する必要があるように感じます
- より適切なテストは、_このユーザー入力が与えられた場合、正しいタイプのプレーヤーで新しいタイプ
Game
を呼び出すかどうかです。 - 次に、スケジューリングのテストを新しい
Game
のテストに抽出します。
最初に「
Game
」に向けてリファクタリングでき、テストは引き続き成功します。必要な構造変更を行ったら、懸念の分離を反映するためにテストをリファクタリングする方法について考えることができますリファクタリングに変更を加えるときは、それらをできるだけ小さくして、テストを再実行し続けるようにしてください。
まず自分で試してください。 「
Game
」が提供するものと「CLI
」が行うべきことの境界について考えてください。今のところは、
NewCLI
の外部インターフェースを変更しないでください。 テストコードとクライアントコードを同時に変更したくないからです。これは私が思いついたものです。
// game.go
type Game struct {
alerter BlindAlerter
store PlayerStore
}
func (p *Game) Start(numberOfPlayers int) {
blindIncrement := time.Duration(5+numberOfPlayers) * time.Minute
blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000}
blindTime := 0 * time.Second
for _, blind := range blinds {
p.alerter.ScheduleAlertAt(blindTime, blind)
blindTime = blindTime + blindIncrement
}
}
func (p *Game) Finish(winner string) {
p.store.RecordWin(winner)
}
// cli.go
type CLI struct {
in *bufio.Scanner
out io.Writer
game *Game
}
func NewCLI(store PlayerStore, in io.Reader, out io.Writer, alerter BlindAlerter) *CLI {
return &CLI{
in: bufio.NewScanner(in),
out: out,
game: &Game{
alerter: alerter,
store: store,
},
}
}
const PlayerPrompt = "Please enter the number of players: "
func (cli *CLI) PlayPoker() {
fmt.Fprint(cli.out, PlayerPrompt)
numberOfPlayersInput := cli.readLine()
numberOfPlayers, _ := strconv.Atoi(strings.Trim(numberOfPlayersInput, "\n"))
cli.game.Start(numberOfPlayers)
winnerInput := cli.readLine()
winner := extractWinner(winnerInput)
cli.game.Finish(winner)
}
func extractWinner(userInput string) string {
return strings.Replace(userInput, " wins\n", "", 1)
}
func (cli *CLI) readLine() string {
cli.in.Scan()
return cli.in.Text()
}
「ドメイン」の観点から
- 何人がプレイしているかを示す「
Game
」を「開始Start
」したい - 勝者を宣言して「
Game
」を「終了Finish
」したい
新しい
Game
タイプはこれをカプセル化します。この変更により、
BlindAlerter
とPlayerStore
がGame
に渡されました。 これは、結果のアラートと保存を担当するためです。私たちの
CLI
は今ちょうど関係しています- 既存の依存関係を使用して
Game
を構築します(これは次にリファクタリングします) - ユーザー入力を
Game
のメソッド呼び出しとして解釈する
「大きな」リファクタリングを行わないようにしたいと考えています。 これにより、長期間にわたってテストが失敗し、ミスの可能性が高まるためです。 (大規模な分散型チームで作業している場合、これは非常に重要です)
まず最初に、
Game
をリファクタリングして、CLI
に注入します。 テストを最小限に変更してそれを容易にし、次に、テストをユーザー入力の解析とゲーム管理のテーマに分割する方法を確認します。今必要なのは、
NewCLI
を変更することだけです。func NewCLI(in io.Reader, out io.Writer, game *Game) *CLI {
return &CLI{
in: bufio.NewScanner(in),
out: out,
game: game,
}
}
これはすでに改善のように感じられます。 依存関係は少なく、私たちの依存関係リストは、CLIが入出力に関係し、ゲーム固有のアクションを
Game
に委任するという、全体的な設計目標を反映しています。コンパイルしてみると問題があります。これらの問題は自分で修正できるはずです。 今すぐ
Game
のモックを作成する必要はありません。すべてをコンパイルしてテストするために「実際の」Game
を初期化するだけです。これを行うには、コンストラクタを作成する必要があります。
func NewGame(alerter BlindAlerter, store PlayerStore) *Game {
return &Game{
alerter:alerter,
store:store,
}
}
これは、修正されたテストの設定の1つの例です。
stdout := &bytes.Buffer{}
in := strings.NewReader("7\n")
blindAlerter := &SpyBlindAlerter{}
game := poker.NewGame(blindAlerter, dummyPlayerStore)
cli := poker.NewCLI(in, stdout, game)
cli.PlayPoker()
テストを修正して再び緑に戻るのにそれほどの労力は必要ありませんが、それがポイントです! 次の段階の前に
main.go
も修正するようにしてください。// main.go
game := poker.NewGame(poker.BlindAlerterFunc(poker.StdOutAlerter), store)
cli := poker.NewCLI(os.Stdin, os.Stdout, game)
cli.PlayPoker()
「ゲーム
Game
」を抽出したので、ゲーム固有のアサーションをCLIとは別のテストに移動する必要があります。これは、
CLI
テストをコピーするための単な る練習ですが、依存関係は少なくなっています。func TestGame_Start(t *testing.T) {
t.Run("schedules alerts on game start for 5 players", func(t *testing.T) {
blindAlerter := &poker.SpyBlindAlerter{}
game := poker.NewGame(blindAlerter, dummyPlayerStore)
game.Start(5)
cases := []poker.ScheduledAlert{
{At: 0 * time.Second, Amount: 100},
{At: 10 * time.Minute, Amount: 200},
{At: 20 * time.Minute, Amount: 300},
{At: 30 * time.Minute, Amount: 400},
{At: 40 * time.Minute, Amount: 500},
{At: 50 * time.Minute, Amount: 600},
{At: 60 * time.Minute, Amount: 800},
{At: 70 * time.Minute, Amount: 1000},
{At: 80 * time.Minute, Amount: 2000},
{At: 90 * time.Minute, Amount: 4000},
{At: 100 * time.Minute, Amount: 8000},
}
checkSchedulingCases(cases, t, blindAlerter)
})
t.Run("schedules alerts on game start for 7 players", func(t *testing.T) {
blindAlerter := &poker.SpyBlindAlerter{}
game := poker.NewGame(blindAlerter, dummyPlayerStore)
game.Start(7)
cases := []poker.ScheduledAlert{
{At: 0 * time.Second, Amount: 100},
{At: 12 * time.Minute, Amount: 200},
{At: 24 * time.Minute, Amount: 300},
{At: 36 * time.Minute, Amount: 400},
}
checkSchedulingCases(cases, t, blindAlerter)
})
}
func TestGame_Finish(t *testing.T) {
store := &poker.StubPlayerStore{}
game := poker.NewGame(dummyBlindAlerter, store)
winner := "Ruth"
game.Finish(winner)
poker.AssertPlayerWin(t, store, winner)
}
ポーカーのゲームが始まったときに何が起きるかの背後にある意図が今やはるかに明確になりました。
ゲームがいつ終了するかについても、テストの上を移動してください。
満足したら、ゲームロジックのテストを移行しました。 CLIテストを簡略化して、意図した責任をより明確に反映できます。
- ユーザー入力を処理し、必要に応じて
Game
のメソッドを呼び出します - 出力を送信
- 重要なのは、ゲームがどのように機能するかについての実際の仕組みについて知らない
これを行うには、
CLI
が具体的なGame
タイプに依存しないようにする必要がありますが、代わりにStart(numberOfPlayers)
およびFinish(winner)
のインターフェースを受け入れます。次に、そのタイプのスパイを作成し、正しい呼び出しが行われたことを確認できます。ここに、ネーミングが時々厄介であることがわ かります。
Game
の名前をTexasHoldem
に変更します(これが現在プレイしているゲームの 種類 であるため)。新しいインターフェイスはGame
と呼ばれます。これは、CLIが私たちがプレイしている実際のゲームに気づいていないという概念と、「Start
」および「Finish
」したときに何が起こるかを忠実に保ちます。type Game interface {
Start(numberOfPlayers int)
Finish(winner string)
}
CLI
内の*Game
へのすべての参照を置き換え、それらをGame
(our new interface)に置き換えます。 いつものように、リファクタリングしている間は、テストを再実行してすべてが緑色であることを確認します。TexasHoldem
からCLI
を分離したので、スパイを使用して、正しい引数で、期待どおりにStart
とFinish
が呼び出されることを確認できます。Game
を実装するスパイを作成するtype GameSpy struct {
StartedWith int
FinishedWith string
}
func (g *GameSpy) Start(numberOfPlayers int) {
g.StartedWith = numberOfPlayers
}
func