Chapter 06

ステップカウンター

eagle
eagle
2021.12.26に更新

目標

ここまではカウント数を1だけ変動させることができましたが、次はステップ数も画面上で変更できるようにします。

ステップ数コントロールの配置

DesignerプロジェクトのMainForm.csを選択してフォームデザイナを表示します。
ツールボックスからNumericUpDownコントロールを追加し、以下のようにプロパティを設定します。

  • NameプロパティをnumericUpDownStepとします。
  • ModifierプロパティをPublicとします。
  • その他お好みで文字色やサイズなどを変更します。


NumericUpDownコントロールの配置

ステップ数の反映

それでは変動数を固定値の1から、先ほど追加したコントロールの値となるように書き換えてみましょう。
NumericUpDownコントロールの数値はValueプロパティで取得できます。
ただし、その数値の型はdecimalなので注意が必要です。
decimalは16バイトの10進浮動小数点数を表す型なので、整数だけでなく一部の小数を表すこともできます。

NumericUpDownコントロールのDecimalPlacesプロパティで小数点以下の桁数を指定することもできますが、今回、カウント数は整数とするのでDecimalPlacesは初期値の0のままとします。
したがって、Valueプロパティの値は丸めを気にせずにintへ変換することができます。

MainForm.fs
  module MainForm
  
  open App.Designer
  
  type View() as this =
      inherit MainForm()
  
      let mutable count = 42
  
      let setCount nextCount =
          count <- nextCount
          this.labelCount.Text <- $"現在のカウント数は %d{count} です。"
  
      do setCount count
  
      let subscriptions = [|
          this.buttonIncrement.Click
          |> Observable.subscribe (fun e ->
-             setCount (count + 1))
+             setCount (count + (this.numericUpDownStep.Value |> int)))
  
          this.buttonDecrement.Click
          |> Observable.subscribe (fun e ->
-             setCount (count - 1))
+             setCount (count - (this.numericUpDownStep.Value |> int)))
  
      |]
  
      override this.Dispose(disposing) =
          if disposing then
              subscriptions
              |> Array.iter (fun subscription -> subscription.Dispose())
  
          base.Dispose disposing

これにより、ステップ数を自由に変更できるようになりました。


ステップ数

しかし、ボタンの表示はステップ数に関わらず「+1」「-1」のままです。
この問題は後で解決します。

依存の逆転

さて、今回ステップ数をdecimal型としてnumericUpDownStepコントロールのValueに保持しています。
しかし、実際にはステップ数は整数なのでした。つまり、int型で保持するのが理想です。
そこで、カウント数のときと同じように、コントロールに状態を持たせるのではなく、変数を用意してステップ数の状態を格納させましょう。

まずステップ数を格納するための変数stepを変更可能な変数として定義します。
合わせてsetStep関数も用意し、初期値の反映も忘れずに行います。

let mutable step = 1

let setStep nextStep =
    step <- nextStep
    this.numericUpDownStep.Value <- nextStep |> decimal

do setStep step

ボタンがクリックされたときの処理で、直接numericUpDownStep.Valueを参照する代わりにstep変数を使用します。

let subscriptions = [|
    this.buttonIncrement.Click
    |> Observable.subscribe (fun e ->
        setCount (count + step))

    this.buttonDecrement.Click
    |> Observable.subscribe (fun e ->
        setCount (count - step))
|]

最後に、numericUpDownStepコントロールのValueプロパティが変更されたことを購読し、step変数が常に最新の値になるように上書きしましょう。

this.numericUpDownStep.ValueChanged
|> Observable.subscribe (fun e ->
    setStep (this.numericUpDownStep.Value |> int))

最終的に次のようなコードになります。

MainForm.fs
module MainForm

open App.Designer

type View() as this =
    inherit MainForm()

    let mutable count = 42

    let setCount nextCount =
        count <- nextCount
        this.labelCount.Text <- $"現在のカウント数は %d{count} です。"

    do setCount count

    let mutable step = 1

    let setStep nextStep =
        step <- nextStep
        this.numericUpDownStep.Value <- nextStep |> decimal

    do setStep step

    let subscriptions = [|
        this.buttonIncrement.Click
        |> Observable.subscribe (fun e ->
            setCount (count + step))

        this.buttonDecrement.Click
        |> Observable.subscribe (fun e ->
            setCount (count - step))

        this.numericUpDownStep.ValueChanged
        |> Observable.subscribe (fun e ->
            setStep (this.numericUpDownStep.Value |> int))
    |]

    override this.Dispose(disposing) =
        if disposing then
            subscriptions
            |> Array.iter (fun subscription -> subscription.Dispose())

        base.Dispose disposing

試しに動かしてみましょう。


ステップカウンター

step変数やsetStep関数を導入することにより、カウント数と同様の方法でステップ数を管理することができました。
しかし、step変数の導入前と導入後を見比べてみると、むしろstep変数を導入する前のほうがコードが短いため単純に感じられます。
もう少しコードを整理してみましょう。

モデルの抽出

前回、アプリケーションの状態の本質を表すようなデータをモデルと呼ぶことにしました。
今回の例で言うと、アプリケーションのモデルはカウント数とステップ数の2つです。

そこで、このモデルを明示的にコードで表してみます。
F#のレコードを用いてModel型を定義します。

type Model =
    { Count: int
      Step: int }

このモデルの初期値を次のように定義できます。

let initialModel =
    { Count = 42
      Step = 1 }

このモデルを使って、ここまでで定義してきたstepcountをまとめてmodelという1つの変数に統一できます。
また、setStep関数とsetCount関数をまとめてsetModelという1つの関数に統一できます。
やってみましょう。

let mutable model = initialModel

let setModel nextModel =
    model <- nextModel
    this.labelCount.Text <- $"現在のカウント数は %d{model.Count} です。"
    this.numericUpDownStep.Value <- model.Step |> decimal

do setModel model

ところで、「+1」「-1」ボタンのテキストをステップ数の変更に合わせるのもここで行っておきましょう。

  let mutable model = initialModel
  
  let setModel nextModel =
      model <- nextModel
      this.labelCount.Text <- $"現在のカウント数は %d{model.Count} です。"
      this.numericUpDownStep.Value <- model.Step |> decimal
+     this.buttonIncrement.Text <- $"+%d{model.Step}"
+     this.buttonDecrement.Text <- $"-%d{model.Step}"
  
  do setModel model

最後に、アプリケーションの状態を変更するときは統一的にsetModel関数を使うように変更します。

let subscriptions = [|
    this.buttonIncrement.Click
    |> Observable.subscribe (fun e ->
        setModel
            { model with
                Count = model.Count + model.Step })

    this.buttonDecrement.Click
    |> Observable.subscribe (fun e ->
        setModel
            { model with
                Count = model.Count - model.Step })

    this.numericUpDownStep.ValueChanged
    |> Observable.subscribe (fun e ->
        setModel
            { model with
                Step = this.numericUpDownStep.Value |> int })
|]

レコード式を用いると、既存のレコードから一部のフィールドのみを変更することができます。

最終的に次のようなコードになります。

MainForm.fs
module MainForm

open App.Designer

type Model =
    { Count: int
      Step: int }

let initialModel =
    { Count = 42
      Step = 1 }

type View() as this =
    inherit MainForm()

    let mutable model = initialModel

    let setModel nextModel =
        model <- nextModel
        this.labelCount.Text <- $"現在のカウント数は %d{model.Count} です。"
        this.numericUpDownStep.Value <- model.Step |> decimal
        this.buttonIncrement.Text <- $"+%d{model.Step}"
        this.buttonDecrement.Text <- $"-%d{model.Step}"

    do setModel model

    let subscriptions = [|
        this.buttonIncrement.Click
        |> Observable.subscribe (fun e ->
            setModel
                { model with
                    Count = model.Count + model.Step })

        this.buttonDecrement.Click
        |> Observable.subscribe (fun e ->
            setModel
                { model with
                    Count = model.Count - model.Step })

        this.numericUpDownStep.ValueChanged
        |> Observable.subscribe (fun e ->
            setModel
                { model with
                    Step = this.numericUpDownStep.Value |> int })
    |]

    override this.Dispose(disposing) =
        if disposing then
            subscriptions
            |> Array.iter (fun subscription -> subscription.Dispose())

        base.Dispose disposing


実行結果

ここまででアプリケーションの状態を1つのモデルに集約することができました。
これにより、アプリケーションが予期せぬ情報を画面に表示しているとき、次の2つの観点から問題の切り分けができます。

  1. 現在のアプリケーションの状態(すなわち型Modelの値)は正しいか?
  2. アプリケーションの状態を正しく描画できているか?(すなわちsetModel関数)