時間

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()
}

time.AfterFunc

プレーヤーの数に応じて特定の期間にブラインドベット値を印刷するようにプログラムをスケジュールできるようにしたいと考えています。

必要なことの範囲を制限するために、ここではプレーヤーの数を忘れて5人のプレーヤーがいると仮定し、ブラインドベットの新しい値が10分ごとに出力されるかどうかをテストします

いつものように、標準ライブラリはfunc AfterFunc(d Duration, f func()) *Timerでカバーされています。

AfterFuncは継続時間が経過するのを待ってから、独自のgoroutineでfを呼び出します。これは、Stopメソッドを使用して呼び出しをキャンセルするために使用できるTimerを返します。

期間は、2つの瞬間間の経過時間をint64ナノ秒カウントとして表します。

タイムライブラリには、これらのナノ秒を乗算できるようにする定数がいくつかあります。これにより、これらの定数は、私たちが行うシナリオの種類に対して、より読みやすくなります。

PlayPoker」を呼び出すと、すべてのブラインドアラートをスケジュールします。

これをテストするのは少し難しいかもしれません。各期間が正しいブラインド量でスケジュールされていることを確認する必要がありますが、time.AfterFuncのシグネチャを見ると、2番目の引数は実行される関数です。 Goでは関数を比較できないため、送信された関数をテストすることはできません。そのため、実行に時間と印刷する量を必要とする、time.AfterFuncの周りにある種のラッパーを書く必要があります。私たちはそれをスパイすることができます。

最初にテストを書く

スイートに新しいテストを追加する

SpyBlindAlerterを作成し、それをCLIに挿入しようとしています。次に、 PlayPokerを呼び出した後、アラートがスケジュールされていることを確認します。

(最初に最も単純なシナリオを実行することを思い出して、次に反復します。)

ここにSpyBlindAlerterの定義があります

テストを実行してみます

テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します

新しい引数を追加しましたが、コンパイラは不満を言っています。 厳密に言えば、最小限のコードはNewCLI*SpyBlindAlerterを受け入れるようにすることですが、少し騙して、依存関係をインターフェイスとして定義しましょう。

そして、それをコンストラクタに追加します。

他のテストはNewCLIBlindAlerterが渡されていないので失敗します。

BlindAlerterをスパイすることは他のテストには関係ないので、テストファイルに追加します。

そして、コンパイルの問題を修正するために他のテストでそれを使ってください。 これを「ダミー"dummy"」と表記することで、テストの読者には重要ではないことが明らかになります。

ダミーオブジェクトは渡されますが、実際に使われることはありません。通常はパラメータリストを埋めるために使われるだけです

これでテストはコンパイルされ、新しいテストは失敗します。

通過するのに十分なコードを書く

これをPlayPokerメソッドで参照できるようにするためにBlindAlerterCLIのフィールドとして追加する必要があります。

テストを通過させるために、BlindAlerterを好きなもので呼び出すことができます。

次は、5人のプレイヤーのために、私たちが望むすべてのアラートのスケジュールを確認したいと思います。

最初にテストを書く

テーブルベースのテストはここではうまく機能し、要件が何であるかを明確に示しています。テーブルを実行し、SpyBlindAlerterをチェックして、アラートが正しい値でスケジュールされているかどうかを確認します。

テストを実行してみる

こんな感じで失敗を重ねるといいですよ。

通過するのに十分なコードを書く

それは、私たちがすでに持っていたものよりもそれほど複雑ではありません。 私たちは今、blindsの配列を反復処理し、増加するblindTimeでスケジューラを呼び出しています

リファクタリング♪

PlayPoker」を少し明確にするために、スケジュールされたアラートをメソッドにカプセル化できます。

最後に、テストは少し不格好に見えます。同じものを表す2つの匿名構造体、ScheduledAlertがあります。それを新しい型にリファクタリングして、いくつかのヘルパーを比較してみましょう。

タイプに String()メソッドを追加したので、テストが失敗した場合にうまく表示されます。

新しいタイプを使用するようにテストを更新します。

自分で「assertScheduledAlert」を実装します。

ここではかなりの時間をかけてテストを作成しており、アプリケーションと統合しないことはややエッチです。これ以上の要件に取り掛かる前に、その問題に取り組みましょう。

アプリを実行してみてください。アプリがコンパイルされず、「NewCLI」への引数が足りないという不満があります。

アプリケーションで使用できるBlindAlerterの実装を作成してみましょう。

BlindAlerter.goを作成し、BlindAlerterインターフェースを移動して、以下の新しいものを追加します

type は、structだけでなく、インターフェイスを実装できることに注意してください。 1つの関数が定義されたインターフェースを公開するライブラリを作成する場合、MyInterfaceFuncタイプも公開することは一般的な慣用法です。

このタイプはあなたのインターフェースも実装するfuncになります。こうすることで、インターフェイスのユーザーは、関数だけでインターフェイスを実装することができます。 空のstructタイプを作成する必要はありません。

次に、関数と同じシグネチャを持つ関数StdOutAlerterを作成し、time.AfterFuncを使用して、それをos.Stdoutに出力するようにスケジュールします。

この動作を確認するには、NewCLIを作成するmainを更新します。

実行する前に、「CLI」の「blindTime」の増分を10分ではなく10秒に変更すると、実際の動作を確認できます。

10秒ごとに予想されるように、ブラインド値が出力されるはずです。 CLIに「Shaun wins」と入力すると、プログラムが予期したとおりに停止することに注意してください。

ゲームは常に5人でプレイされるとは限らないため、ゲームを開始する前に、ユーザー数を入力するようユーザーに促す必要があります。

最初にテストを書く

確認するために、StdOutに書き込まれた内容を記録するプレーヤーの数を入力するように求めています。 これを数回実行しました。os.Stdoutio.Writerであることを知っているので、テストで依存性注入を使用してbytes.Bufferを渡し、私たちのコードが何を書くか見てください。

このテストでは、他の共同編集者についてはまだ気にしていないため、テストファイルでダミーを作成しました。

CLIに4つの依存関係があることに少し注意する必要があります。 これは、多すぎる責任を持ち始めているように思われます。とりあえずそれと共存して、この新しい機能を追加するときにリファクタリングが出現するかどうか見てみましょう。

これが新しいテストです。

mainos.Stdoutになるものを渡し、何が書き込まれるかを確認します。

テストを実行してみます

テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します

新しい依存関係があるため、NewCLIを更新する必要があります

その他のテストは、NewCLIに渡されるio.Writerがないため、コンパイルに失敗します。

他のテスト用に「dummyStdout」を追加します。

新しいテストはそのように失敗するはずです。

成功させるのに十分なコードを書く

CLIに新しい依存関係を追加して、PlayPokerで参照できるようにする必要があります。

最後に、ゲームの開始時にプロンプ​​トを書き込むことができます。

リファクタリング♪

定数に抽出する必要があるプロンプトの重複文字列があります。

テストコードとCLIの両方でこれを使用してください。

次に、番号を送信して抽出する必要があります。 目的の効果があったかどうかを確認する唯一の方法は、スケジュールされたブラインドアラートを確認することです。

最初にテストを書く

いたたたた!たくさんの変更。。

  • StdInのダミーを削除し、代わりに、7を入力するユーザーを表すモックバージョンを送信します。

  • また、ブラインドアラートでダミーを削除して、プレーヤー数がスケジュールに影響を与えていることを確認できるようにしました

  • スケジュールされているアラートをテストします

テストを実行してみます

テストはコンパイルされ、失敗するはずですが、5人のプレーヤーに基づいてゲームをハードコーディングしているため、スケジュールされた時間が間違っていると報告されます。

成功させるのに十分なコードを書く

この作業を行うために必要なすべての罪を自由に犯すことを忘れないでください。 作業用ソフトウェアができたら、作成しようとしている混乱のリファクタリングに取り掛かることができます。

  • numberOfPlayersInputを文字列に読み込みます

  • ユーザーから入力を取得するために cli.readLine()を使用し、次にエラーシナリオを無視して、Atoiを呼び出して整数に変換します。後でそのシナリオのテストを作成する必要があります。

  • ここから、scheduleBlindAlertsを変更して、多数のプレーヤーを受け入れます。次に、ブラインド量を反復するときに「blindTime」に追加するために使用する「blindIncrement」時間を計算します

新しいテストは修正されましたが、システムが機能するのは、ユーザーが数字を入力してゲームを開始した場合に限られるためです。 ユーザー入力を変更してテストを修正する必要があります。 これにより、番号とそれに続く改行が追加されます(これは、現在のアプローチのさらに多くの欠陥を強調しています)。

リファクタリング♪

これは少し恐ろしいことだと思いますか? テストを聞いてみましょう

  • いくつかのアラートをスケジュールしていることをテストするために、4つの異なる依存関係を設定しました。システムに thing の依存関係がたくさんある場合、それは多すぎることを意味します。視覚的には、テストが雑然としていることがわかります。

  • 私には、ユーザー入力の読み取りと、実行したいビジネスロジックとの間をより明確に抽象化する必要があるように感じます

  • より適切なテストは、_このユーザー入力が与えられた場合、正しいタイプのプレーヤーで新しいタイプGameを呼び出すかどうかです。

  • 次に、スケジューリングのテストを新しいGameのテストに抽出します。

最初に「Game」に向けてリファクタリングでき、テストは引き続き成功します。必要な構造変更を行ったら、懸念の分離を反映するためにテストをリファクタリングする方法について考えることができます

リファクタリングに変更を加えるときは、それらをできるだけ小さくして、テストを再実行し続けるようにしてください。

まず自分で試してください。 「Game」が提供するものと「CLI」が行うべきことの境界について考えてください。

今のところは、NewCLIの外部インターフェースを変更しないでください。 テストコードとクライアントコードを同時に変更したくないからです。

これは私が思いついたものです。

「ドメイン」の観点から

  • 何人がプレイしているかを示す「Game」を「開始Start」したい

  • 勝者を宣言して「Game」を「終了Finish」したい

新しいGameタイプはこれをカプセル化します。

この変更により、BlindAlerterPlayerStoreGameに渡されました。 これは、結果のアラートと保存を担当するためです。

私たちのCLIは今ちょうど関係しています

  • 既存の依存関係を使用してGameを構築します(これは次にリファクタリングします)

  • ユーザー入力をGameのメソッド呼び出しとして解釈する

「大きな」リファクタリングを行わないようにしたいと考えています。 これにより、長期間にわたってテストが失敗し、ミスの可能性が高まるためです。 (大規模な分散型チームで作業している場合、これは非常に重要です)

まず最初に、Gameをリファクタリングして、CLIに注入します。 テストを最小限に変更してそれを容易にし、次に、テストをユーザー入力の解析とゲーム管理のテーマに分割する方法を確認します。

今必要なのは、NewCLIを変更することだけです。

これはすでに改善のように感じられます。 依存関係は少なく、私たちの依存関係リストは、CLIが入出力に関係し、ゲーム固有のアクションをGameに委任するという、全体的な設計目標を反映しています。

コンパイルしてみると問題があります。これらの問題は自分で修正できるはずです。 今すぐGameのモックを作成する必要はありません。すべてをコンパイルしてテストするために「実際の」Gameを初期化するだけです。

これを行うには、コンストラクタを作成する必要があります。

これは、修正されたテストの設定の1つの例です。

テストを修正して再び緑に戻るのにそれほどの労力は必要ありませんが、それがポイントです! 次の段階の前にmain.goも修正するようにしてください。

「ゲームGame」を抽出したので、ゲーム固有のアサーションをCLIとは別のテストに移動する必要があります。

これは、CLIテストをコピーするための単なる練習ですが、依存関係は少なくなっています。

ポーカーのゲームが始まったときに何が起きるかの背後にある意図が今やはるかに明確になりました。

ゲームがいつ終了するかについても、テストの上を移動してください。

満足したら、ゲームロジックのテストを移行しました。 CLIテストを簡略化して、意図した責任をより明確に反映できます。

  • ユーザー入力を処理し、必要に応じてGameのメソッドを呼び出します

  • 出力を送信

  • 重要なのは、ゲームがどのように機能するかについての実際の仕組みについて知らない

これを行うには、CLIが具体的なGameタイプに依存しないようにする必要がありますが、代わりにStart(numberOfPlayers)およびFinish(winner)のインターフェースを受け入れます。次に、そのタイプのスパイを作成し、正しい呼び出しが行われたことを確認できます。

ここに、ネーミングが時々厄介であることがわかります。Gameの名前をTexasHoldemに変更します(これが現在プレイしているゲームの 種類 であるため)。新しいインターフェイスはGameと呼ばれます。これは、CLIが私たちがプレイしている実際のゲームに気づいていないという概念と、「Start」および「Finish」したときに何が起こるかを忠実に保ちます。

CLI内の*Gameへのすべての参照を置き換え、それらをGame(our new interface)に置き換えます。 いつものように、リファクタリングしている間は、テストを再実行してすべてが緑色であることを確認します。

TexasHoldemからCLIを分離したので、スパイを使用して、正しい引数で、期待どおりにStartFinishが呼び出されることを確認できます。

Gameを実装するスパイを作成する

ゲーム固有のロジックをテストするすべてのCLIテストを、GameSpyの呼び出し方法のチェックに置き換えます。これは、テストにおけるCLIの責任を明確に反映します。

これは、修正されたテストの1つの例です。 残りを自分で試して、行き詰まった場合はソースコードを確認してください。

懸念事項を明確に分離したので、CLIでIOの周辺のケースをチェックするのが簡単になります。

プレイヤー数の入力を求められたときにユーザーが非数値を入力するシナリオに対処する必要があります。

私たちのコードはゲームを開始してはならず、ユーザーに便利なエラーを出力して終了します。

最初にテストを書く

ゲームが開始しないことを確認することから始めます

GameSpy」にフィールド「StartCalled」を追加する必要があります。これは「Start」が呼び出された場合にのみ設定されます

テストを実行してみます

成功させるのに十分なコードを書く

Atoiと呼ぶところは、エラーをチェックするだけです

次に、ユーザーが間違ったことをユーザーに通知する必要があるため、stdoutに出力される内容を評価します。

最初にテストを書く

前に「stdout」に出力されたものをアサートしたので、今のところそのコードをコピーできます

stdoutに書き込まれる 常に を保存しているので、poker.PlayerPromptがまだ必要です。 次に、追加のものが表示されることを確認します。 今のところ、正確な表現についてあまり気にせず、リファクタリングするときに対処します。

テストを実行してみます

成功させるのに十分なコードを書く

エラー処理コードを変更する

リファクタリング♪

次に、メッセージを「PlayerPrompt」のような定数にリファクタリングします

より適切なメッセージを入れます

最後に、stdoutに送信されたものに関するテストは非常に詳細です。クリーンアップするアサート関数を作成しましょう。

可変長の構文(...string)を使用すると、さまざまな量のメッセージに対してアサートする必要があるため、ここでは便利です。

このヘルパーは、ユーザーに送信されるメッセージに対してアサートする両方のテストで使用します。

一部の「assertX」関数で役立つ可能性のあるテストがいくつかあるので、テストをクリーンアップして読みやすくすることでリファクタリングを練習してください。

時間をかけて、私たちが追い出したいくつかのテストの価値について考えてください。 必要以上のテストは必要ありません。 それらの一部をリファクタリング/削除しても、すべてが機能することを確信できますか?

これが私が思いついたものです。

テストはCLIの主な機能を反映するようになり、何人のプレイヤーに不正な値が入力されたときに何人がプレイし、誰が勝って処理するかという観点からユーザー入力を読み取ることができます。 これを行うことにより、CLIが何をするかを読者に明らかにするだけでなく、何をしないかもわかります。

「ルースが勝つRuth wins」の代わりに「ロイドはキラーLloyd is a killer」にユーザーが入れるとどうなりますか?

このシナリオのテストを記述して成功させることにより、この章を終了します。

まとめ

プロジェクトの簡単な要約

過去5つの章については、かなりの量のコードをゆっくりTDDしました

  • コマンドラインアプリケーションとWebサーバーの2つのアプリケーションがあります。

  • これらのアプリケーションは両方とも、勝者を記録するために「PlayerStore」に依存しています

  • Webサーバーは、最も多くのゲームに勝っている人のリーグテーブルを表示することもできます

  • コマンドラインアプリは、プレーヤーが現在のブラインド値を追跡することでポーカーゲームをプレイするのに役立ちます。

time.Afterfunc

特定の期間後に関数呼び出しをスケジュールする非常に便利な方法。時間を費やす価値がありますtimeのドキュメントを見る。時間を節約するための関数やメソッドがたくさんあるので、作業するのに役立ちます。

私のお気に入りのいくつかは

  • 期間が終了すると、time.After(duration)chan Timeを返します。したがって、特定の時間の後に何かをしたい場合は、これが役立ちます。

  • time.NewTicker(duration)は、チャネルを返すという点で上記と同様の Tickerを返しますが、これは1回だけではなく、すべての期間を「ティック("ticks")」します。これは、N durationごとにコードを実行する場合に非常に便利です。

懸念事項の適切な分離のその他の例

一般的には、ユーザー入力と応答の処理の責任をドメインコードから分離することをお勧めします。 コマンドラインアプリケーションとWebサーバーで確認できます。

テストが乱雑になりました。アサーションが多すぎ、この入力を確認し、これらのアラートをスケジュールなどをして、依存関係が多すぎた。 散らかっていることを視覚的に確認できました。 テストに耳を傾けることはとても重要です

  • テストが乱雑に見える場合は、リファクタリングしてみてください。

  • これを行ってもまだ混乱している場合は、デザインの欠陥を指摘している可能性が高いです。

  • これはテストの真の強みの1つです。

テストと製品コードは少し雑然としていましたが、テストに基づいて自由にリファクタリングできました。

これらの状況に陥ったときは、常に小さなステップを踏んで、変更のたびにテストを再実行することを忘れないでください。

テストコードと本番コードの両方を同時にリファクタリングするのは危険でした。 そのため、インターフェースを変更せずに、最初に本番コードをリファクタリングしました(現在の状態では、テストを大幅に改善することはできませんでした)。 物事を変えている間、私たちができる限りテストに依存することができました。 そして デザインが改善された後、テストをリファクタリングしました。

依存関係リストをリファクタリングした後、設計目標を反映しました。 これは、意図を文書化することが多いという点で、DIのもう1つの利点です。グローバル変数に依存すると、責任が非常に不明確になります。

インターフェースを実装する関数の例

1つのメソッドでインターフェースを定義するとき、ユーザーが関数だけでインターフェースを実装できるように、それを補完するために「MyInterfaceFunc」型を定義することを検討することができます

こうすることで、あなたのライブラリを利用する人は、関数だけであなたのインタフェースを実装することができます。彼らは Type Conversion を使って関数を BlindAlerterFunc に変換し、それを BlindAlerter として使うことができます (BlindAlerterFuncBlindAlerter を実装しているので)

ここで重要なのは、Goでは構造体だけでなく、 にもメソッドを追加できるということです。これは非常に強力な機能で、これを利用すればより便利な方法でインターフェースを実装することができます。

関数の型を定義するだけでなく、他の型の周りに型を定義して、その型にメソッドを追加することができることを考慮してください。

ここでは、非常にシンプルな「ブログ」を実装するHTTPハンドラを作成しました。このハンドラでは、URLパスをキーとして、マップに格納された投稿を表示します。

最終更新

役に立ちましたか?