オブジェクトのモデリングでは属性(attribute)や関連(association)の多重度(multiplicity)が重要なモデル要素になっています。
今回は、Scalaで多重度を表現する方法について考えます。
多重度
代表的な多重度は以下の4つです。表ではXMLのDTDによる指定方法を併記しました。UMLではもっと細かい条件も指定できますが、実用的にはこの4つで考えていくとよいでしょう。
多重度 | UML | DTD |
---|---|---|
1 | 1 | 表記なし |
0または1 | 0,1 | ? |
1以上 | 1..N | + |
0以上 | 0..N | * |
Scalaの表現
多重度をScalaで表現する代表的な方法を以下の表に示します。
多重度 | Scala | Scalaz |
---|---|---|
1 | - | - |
0または1 | Option | Option |
1以上 | List | NonEmptyList |
0以上 | List | List |
多重度が1の場合は、オブジェクトをそのまま参照すればOKです。
多重度が0または1の場合、Javaだとnull値か否かで判定するのが普通でしたが、ScalaではOptionを使います。nullは例外を除いて使わないのが基本方針です。多重度0または1の表現にnull値を利用すると、変数型やメソッド引数の型、メソッド返却値の型で多重度1の場合と多重度0または1の場合の見分けがつかず、バグを温床になっていました。多重度0または1をOptionで表現することで、この問題を回避することができます。
多重度1以上の場合、ScalaではListなどのコレクションクラスを用いて表現します。表では代表してListを記述していますが、用途によって他にも選択肢があります。Listなどのコレクションクラスは多重度0の場合も使えるので、静的型付けで多重度0を排除することはできません。制約などを用いて別枠で処理を加える必要があります。
多重度1以上の場合、Scalazを使っている場合には、空でないことが保証されたリストであるNonEmptyListを使うことができます。NonEmptyListを使うことで、多重度0のケースを静的型付けで弾くことができるのは大きなメリットです。
多重度0以上の場合、Scala、ScalazともListなどのコレクションクラスを用います。ここでも代表してListを記述していますが、用途によって他にも選択肢があります。
実例
「Validation (14) - オブジェクトの生成」で出てきたPersonクラスを、多重度を使ってより精密な指定をしてみました。
case class Person( name: String, age: Int, address: Option[String], phones: NonEmptyList[String], facsimiles: List[String])
NonEmptyListは、Listと比べると使いにくいので無理をせず多重度1以上の場合も普通にListを使うという選択もあります。
コンテナの選択
多重度が「1以上」または「0以上」の場合、オブジェクトの集りを格納するためのコンテナとしてコレクションクラスを使用しましす。Scalaの代表的なコレクションクラスはListで、多くの場合はListを用いるのがよいのですが、必要に応じて選択していく必要があります。
各、コンテナの特徴は、別の機会に取り上げる予定ですが、ここでも簡単に説明しておきます。Scalaのコンテナは可変版(mutable)と不変版(immutable)がありますが、普通のScalaプログラミングをしていくとほぼ不変版しか使わないでよいので、以下では不変版のみを対象にします。
Seq
- Seq
- シーケンス(デフォルトはList)
- List
- リスト処理向けのシーケンス
- Vector
- ランダムアクセス向けのシーケンス
- Stream
- 遅延評価List
結論から言うと、一般的にはListを選択しておくのが無難です。
Seq
Seqはシーケンスを記述するコンテナです。List, Vector, Streamの親トレイトとなっています。
Seqを具象オブジェクトとして使うと、実装はListが使用されます。このため、型としてListよりもSeqが望ましい場合にSeqをコンテナとして使うことになります。
List
Listは以下の特徴を持っています。
- LinearSeq
- 永続データ構造として使いやすい
- ケースクラス「::」によるパターンマッチング
- 「::」メソッド、「:::」メソッド
「::」メソッドと「:::」メソッドはそれぞれSeqの「+:」メソッドと「++」メソッドと同じなので、特にアドバンテージというわけではありません。Lispのリスト処理に似たような記述ができるので、見栄えがよくなるという効用が期待できます。
Listは再帰的かつ永続データ構造のコンテナなので、関数型で多用する再帰的なアルゴリズムとの相性がよいのが美点です。また、ケースクラス「::」によるパターンマッチングが必要かどうかもListを選ぶ際のポイントになります。
Scalaプログラミングでは、「迷ったらList」と考えておくとよいでしょう。
Vector
Vectorは以下の特徴を持っています。
- IndexedSeq
IndexedSeqは、インデックスによるランダムアクセス向きのSeqであることを示しています。また、データを配列で持つので大規模データを効率よく格納、アクセスすることが期待できます。
まとめると以下のユースケースに適したコンテナです。
- 永続データ構造的なアルゴリズムを使わない
- 大規模データ
- ランダムアクセスを行う
Stream
Streamは遅延評価のListで以下の特徴を持っています。
- LinearSeq
- 遅延評価
- オブジェクト「#::」によるパターンマッチング
- 「#::」メソッド、「#:::」メソッド
Streamは遅延評価なので用途が限定されます。
Set
Setは集合、つまりデータの重複がないオブジェクトの集りを格納するコンテナです。
- Set
- 集合(デフォルトはHashSet)
- HashSet
- 実装にハッシュを用いた集合(順序が保証されない)
- ListSet
- 実装にListを用いた集合(順序が格納逆順)
- SortedSet
- 順序が整列順になる集合(デフォルトはTreeSet)
- TreeSet
- 実装に木構造を用いた集合(順序が整列順)
Setは、個々のコンテナの特徴がはっきりしているので、用途に応じて選択していけばよいでしょう。
実例2
色々なコンテナを説明してきました。例えば、Setを用いてPersonを以下のように表現することもできます。
case class Person( name: String, age: Int, address: Option[String], phones: Set[String], facsimiles: Set[String])
この場合には、phones(電話番号)とfacsimile(FAX番号)はそれぞれ重複した値を持たないことを静的型付けで保証しています。
0 件のコメント:
コメントを投稿