ASP MVC5

ASP.NET MVC5を採用したシステムを手がけています。言語は C# を使用しています。

前回手がけた有名どころのフレームワークSymfonyとの比較にも触れます。

コンパイル

PHPでは実行時にコンパイルが実行されるので大きなライブラリーを使用するのにためらいがあります。
ASP.NET(C#)ではコンパイル済みになっているのでパフォーマンスがよいと思われます。

メモリ

Symfony でバッチ処理した場合、メモリ食いが激しく、数万件の処理でも問題があったので、Doctrine管理外のプレーンオブジェクトを使う、クエリで最低限必要なカラムのみ読み込むなどの工夫が必要でした。経験上C#で数万件の処理はオンメモリで問題なく扱えます。

テンプレートエンジン

Razerが標準で使えます。インテリセンスも効いて Typo を防げます。
但し、デザイナーとの分業を考えたとき、Razerは開発者向けなので継続的にデザイナーに編集を依頼するのは難しいと思います。
この点はSymfonyが採用しているtwigでも事情はあまり変わりません。

フレームワークの柔軟性

テンプレートエンジンはRazer、O/RマッピングはEntity Frameworkが標準で使えるようになっていますが、これを差し替えることができます。
テンプレートエンジンはデザイナーと協業できるテンプレートに差し替え、O/Rマッピングは普段使用しているフレームワークに差し替えました。
差し替え時、ASP.NETが持っているバリデーションの機能を使えるようにするため工夫が必要でした。差し替えのためにはフレームワーク内部の動作を理解する必要があります。

ASP.NET MVC5採用決定時に、ASP.NET Coreについても検討したのですが、利用予定のActiveReportsが対応していなかったので不採用となりました。

参照

Symfony(PHP フレームワーク比較)

phpでSymfonyを採用したシステムを開発しました。開発を通じて感じた利点・欠点を紹介します。

次の点が優れていると思いました。

  • DoctrineにO/Rマッパーの機能が一通り揃っている。

  • 標準的なフォーム部品に相当するクラスが用意され、しかもカスタマイズしやすい。

  • データベース・ドメイン間、ドメイン・ビュー間の変換をカスタマイズしやすい。

  • フォームが階層構造に対応しているため、マスター・ディテールフォーム・部品表などのツリー構造を持つデータに容易に対応できる。

    次のような点が不便と思いました。

  • 学習コストが大きい。

  • DQLが使いにくい。

  • エンティティで関連を操作する際、参照が必要になるので回りくどい処理が必要になる場合がある。

  • エンティティがDIで生成されるので大量の行を操作すると処理が重い。

  • データベースからエンティティを生成する機能があるが、必ずしも適切なエンティティを生成しない。

  • 既定ではサブエンティティ(明細行)を保持する配列のキーとしてカラムの値が使われ、それがINPUT要素の名前の一部に使われ、文字によってはエラーになるので、これを回避しなければならない。

他のフレームワークと比較しました。Symfony のほか Laravel, ASP.NET5 の経験はありますが。 開発経験がない他のフレームワークはドキュメント等や各種Webサイトの情報を元に判断しました。

項目 Symfony 2.8
Laravel 9.6 CakePHP 3 ASP.NET Core 備考
O/Rマッピング コンポーネント Doctrine Eloquent 一体 EntityFramework
問い合わせ言語
DQL(外部DSL)
QueryBuilder(内部DSL)
QueryBuilder(内部DSL) QueryBuilder(内部DSL) Linq(内部DSL)
関連エンティティの暗黙的ロード
  • 遅延
  • 特殊遅延(行毎読み込み・Count関数発行)
  • 即時
  • 遅延
× ×
関連エンティティの明示的ロード クエリーで指定(Join) クエリーで指定(with) クエリーで指定(contain) クエリーで指定(Include)
埋め込みバリュー × ○(ComplexType)
トラッキング × ×
トラッキング指定 エンティティ毎 ロード毎 ロード毎に指定できた方がパフォーマンスチューニングしやすい。
POPO/POCO × × 特定クラスを継承しなくても良いかどうか。
アイデンティティ・マッピング × 不明
ドメインの構造 構造
  • Entity
  • Repository
    (Finder+Updater)
  • EntityManager
    (Updater)
  • ActiveRecord
    (Entity+Finder+Updater)
  • ActiveRecord
    (Entity+Updater)
  • Table
    (Finder+Updater)
バリデーション指定 アノテーションまたは設定ファイル 内部DSL 内部DSL 属性
ビュー 標準テンプレート twig Blade PHP Razor
データ型変換 不明
マスター・ディテールフォーム × × × 標準的な枠組みが用意されているかどうか。
参照

イベントハンドラのフォームからの分離

対話型システムで Windows Forms をはじめイベント駆動で処理を記述するプラットフォームでは、ドメインロジックからキーの処理まで雑多な処理が集中しがちになるといった反省から、レイヤーアーキテクチャを採用しています。書籍を読んだりインターネットの情報を吸収してそれなりの効果を上げていると考えています。

雑多な処理を分離するもう一つの方法があります。それはイベントハンドラをフォームから分離してしまうものです。下記コードはWindows Forms に対応するもので、エンターキーでフォーカス移動したいとの要望にこたえるものです(但し当方のライブラリの呼び出しがありますのでそのまま使えるものではありません)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// <summary>
/// Enter キーが押されたら Tab キーに変換します。
/// </summary>
public class EnterToTabHandler
{
Form Form;

void FormKeyDown(object sender, KeyEventArgs e)
{
if (e.Handled) return;
if (e.KeyCode == Keys.Enter && (e.Modifiers & (Keys.Alt | Keys.Control)) == 0) {
var control = ViewUtility.GetCurrentControl(Form);
if (control is TextBoxBase && !(control as TextBoxBase).Multiline ||
control is ComboBoxBase && !(control as ComboBoxBase).DroppedDown ||
control is CheckBox || control is RadioButton) {
SendKeys.Send("{TAB}");
e.SuppressKeyPress = true;
}
}
}

/// <summary>
/// <see cref="EnterToTabHandler"/> 型のインスタンスを作成します。
/// </summary>
/// <param name="form">処理対象となるフォーム</param>
public EnterToTabHandler(Form form)
{
Form = form;
form.KeyDown += FormKeyDown;
}
}

利用するフォーム側ではEnterToTabHandlerのコンストラクタ呼び出しを1行記述だけです。

他にもボタンを押したらモデルのコマンドを実行するもの、ファンクションキーを押したら特定のボタンをクリックしたことにするものなど、多くの処理をフォームから追い出すことができます。

この方法は次のようなメリットがあります。

  • 継承を気にしないで使うことができるので機能間の分離が良い
  • 継承とは異なり、多重度に縛られない(モデルのコマンドを実行する例ではボタンとコマンドの組み合わせをいくつ持っても良い)

そしてこの方法はAccessVBAでも使えて当方では長く実績があったものです。

現在は Windows Forms でレイヤーアーキテクチャと組み合わせて使っています。従来はビューとモデルがパラレル継承階層となっていて硬直していたのですが、ビューの機能継承に縛られなくなったので、ビューはもっぱらデザインの継承階層、モデルは編集、一覧など機能での継承階層と全く別のベクトルで継承するようにしました。

アイデンティティ・マッピングと弱参照

データベースアプリケーションのテクニックの1つにアイデンティティ・マッピングがあります。これはデータベース上の行とメモリー上のオブジェクトを1対1に対応させようというものです。すべての行をメモリーに読み込むという意味ではなく、1つの行に対応するオブジェクトを複数作成しない点に主眼が置かれています。一意マッピング・恒等写像などと訳すことができますが、ここではアイデンティティ・マッピングと呼んでおきます。

1つの行に対応するオブジェクトがメモリー上に複数ある状況というのは、複数画面で同じ行を参照している場合に起こりやすいです。例えば一覧画面でロードしたいずれかの行が別画面で表示されていたりロジック実行で必要となったりといった状況です。

1つの行に対応するオブジェクトがメモリー上に複数あると、行を更新したときに同じ行に対応する別のオブジェクトも更新する必要があります。これを怠ると更新前のオブジェクトが表示されたり、更新前のオブジェクトを使ってロジックが実行されるといった不具合が発生します。アイデンティティ・マッピングではオブジェクト更新時に同じ行を表す別のオブジェクトがありませんのでこのような配慮が不要となります。

アイデンティティ・マッピングを実現するためにテーブルの主キーをキーに持ち、行に対応するオブジェクトを値に持つディクショナリーを用意することが考えられます。データベースからテーブルを読み取ったときは、このディクショナリと突き合わせ処理して、同一キーの要素があればオブジェクトのプロパティをデータベースから読み取った値に更新します。同一キーの要素がなければディクショナリーに追加します。リポジトリーにこのような仕組みを持たせておけば、すべての読み取り処理でアイデンティティ・マッピングを施すことができます。

この仕組みで問題となるのが行に対応するオブジェクトを保持するために設けたディクショナリーは要素を除外することができず、プログラム起動後に1度でも読み取った行が居座り続けるためメモリーを圧迫することです。NetFrameworkのガーベジコレクターはどこからも参照されなくなったオブジェクトを破棄対象とみなしますが、この場合ディクショナリーが参照を持っているので破棄対象になりません。

そこでこのディクショナリーの要素を弱参照するのです。 どこかの画面から参照されていれば要素は破棄されませんがどの画面からも参照されなくなりディクショナリーが弱参照するのみとなればガーベージコレクターによる破棄対象となります。

下記は要素を弱参照で持つディクショナリの例です(C# 6.0文法を使用しています)。インデクサーは読み取り専用にしています。新しい要素は内部で保持するItemCreatorによって作成します。RemoveDeadItems()によって破棄済みの要素を除外します。このようなディクショナリーを使って読み取ったデータを保持します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/// <summary>
/// 弱い参照によって、キーに対応する値を保持します。
/// </summary>
/// <typeparam name="TKey">キーの型</typeparam>
/// <typeparam name="TValue">値の型</typeparam>
/// <remarks>
/// このオブジェクト以外から参照されなくなった値はガーベジコレクションの対象となります。
/// ガーベジコレクションによって破棄された値を参照したときは、再作成します。
/// 全ての値を保持するとヒープメモリが不足する恐れがある場合に使用します。
/// </remarks>
public class WeakReferenceDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>
where TValue : class
{
IDictionary<TKey, WeakReference<TValue> UnderlyingDictionary = new Dictionary<TKey, WeakReference<TValue>();

/// <summary>
/// キーに対応する値を作成する関数を取得します。
/// </summary>
public Func<TKey, TValue> ItemCreator { get; private set; }

/// <summary>
/// 破棄されているキーのシーケンスを取得します。
/// </summary>
IEnumerable<TKey> GetDeadKeys()
{
foreach (var kv in UnderlyingDictionary) {
TValue target;
if (!kv.Value.TryGetTarget(out target)) yield return kv.Key;
}
}

/// <summary>
/// 破棄された値を持つ要素を除外します。
/// </summary>
public void RemoveDeadItems()
{
lock (this) {
foreach (var key in GetDeadKeys().ToArray()) {
UnderlyingDictionary.Remove(key);
}
}
}

/// <summary>
/// キーに対応する値を取得します。
/// </summary>
/// <param name="key">キー</param>
/// <returns>キーに対応する値</returns>
/// <remarks>
/// キーに対応する値がないかまたは既に破棄されている場合は <see cref="ItemCreator"/> によって作成します。
/// </remarks>
public TValue this[TKey key]
{
get
{
lock (this) {
WeakReference<TValue> value;
TValue target;
if (UnderlyingDictionary.TryGetValue(key, out value) && value.TryGetTarget(out target)) return target;
target = ItemCreator(key);
UnderlyingDictionary[key] = new WeakReference<TValue>(target);
return target;
}
}
}

/// <summary>
/// 格納されている要素の数を取得します。
/// </summary>
public int Count => UnderlyingDictionary.Count;

/// <summary>
/// 格納されているキーのシーケンスを取得します。
/// </summary>
/// <remarks>
/// キーに対応する値は破棄されている場合があります。
/// </remarks>
public IEnumerable<TKey> Keys => UnderlyingDictionary.Keys;

/// <summary>
/// 格納されている値のシーケンスを取得します。
/// </summary>
public IEnumerable<TValue> Values
{
get
{
foreach (var kv in UnderlyingDictionary) {
TValue target;
if (kv.Value.TryGetTarget(out target)) yield return target;
}
}
}

/// <summary>
/// コレクションを反復処理する列挙子を返します。
/// </summary>
/// <returns>コレクションを反復処理するために使用できる <see cref="IEnumerator{T}"/></returns>
public IEnumerator<KeyValuePair<TKey, TValue> GetEnumerator()
{
foreach (var kv in UnderlyingDictionary) {
TValue target;
if (kv.Value.TryGetTarget(out target)) yield return new KeyValuePair<TKey, TValue>(kv.Key, target);
}
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();

/// <summary>
/// <see cref="WeakReferenceDictionary{TKey, TValue}"/> 型のインスタンスを作成します。
/// </summary>
/// <param name="itemCreator"> キーに対応する値を作成する関数</param>
public WeakReferenceDictionary(Func<TKey, TValue> itemCreator)
{
ItemCreator = itemCreator;
}
}

PhpStorm

PHPを使った開発では、しばらくNetBeansを使っていました。

PHPについては正確に補完してくれているのですが、JavaScriptの補完はあまり当てになりません。

情報を探しているとPhpStormというIDEが優れているとの書き込みをよく見かけるので使ってみました。
その結果、JavaScriptについても正確に補完してくれることがわかりました。NetBeansに比べて優れている点としては、

  • JavaScriptの補完が正確
  • 関数の順序変更が簡単
  • Ctrl-Wによる選択領域の拡大が多段階にできる
  • ソースコードフォーマットが細かく設定できる
  • 箱形選択時のカーソル移動が合理的(各行で異なる長さの単語があってもCtrl-Wで選択してくれるなど)

などです。NetBeansとは違って有償なのですがそれだけの価値はあったと判断しています。

参照

PHPのトレイトでダイヤモンド継承

PHPのトレイトを使うと多重継承に相当する処理を記述できることを3種類の帳票の例で紹介しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
trait A
{
public function a(){}
}
trait B
{
use A;
}
trait C
{
use A;
}
class D
{
use B, C;
} //メソッド競合エラー

$d = new D();

トレイトを使い込む内に1つの制限が見えてきました。それはダイヤモンド継承ができないことです(現在ではこの問題は解消されています。恐らくPHP7.3で解消されたものと思われます)。

右のコードで、クラスDは、トレイトBとトレイトCを取り込んでいます。トレイトB、トレイトC共にトレイトAを取り込んでいます。トレイトAはメソッドaを持っています。この場合、PHPではメソッドaが衝突しているという意味のエラーが発生します。トレイトBとトレイトCのそれぞれにメソッドaがあるとみなすようです。

私は共通のトレイトAの実体は1つなのでエラーにならないことを期待しました。includeinclude_onceに例えれば、includeではなくinclude_onceに相当する動作を期待したのですが実際にはincludeに相当する動作をします。

この回避方法としてはトレイトB、トレイトCでトレイトAを取り込まないで、クラスDでトレイトABCを取り込むことが考えられます。

但しトレイトB、トレイトCがトレイトAを取り込むことがコード上で明示されないというデメリットがあります。これについてはコメントで注釈しています。

トレイトB、トレイトCをコーディングするとき、トレイトAに関するメンバーをIDEが補完してくれない可能性があります。PHPStormではトレイトをuseしているクラス、この場合はクラスDを通じてAのメンバーを候補に表示してくれます。Eclipseは調査していません。

参照

PHPでトレイトを使ってみた

C#, Javaなど多重継承ができない言語では、複数のクラスをまとめて新たなクラスを作るのに多くの委譲コードを書く必要があります。PHPも多重継承はできません。しかし、version 5.4 からトレイトが導入されましたのでこれを使うと委譲コードを減らすことができます。

エンティティクラスへの応用

項目グループ 帳票1 帳票2 帳票3
A
B
C
D
E

3種類の帳票に対応するエンティティを定義するのにトレイトを使いました。

それぞれの帳票は多くの項目印刷項目があり、全ての帳票に印刷する項目もあれば、2帳票でのみ印刷する項目や1帳票でのみ印刷する項目もあります。各帳票の印刷項目は意味的なまとまりがあるものにグループ化することができ、帳票はこれらのグループを組み合わせたものになります。レイアウト上では項目の一つ一つは細い罫線で区切られていて、
グループは太い罫線で区切られています。
帳票とグループの関係は右表のようになっているとします。

もし継承だけでエンティティクラスを作成するのであれば、項目グループAに含まれる印刷項目のみを基本クラスに置き、項目グループB~Eに含まれる印刷項目はそれぞれ帳票1~3のクラスに置くことが考えられます。

この方法は下記の問題があります。

  • 2帳票で印刷する項目グループB, Cの実装が重複する
  • 項目グループのまとまりを表せない

これを解決するために、各項目グループA~Eをクラスとして、これらのクラスを使って帳票1~3のクラス(以後具象エンティティ)を作成することが考えられます。各帳票クラスの項目グループAを基本クラスとした場合、具象エンティティには項目グループB~Eに定義した印刷項目にアクセスする委譲コードを書く必要があります。委譲コードがあまりにも多いので、各項目グループA~Eをクラスにすることを諦めて最初の方法に戻ってしまいそうです。

1
2
3
4
5
6
7
8
9
10
11
class Report1 {
use A, B;
}

class Report2 {
use A, C, D, E;
}

class Report3 {
use A, B, C;
}

各項目グループA~Eをクラスではなくトレイトとした場合、委譲コードは不要となり無理なく実装できます。

リポジトリへの応用

リポジトリでは以下のようなさまざまな機能が必要となる場合があります。

  • データベースにアクセスする機能
  • データベース以外のストレージからアクセスする機能
  • リポジトリ内にデータをキャッシュする機能
  • キーを指定してエンティティを取得する機能
  • 抽出条件を指定してロードする機能

各エンティティクラスに対応するリポジトリ(以後具象リポジトリ)ではこれらの機能のすべてが必要になるのではなく、いくつかの機能のみが必要となります。その組み合わせはさまざまです。上記に挙げた機能をトレイトにしておけば、具象リポジトリはトレイトの組み合わせによって実装することができます。

実感したメリット

直接的には委譲コードを減らすことができるメリットがあります。それだけではなく下記のようなメリットが大きいと感じました。

  • トレイトはクラスに比べて粒度を細かくしやすいのでそれぞれの機能が明快になる。
  • まるでLEGOブロックを組むようにトレイトを組み合わせてクラスを作ることができる。
参照

ドメインオブジェクトを生成するファクトリ

ドメインオブジェクトがドメイン層の機能を果たす様にするため、その生成時に複雑な処理が必要になります。当方では生成時に次のような処理を行っています。

  • 所有する値オブジェクトの生成
  • 所有する子エンティティリストの生成
  • 参照しているエンティティへの Null Object の生成
  • 変更通知機能(INotifyPropertyChanged)の注入
  • 編集・取消機能(IEditableObject)の注入
  • 変更トラッキング機能の注入
  • 継承の解決

これら生成時の処理の多くは開発するアプリケーションのドメインに依存しない処理となります。そこで生成の機能をファクトリにまとめ、ドメインオブジェクトから分離しています。

生成するドメインオブジェクトはその機能によって下記の3種類に分類しています。

依存性注入の分類
分類 目的 動作
表示用 一覧表示および、一括更新のバッファとして使用 プロパティは初回アクセス時に遅延評価
編集用 編集画面で編集に使用 変更通知機能、変更・取消機能・変更トラッキング機能を注入
Null Object用 あるオブジェクトが別のエンティティを参照するプロパティを持っている場合(例えば、伝票エンティティが得意先マスタプロパティを持っている場合)、プロパティの初期値および該当するエンティティがない場合の値として使用 プロパティリードは常に型の既定値を返す。プロパティライトは例外発生、Null Objectは1つの型に1つのインスタンスのみ生成されることを保証

これらはUnity依存性注入コンテナを利用した3種類のビヘイビアを用意して、生成時に使い分けています。

生成時の処理の内、継承の解決はドメインに依存します。データベースで継承を表す方法として、シングルテーブル継承クラステーブル継承具象テーブル継承があります。このうちシングルテーブル継承を当方はよく使います。例えば販売管理で売上と入金がありこれを別のテーブルで管理しないで1つの伝票というテーブルで管理し、伝票区分コードというカラムによって売上と仕入を識別しています。データベースからエンティティをロードするとき伝票区分コードによって売上オブジェクトを作成するか仕入オブジェクトを作成するかを振り分けます。

参照

パラレル継承階層

レイヤーアーキテクチャーで使用されるテクニックにレイヤースーパータイプというものがあります。各レイヤーは基本となるクラスを設けて、具象画面用のクラスはこれらの基本クラスを継承します。この基本となるクラスをレイヤースーパータイプと呼びます。複数のレイヤーでレイヤースーパータイプを使用するとそれぞれのレイヤーで継承階層ができます。それぞれのレイヤーは一方を継承すれば別のレイヤーも継承が必要になる場合があります。このような状態をパラレル継承階層と呼び、アンチパターンの1つとされています。

このような状況をアンチパターンとみなすかパターンとみなすかメリット・デメリットを天秤に掛ける必要があるでしょう。NetFrameworkではDataTableTypedDataTableDataRowTypedDataRowにパラレル継承階層が見られます。入れ物と中身の関係で中身を継承すると入れ物も継承しなければならないといった場合がありますので私はアンチパターンと言えない場合もあると考えています。

別の継承階層を参照している継承階層では継承のたびにダウンキャストが発生します。この場合はView側でアプリケーション層のクラスを参照するのにダウンキャストが発生します。

パラレル継承階層においてダウンキャストを無くすためにはコーディングの工夫で対応することはできず、言語が対応する必要があります。

例えば下記のような機能があると良いのです。

継承可能な型引数をサポートすること
C#等現在普及している言語は、継承によってジェネリックの型引数を実体化すると型引数が固定されてしまい、さらに継承する場合には型引数をオーバーライドすることができません。ダウンキャストを無くすためにはオープンジェネリックのままで型引数を実体化できる必要があります。
ジェネリックワイルドカードをサポートすること
継承可能な型引数をサポートするとリスコフの代入原則(一般には置換原則と邦訳されている)との整合性をどうするかが問題になります。型安全を保ったまま他の階層の継承されたクラスを操作できるようにするためには、Javaで実装されているジェネリックワイルドカードをサポートする必要があります。

残念ながら現状ではダウンキャストを無くすことはできません。

値オブジェクト(Value Object)の実装

エリック・エバンズのドメイン駆動設計によると、オブジェクトはエンティティ(参照オブジェクト)値オブジェクトに分類できます。エンティティは一意性・継続性が問われるオブジェクト、それに対して値オブジェクトは一意性は問われず属性のみが注目されるオブジェクトです。

当方のドメイン層の設計では、データベースのテーブルに相当するオブジェクトはエンティティ、テーブル内のカラムの1つや複数のカラムを塊として扱うときは値オブジェクトとして表われます。

カラムは通常文字列・数値・日付など組み込み型で表されます。これらは値オブジェクトです。あえて新たな型を作成したい場合として、特殊なバリデーションを施したいときがあります。例えば電話番号に数値とハイフン以外を許可しないといった場合、そのようなバリデーションを組み込んだ電話番号型を作っておけば再利用できます(実際の設計とは異なります)。これも値オブジェクトです。

得意先マスタ
カラム名 プロパティ名 型名
得意先コード 得意先コード 整数
得意先名 得意先名 文字列
届け先郵便番号 届け先 送付先
届け先住所1
届け先住所2
請求先郵便番号 請求先 送付先
請求先住所1
請求先住所2
送付先
プロパティ名 型名
郵便番号 文字列
住所1 文字列
住所2 文字列

一方、データベーステーブルの複数のカラムを値オブジェクトで表すことがあります。これを埋め込みバリュー(Embedded Value)と呼びます。右表は埋め込みバリューの例で、得意先マスタテーブルのカラムと対応する得意先マスタクラスのプロパティの対応を表しています(実際の設計とは異なります)。得意先マスタクラスはエンティティ、届け先・請求先はともに送付先クラスの値オブジェクトです。例えば郵便番号が入力されたら郵便番号辞書を参照して対応する住所を住所1・住所2に設定するロジックを組み込むことを考えます。もし、単純にテーブルのカラムと対応したプロパティを持つクラスを設計すると、上記ロジックを2カ所に実装する必要があります。届け先と請求先を値オブジェクトとして設計するとロジックの実装は送付先クラス内の1カ所で済みます。また、別のテーブルで同じパターンが表われた場合にも再利用できます。

複数のカラムに対応する値オブジェクトを扱うときに問題になる点として、ドメイン層以外ではこのような構造のまま扱いにくいことがあります。Viewデータアクセス層ともに単純にカラムに対応している方が自然に扱えます。

NetFramework上で、上記問題を解決するため、エンティティの子である値オブジェクトが持つプロパティを、あたかもエンティティが直接持っているかのようにアクセスできるPropertyDescripterを作成し、これらPropertyDescripterのコレクションを通じてView、データアクセス層からアクセスするようにしました。Windows Formsのバインディング機構ともスムーズに連携できます。

値オブジェクトは不変オブジェクトまたは可変オブジェクトのどちらでも実装できます。どちらがよいかその得失を比較します。

不変オブジェクトと可変オブジェクト
不変オブジェクト 可変オブジェクト
参照コピー 参照をコピーしてもコピー元のオブジェクトが書き換わる心配がない 参照をコピーした後、コピー先オブジェクトのプロパティを変更すると、元のオブジェクトも書き換わる
共有 同じプロパティ値を持つオブジェクトを共有できる 共有できない
変更 一部のプロパティを変更するだけでも、オブジェクトを再作成する必要がある 一部のプロパティを変更する際に、それ以外の操作は不要
NetFrameworkとの対応 値型 および不変オプジェクトとして設計された参照型(※値型単体では可変だが他のオブジェクトに所有された値型のプロパティは変更できない) 可変オブジェクトとして設計された参照型

不変オブジェクトは参照コピーおよび共有の点で有利ですが、埋め込みバリューに応用する場合は一部のプロパティを変更しただけで再生成する必要がある点が不利です。このように動作するPropertyDescripterの実装が面倒になります。

そこで当方では埋め込みバリューを可変として実装しています。その代わりに埋め込みバリューを上位オブジェクトにコンポジット集約させて、一旦参照をコンストラクタで設定した後は参照を書き換えないことで意図しない変更を防いでいます。