[Swift]Value-type なモデルを使った UNDO の実装(その2: Value-type で モデルを作成)

Value-type を意識して、モデルを作っていきます。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.2.2
  • Xcode 12.4
  • iOS 14.4

value-type で作るモデル

WWDC のビデオとサンプルを確認して、value-type のモデルを使うことで、UNDO/REDO の実装がシンプルになりそうなことが見えてきたので、実際に 作っていきます。

value-type ??

Swift の基本型である Int や Double だけでなく、Array 等も すでに value-semantics の型になっています。つまり、代入時にコピーされるということです。

Swift で使える全ての型が value-type ではないので、モデルを作る時に、気をつけなくてはいけません。

例えば、NSArray は reference-semantics な型です。

value-type でモデルを作る(TODOItem)

例によって、TODO アプリを作る予定なので、以下のような struct を要素 TODOItem として定義します。

TODOItem

TODOItem としては、「タイトル」と「優先度」を持ちます。内部管理用として、id を持っています。

いずれも struct な型であり、value-type です。

モデルを作る(TODOItems)

アプリでは、TODOItem を複数もつ予定なので、TODOItem を複数持つ struct を定義します。

example

TODOItem を そのまま配列 でもつのではなく、[UUID : TODOITem] という Dictionary 型で保持することにしてみます。

配列で持つ方法も考えられますが、以下を考慮し、Dictionary にしています。

value-type の モデルで気をつけるべき点 と Dictionary を採用した理由

Value 型を配列で持つときに、気をつけるべき点があります。それは、

「配列の中の要素を変更したい時に、配列から要素を取り出してしまうと 取り出した瞬間にコピーになってしまう。つまり、以降の変更は取り出した要素の変更となり、配列中の要素の変更にはならない」

ということです。

value 型は、受け渡しの時にコピーが行われ、意図しない変更が行われないという点は良い点でもあるのですが、変更を意図するときには、コピーが発生しているという点に気をつけなければいけません。

reference 型の変更

つまり、上記のようなコードでは、コピーした targetItem を変更しているのであり、items[index] にある要素については、変更されないということです。

配列中の要素を変更したい時には、以下のようにしないと変更されません。

value 型の変更

つまり 配列に保存されている要素を変更する時には、改めて、配列中の index を算出し、その index を使用して変更を行うことが必要となります。

このことは、該当要素の index を探す回数が増えることにつながると考え、index 検索のコストを安くするという目的で、Dictionary を採用しました。

# Dictionary は key を使っての value 取得は、(Dictionary のもつ) 要素数に影響されず、固定時間で取得できるという特徴を持ちます。

# 対象が 100要素程度では、いずれにしても誤差かもしれません・・・

ここまでで TODOItems は、TODOItem 含め すべて value-type で構成されています。

以降では、テストを作りながら、CRUD の API 詳細を決めていきます。CRUD 以外のAPIは 必要となった時に 追加していく方針です。

テスト設計/実装

TODOItems をテストするということで、TODOItemsTests というクラスを作って テストを記述していきます。
作るテストは以下です。

  • TODOItems モデルの作成をテスト(作成でき、要素数 0 であること)
  • TODOItems から [TODOItem] を取得する(配列として取得できること)
  • TODOItems から TODOItem を取得する(id 指定で、TODOItem を取得できること)
  • TODOItem を追加する(追加でき、保存されている TODOItem が作成時に指定したものと一致すること)
  • TODOItem を削除する(削除できること)

TODOItem の取得は、追加・削除のテストで使われるので、暗にテストされるかたちになります。

CRUD それぞれについて、以下のような メソッドを想定してます

  • 配列で取得 → items:[TODOItem]
  • UUID を使って取得 → item(withID:)
  • TODOItem を追加 → add(_ TODOItem)
  • TODOItem を削除 → remove(_ TODOItem)

以下が、TODOItems モデル作成と TODOItem 追加のテストコードです。

TODOItemsTests

まだ add や items, item(withID:) を実装していないので、コンパイルエラーになってしまいますが、テストコードは上記のような感じです。

# 実装中に困った時には、修正していきます。

モデル実装

TODOItems に items, add, item(withID:) を以下のように実装しました。

TODOItems

上記実装で、テスト結果が緑になります。

MEMO
テストをパスしたタイミングで、テストコード中の重複コードをまとめたくなりますが、まとめるのは少し大変です。

モデルが value-type で構成されているので 関数の引数で受け渡しすると作られたコピーが渡されてしまうため、渡した先での変更となり、テストできなくなってしまいます。

テスト設計/実装(続)

以下は、remove のテストコードです。

test_removeItem_addThenRemove_shouldBecomeZeroItems

モデル実装(続)

remove を実装します。

example

上記の実装で、先ほど追加した remove のテストもパスします。

まとめ:value-type でモデルを作る時のポイント

value-type でモデルを作る時のポイント
  • モデルを構成する要素に reference-type を入れ込まないように気をつける
  • 受け渡しするとコピーが発生していることに気をつける。

次回は、ViewModel を作成し、UNDO/REDO ができる ベースを作ります。

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です