Chapter 05

アプリケーションモデルの抽出

eagle
eagle
2021.12.26に更新

目標

前回のカウンターアプリケーションの主にコードの中身を改良します。
カウント数の表示形式を変更し、起動時のカウント数の初期値を設定します。


カウンターアプリ

依存の逆転

さて、前回はシンプルなカウンターアプリケーションを作成しましたが、カウント数の表示形式を変えるのが難しいという問題がありました。
そこで、コードを保守しやすい形に書き直していきましょう。

論点はシンプルです。カウント数を表示用テキストに変換するのは簡単ですが、その逆は容易ではありません。
それにもかかわらず、現在はlabelCount.Textを基にしてカウント数を取得しています。
最初からカウント数を数値として保持しておけば話が簡単になるはずです。早速やってみましょう。

カウント数を保持するための変数countを変更可能な変数として宣言します。

let mutable count = 0

クリックされたときに、変数countの値を上書きし、上書き後の値をラベルに表示します。

let subscription =
    this.buttonIncrement.Click
    |> Observable.subscribe (fun e ->
        count <- count + 1
        this.labelCount.Text <- $"Count: %d{count}")

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

MainForm.fs
module MainForm

open App.Designer

type View() as this =
    inherit MainForm()

    let mutable count = 0

    let subscription =
        this.buttonIncrement.Click
        |> Observable.subscribe (fun e ->
            count <- count + 1
            this.labelCount.Text <- $"Count: %d{count}")

    let subscription2 =
        this.buttonDecrement.Click
        |> Observable.subscribe (fun e ->
            count <- count - 1
            this.labelCount.Text <- $"Count: %d{count}")

    override this.Dispose(disposing) =
        if disposing then
            subscription.Dispose()
            subscription2.Dispose()

        base.Dispose disposing

コードの中身は変わりましたが、挙動は前回と同じになるはずです。

ここで重要なのは、今までカウント数を保持していたのはラベルのテキスト(文字列)でしたが、今回のコードではcount変数(数値)が保持しているという違いです。
このように、アプリケーションの状態の本質を表すようなデータをモデルと呼ぶことにします。

共通ロジックの集約

さて、先ほどのコードを見ていると、全く同じ記述をしている箇所があることに気がつきます。

this.labelCount.Text <- $"Count: %d{count}"

カウント数をラベルに表示する部分のコードは全く同じになります。
しかし、よくよく考えてみるとそれ依然に、countの値が変更されたときは毎回ラベルを更新する必要があることに気が付くはずです。

countの値が変更されたらそれに合わせてラベルのテキストも更新する必要があるため、これはひとまとまりの処理と言えます。
そこで、setCountという関数を作成し、共通化することにしましょう。

let setCount nextCount =
    count <- nextCount
    this.labelCount.Text <- $"Count: %d{count}"

変数countの値を上書きする際は、直接<-を使うのではなく代わりにsetCount関数を使うようにします。
これにより、ラベルのテキストも合わせて変更されるようになります。
setCount関数を使うように既存のコードを書き換えてみましょう。

MainForm.fs
  module MainForm
  
  open App.Designer
  
  type View() as this =
      inherit MainForm()
  
      let mutable count = 0
  
+     let setCount nextCount =
+         count <- nextCount
+         this.labelCount.Text <- $"Count: %d{count}"
  
  
      let subscription =
          this.buttonIncrement.Click
          |> Observable.subscribe (fun e ->
-             count <- count + 1
-             this.labelCount.Text <- $"Count: %d{count}")
+             setCount (count + 1))
  
      let subscription2 =
          this.buttonDecrement.Click
          |> Observable.subscribe (fun e ->
-             count <- count - 1
-             this.labelCount.Text <- $"Count: %d{count}")
+             setCount (count - 1))
  
      override this.Dispose(disposing) =
          if disposing then
              subscription.Dispose()
              subscription2.Dispose()
  
          base.Dispose disposing

初期値の変更

さて、ここまではカウント数の初期値を0としていましたが、この初期値を例えば42に変更してみましょう。

- let mutable count = 0
+ let mutable count = 42

ここでアプリケーションを実行してみると、初期値が反映されていません。


初期値の変更が反映されない

これは、フォームデザイナで設定したテキストがそのまま表示されているからです。
初期値を変えるたびに合わせてフォームデザイナの設定を変えるのは現実的ではないため、コード上で初期値をラベルに反映させることにします。

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 <- $"Count: %d{count}"
  
+     do setCount count
  
  
      let subscription =
          this.buttonIncrement.Click
          |> Observable.subscribe (fun e ->
              setCount (count + 1))
  
      let subscription2 =
          this.buttonDecrement.Click
          |> Observable.subscribe (fun e ->
              setCount (count - 1))
  
      override this.Dispose(disposing) =
          if disposing then
              subscription.Dispose()
              subscription2.Dispose()
  
          base.Dispose disposing
  

これにより、コード上で指定した初期値が反映されるようになります。


初期値の反映

表示形式の変更

さて、これでカウント数を数値で保持することができるようになりました。
これにより、カウント数の表示方法を比較的簡単に変更できます。
早速、Count: XXXという表示から現在のカウント数は XXX です。という表示に変更してみましょう。

- this.labelCount.Text <- $"Count: %d{count}"
+ this.labelCount.Text <- $"現在のカウント数は %d{count} です。"


カウンターアプリの実行

初期値が反映されるようにコードで記述しているので、フォームデザイナのほうは変更する必要がありません。


フォームデザイナ

購読解除の定型化

最後に、イベントの購読解除を少し楽にしておきましょう。
まだイベントは2つなので管理できていますが、今後イベントが増えてくると購読のたびにそれを命名しなければなりません。
現在のようにsubscription, subscription2など機械的に命名していくと、管理が煩雑になり解除漏れも発生しやすくなります。
そこで、逐一命名をせずに済む方法を考えましょう。

各購読について個別に変数を用意するのではなく、配列を用意してそこでまとめて購読を管理することにします。
F#の配列は[| ... |]で要素を囲みます。区切り文字はセミコロン;ですが、改行した場合はこれを省略することができます。

let subscriptions = [|
    this.buttonincrement.click
    |> Observable.subscribe (fun e ->
        setCount (count + 1))

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

フォームを使い終わったタイミングで配列subscriptionsの各要素をまとめてDisposeします。
今回はArray.iterを使います。

subscriptions
|> Array.iter (fun subscription -> subscription.Dispose())

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

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

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

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

        base.Dispose disposing

これにより、各購読を命名する必要が無くなりました。
個別にイベントの購読解除タイミングを管理する必要がない場合はこの方法で良いでしょう。