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

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

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

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

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

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

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

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

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

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

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

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

Unity で INotifyPropertyChanged

データバインディングを利用したアプリケーション開発で、双方向バインディング(View→DataSourceおよびDataSource→Viewに変更を通知)を実現するためデータソースとなるオブジェクトにINotifyPropertyChanged を実装する方法がああります。そのための基本的な方法は各プロパティのセッターでPropertyChangedイベントを発生させるコードを記述することです。

1
2
3
4
5
6
7
8
9
10
11
12
13
int testProperty;

public int TestProperty
{
get { return testProperty; }
set
{
if (value != testProperty) {
testProperty = value;
OnPropertyChanged(new PropertyChangedEventArgs("TestProperty"));
}
}
}

この方法の問題点は

  • 記述量が多く、時間の掛かる単純作業となること
  • ドメインコード(アプリケーション固有コード)とインフラストラクチャコードが混在すること

です。単純作業はスニペット・T4テンプレート・コードジェネレーションなどの方法で回避できます。

ドメインの記述の自由度を保つためにはなるべくインフラストラクチャの制限を受けたくありません。POCOエンティティが見直されているのはそのためだと理解しています。ドメインコードとINotifyPropertyChangedを実現するコードを分離するため、MicrosoftのUnity依存性注入コンテナを使うことが考えられます(下記コード参照)。

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
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.InterceptionExtension;

namespace UnityAdditionalInterfaceTest
{
class NotifyPropertyChangedBehavior : IInterceptionBehavior
{
static readonly MethodInfo addPropertyChangedMethodInfo =
typeof(INotifyPropertyChanged).GetMethod("add_PropertyChanged");
static readonly MethodInfo removePropertyChangedMethodInfo =
typeof(INotifyPropertyChanged).GetMethod("remove_PropertyChanged");

public object Target { get; set; }

event PropertyChangedEventHandler PropertyChanged;

void RaisePropertyChanged(string name)
{
if (PropertyChanged != null) PropertyChanged(Target, new PropertyChangedEventArgs(name));
}

public IEnumerable<Type> GetRequiredInterfaces()
{
return Type.EmptyTypes;
}

public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
if (input.MethodBase == addPropertyChangedMethodInfo) {
PropertyChanged += (PropertyChangedEventHandler)input.Arguments[0];
return input.CreateMethodReturn(null);
} else if (input.MethodBase == removePropertyChangedMethodInfo) {
PropertyChanged -= (PropertyChangedEventHandler)input.Arguments[0];
return input.CreateMethodReturn(null);
} else if (input.MethodBase.Name.StartsWith("set_")) { //プロパティ設定
RaisePropertyChanged(input.MethodBase.Name.Substring(4));
}
return getNext()(input, getNext);
}

public bool WillExecute
{
get { return true; }
}
}

public class Person
{
public virtual string Name { get; set; }
public virtual int Age { get; set; }
}

public static class Program
{
static void AcceptNotifyPropertyChanged(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine("NotifyPropertyChangedOccured:" + e.PropertyName);
}

public static void Main()
{
var behavior = new NotifyPropertyChangedBehavior();
var target = Intercept.NewInstanceWithAdditionalInterfaces<Person>
(new VirtualMethodInterceptor(),
new IInterceptionBehavior[] { behavior },
new[] { typeof(INotifyPropertyChanged) }, new object[] { });
behavior.Target = target;
(target as INotifyPropertyChanged).PropertyChanged += AcceptNotifyPropertyChanged;
target.Name = "test";
target.Age = 1;
Console.ReadKey();
}
}
}

PersonはDataSourceに指定するエンティティ、NotifyPropertyChangedBehaviorクラスはINotifyPropertyChangedを実現するクラスです。Personは自動実装プロパティを使用していて単純な記述になっています。またセッターに割り込むためにvirtualを付けています。Intercept.NewInstanceWithAdditionalInterfaces<Person>を使ってインスタンスを作成すると、元のクラスがインタフェースを実装していないにもかかわらず、実装しているかのように扱うことができます。特定の基本クラスを必要としませんので、インフラストラクチャコードを気にしないで継承を使用できます。また表示用には直接Personのコンストラクタを呼び出し、編集用にはUnityを使ってインスタンスを作成するといった使い分けをすることもできます。

上記コードではエンティティ内でプロパティ変更時アクションを記述する場所がありませんので、外部から使用するのと同様にイベントハンドラを追加するか、特定の名前のメソッドを決めておきプロパティ変更時に呼び出すなどの工夫が必要になります。

ドメインロジック

ドメインロジックとは

エンタープライズアプリケーションアーキテクチャ(マーチンファウラー著)を読むとドメインロジックという言葉が出てきます。ドメインロジックとはアプリ-ケーションが扱う問題領域の論理であると私なりに理解しています。例えば販売管理で請求額の計算など規模の大きい物や、数量と単価からの金額計算などの小さな物までがこれに含みます。画面遷移・変数の画面表示・データベースの更新等はこれに含みません。ドメインロジックは当方のWindows Formsのアーキテクチャではドメイン層に記述します。

ドメイン層のデータ構造

データベースは表形式でデータを持っています。それに対しドメイン層ではオブジェクトグラフ形式でデータを持っています。テーブルはエンティティ(参照オブジェクト)で表します。エンティティ間の関連は参照で表します。

例えばデータベース上に伝票・得意先マスタ・伝票明細テーブルがあったとします。ドメイン層では伝票エンティティに得意先マスタを参照するプロパティと伝票明細のリストを保持するプロパティを持たせています。

エンティティは、固定繰り返しを持つ場合など、必要に応じて値オブジェクトによって複合的な構成にする場合があります。

ドメインロジックパターンの分類

ドメインロジックの書き方について、トランザクションスクリプトドメインモデルテーブルモジュールがあります。これらの違いは以下の通りです。

ドメインロジックパターンの分類
分類 形態 得失(当方の考え)
トランザクションスクリプト ビジネスロジックを一連の手続きで構成する。処理は手続きに集中している。1つのエンティティに関連する処理は各トランザクションスクリプトに分散している。
  • シンプルに記述できる
  • ビジネスロジック相互の干渉は発生しにくい
  • エンティティ毎、テーブル毎に処理を切り分けないので重複が発生しやすい
ドメインモデル データベースの行に対応するエンティティに処理を記述。ビジネスロジックはエンティティを渡り歩くように実行する。1つのビジネスロジックに対して記述は分散している。
  • エンティティに関連する処理はエンティティに集中するので重複が発生しにくい
  • 継承による処理の振り分けに対応しやすい
  • ビジネスロジック相互の干渉に注意が必要
テーブルモジュール データベーステーブルに対応するクラスに処理を記述。ビジネスロジックはテーブルモジュールを渡り歩くように実行する。1つのビジネスロジックに対して記述は分散している。
  • テーブルに関連する処理はテーブルモジュールに集中するので重複が発生しにくい
  • 継承によってエンティティ毎に処理を振り分けることはできない
  • ビジネスロジック相互の干渉に注意が必要
  • 行内の処理は記述しにくい
  • 行のコレクションの処理は記述しやすい
ドメインロジックパターンの比較
  トランザクションスクリプト ドメインモデル テーブルモジュール アクティブレコード(参考)
処理とデータ 分離 一体 一体 一体
インスタンス エンティティとは独立 行毎 テーブル毎 行毎
データアクセス 分離 分離 分離 一体

ドメインロジックパターンの適用

当方では従来主にトランザクションスクリプトで記述していました。今はドメインモデルとテーブルモジュールを併用しています。行内の処理およびエンティティが保持する子エンティティの集計処理はドメインモデルで、ルートエンティティの集計処理、例えば複数行の金額合計計算などはテーブルモジュールで記述しています。伝票と伝票明細を編集することを想定した場合には、伝票明細の金額を合計して伝票の合計金額項目を更新する処理は伝票に記述しています。

パターンを変更した理由はトランザクションスクリプトだと処理の重複が発生しやすいからです。ドメインモデルとテーブルモジュールを使った場合1つの処理だけを見れば処理が分散して複雑になるのですが、アプリケーション全体で見ると重複が少なくなりどこに何が書いてあるか見通しが良くなります。

ドメインロジックの構成

当方ではドメインロジックを下記のもので構成しています。

ドメインロジックの構成
  記述内容
ドメインモデル
行内の処理

例)数量×単価から金額を求める処理

子エンティティの集計処理

例) 伝票と伝票明細があり、伝票明細の金額集計を行う処理を伝票エンティティに記述

テーブルモジュール
ルートエンティティの集計処理

例)伝票一覧の金額合計処理

サービス
バッチ処理その他

例) 日次更新・請求処理

リポジトリ
クエリーに基づくエンティティのロード

抽出条件やキーを指定してデータソースからロード

集計を伴うクエリー、例)請求一覧作成

Unit of Work を通さない場合の更新

エンティティの選択

エンティティは次の3種類に分けることができます。

  • データベーステーブルに対応するエンティティ
  • テーブルと異なる結果セットを持つクエリーに対応するエンティティ
  • 抽出条件等ユーザー入力に必要なエンティティ

クエリーに対応するエンティティについて補足説明します。ドメイン層はオブジェクトグラフでデータを保持していますので必要なクエリーを削減することができます。例として伝票一覧に得意先マスタの項目である得意先名を表示することを想定します。件数の少ないマスタ類は参照されたときに全件ロードするようにしてあります。そのため得意先名は伝票.得意先マスタ.得意先名で参照できるのでテーブルを結合する必要はありません。伝票-伝票明細といったトランザクションテーブル同士の結合および集計を伴う場合にクエリーに対応するエンティティを作成します。

リポジトリの役割

リポジトリでは、クエリーによるエンティティのロードと更新処理を行います。

キーを指定して該当するエンティティをリストにロードする場合、呼び出し元コードはリポジトリに対しExtractByKey(Key)を呼び出します。リポジトリはクエリーを発行し、その結果をIEnumerableで返します。呼び出し元コードこれを所定のリストに格納します。

抽出条件をユーザが指定できる一覧画面では、抽出条件エンティティのGetFilterExpressionを呼び出して、入力された抽出条件を元にExpression型の条件式を作成します。条件式はExpression Tree によるDSLで表現しています。この条件式をリポジトリのExtractメソッドの引数に指定して呼び出します。ExtractメソッドはIEnumerableの形式でエンティティを取得します。

リポジトリではSQLを利用しながらドメインロジックを実現する場合があります。例えば請求一覧表を作成する処理を大雑把に説明すると、まず各得意先について売上・入金の合計を計算して差額を出します。このときの合計計算はSQLを利用し、その結果はデータベース上のテンポラリテーブルに書き込みます。テンポラリテーブルをロードして請求一覧表とします。これら一連の処理は請求一覧リポジトリに記述しています。SQLを利用する利点はSQLによる簡潔な記述が活かせること、通信量が少ないこと、パフォーマンスがよいことです。欠点はデータベースエンジンによってSQLの記述が異なる場合があること、ドメインロジック内にSQLによる処理とオンメモリテーブル上の処理が混在してしまうことです。

参照

Expression Tree で DSL

データベースを利用したアプリケーション開発で、ユーザの入力した条件に従って抽出する場合など、SQLを動的に作成して発行しなければならない場合があります。そのようなときクエリーオブジェクトを使っていました。クエリーオブジェクトはSQLの文字列作成をラップしたオブジェクトで、例えば次の様な記述になります。

1
2
3
4
5
6
var query = new Query();
query.Select.Add(伝票);
query.Select.Add(new SqlColumn("Coalesce(", 顧客マスタ.住所1Column, ",) || Coalesce(", 顧客マスタ.住所2Column, ",");
query.From = new SqlFrom(伝票).InnerJoin(顧客マスタ).On(伝票.顧客コードColumn, 顧客マスタ.顧客コードColumn);
query.Where.Add(伝票.顧客コードColumn, MatchKind.Equal, ConditionRow.顧客コード);
query.Where.Add(伝票.仮伝票Column, MatchKind.NotEqual, true);

queryはクエリーオブジェクトでSelect,From,Whereなどの各句に適したビルダーをメンバーに持ちます。ConditionRowはユーザが入力した抽出条件を表すオブジェクトです。この方法での問題点は記述量が多いことと、型付きデータセットのメリットを活かせないこと、データベース関数への対応が難しく、その部分だけ文字列の組み立てが必要になるなどです。

解決策をなかなか見つけられずにいましたがExpression TreeによるDSL処理系を開発すれば解決できることがわかりました。

例えば上記のクエリーオブジェクトと同等の記述は次のようになります。

1
2
3
var query = new Query(() => SqlExp.Select(伝票.AllColumns, ((顧客マスタ.住所1 ?? "") + (顧客マスタ.住所2 ?? "")).As(住所))
.From(伝票).InnerJoin(顧客マスタ).On(伝票.顧客コード == 顧客マスタ.顧客コード)
.Where(伝票.顧客コード == ConditionRow.顧客コード && !伝票.仮伝票));

Expression Treeを応用したフレームワークLinq To SQLは標準クエリー演算子と仕様が合わせられています。それに対して当方が取り組んだDSL(SqlExpと命名)はSQL文との互換性を重視しています。ラムダ式を組み合わせてクエリーを表現するLinq To SQL とは違い、1つのラムダ式でクエリーを表現します。また、SELECT文だけでなくINSERT, UPDATE, DELETE文も表現できます。

SqlExp処理系は、与えられたラムダを解釈しExpression TreeをたどってSQLを作成します。

Expression Treeではメソッド名・プロパティ名を取得することもできますし、実行時にコンパイルして値を取得することもできます。伝票顧客マスタ住所1等のテーブル名・カラム名はそのまま文字列に変換し、ユーザが入力したConditionRow.顧客コード等は値を取得してから文字列に変換します。テーブル名・カラム名はスキーマに基づいてコード生成しています。DSL中に表われるSelect,As,From,InnerJoin等のメソッドは実装する必要がなく単なるダミーです。SqlExp処理系でそれらをSQL文字列に変換できればよいのです。また、データベース関数を処理できるように作っています。新たなデータベース関数に対応したい場合には名前と引数リストを合わせたダミーのメソッドを作れば良いだけですので機能を簡単に拡張できます。動的な追加はクエリーオブジェクトと同様に各句のビルダーを使います。ビルダーは文字列ではなくExpression Treeを保持するようにしています。ビルダーをSqlExp上の挿入したい位置に記述することでビルダーの持っている部分式を展開するようにしています。

1
2
3
4
var columns = new SqlColumns();
columns.Add(() => 伝票.伝票番号);
columns.Add(() => 伝票.仮伝票);
var query = new Query(() => SqlExp.Select(columns).From(伝票));

工夫した点としては、比較演算の一方がnullであれば演算自体を消去していることです。抽出条件はユーザが入力しなければ条件に含めないことが多いのですが、そのような場合にも条件分岐を記述する必要がありません。

SqlExp処理系を開発することで下記メリットを得られました。

  • 記述が簡潔になった
  • タイプミスはコンパイル時に気づく
  • データベース関数対応等容易に拡張できるようになった

今のところ開発プロジェクトで使用するDMLをすべて表現できています。

SqlExp処理系開発に際して、Expression Treeについて書いておられるあちらこちらのサイトを参考にさせていただきました。

パッケージとの連係とトランザクション(弥生API)

DBMSのトランザクションを使うと中途半端な状態でデータが登録されることを防げます。例えば会計システムで借方・貸方の順にデータを登録し、貸方登録時にエラーが発生したら借方の登録がキャンセルされます。

1つのデータベース・1つのシステムで完結するときはトランザクションが有効に機能します。既存パッケージと連係するようなシステムでパッケージ側のAPIがトランザクションを備えていないと対策が必要になります。

弥生販売管理ではAPIが用意されていて、例えば外部システムで作業指示書発行し、売上計上したら弥生にも同時に売上登録することができます。弥生APIにはトランザクションを直接使用する機能はないので、処理順序を工夫します。

  • 外部システムでトランザクション開始
  • 外部システムを更新
  • 弥生APIで弥生側を更新
  • 外部システムでトランザクションコミット

このような順序にすると、3での弥生側の更新段階でエラーが発生すれば2での外部システムの更新もキャンセルされますので矛盾したデータが登録されることはありません。

但し3の段階でAPIを複数回呼び出して更新しなければいけないような複雑なシナリオでは、トランザクションによって矛盾したデータの登録を防ぐことはできません。弥生APIでトランザクションをサポートしてくれるとこのような場合にも対応できるようになります。

PHPのはまりどころ

私はPHPを開発に使用するまでにはVB, C#, rubyなどを使ってきました。

PHPを使って勘違いなどで思うような動作にならずに手こずった点や、勘違いしそうだなと思う点を挙げてみます。

switchによる比較がifによる比較と異なる?

PHPでは等しいを表す比較演算子に 緩やかな比較==と厳密な比較===があります。==は、型変換をした上で比較します。===は型が異なると不一致となります。switchでは==に相当する比較をしますので、swicth($a) case 1: と書けば、$aが文字列の'1'でも一致します。

セッションにオブジェクトを保存するのにシリアライズする必要はない

$_SESSION['test'] = new Test();とそのまま代入すればよいです。セッションファイルの中身を見ればわかりますが、serialize()でシリアライズしたのと同じ形式で保存されます。これははまりどころというよりは他のサイトでserialize関数を呼び出すと書いてあるのを見かけましたので。

旧バージョンでセッションに日付型が保存できない

PHPのバージョンによってはDateTimeをセッションに保存できません。組み込み型は当方手持ちですと5.3.8は保存でき、5.2.14は保存できませんでした。serialize関数のマニュアルにはPHP の組み込みオブジェクトの多くはシリアル化できないことに注意しましょう。 と書いてあります。

データベースのNumberに相当する型がない

Floatで代用すると小数部の計算で誤差が出ます。bcで始まる関数群を使えば誤差無しの演算ができるのですが、標準ではインストールされないのでレンタルサーバではこれらが使えない場合があります。

リファレンスを作成すると元の変数までリファレンスになる

1
2
3
4
5
6
7
$a = array();
$a['x'] = 'x';
$ref = &$a['x']; //この行の有無で結果が変わる
$b = $a;
$b['x'] = 'y';
var_dump($a);
var_dump($b);

リファレンスは変数にエイリアスを設定できる物で使い方によっては大変便利な物です。しかしときには想像しがたい動作をします。配列$aのキー'x''x'をセットし、配列$a$bに代入後、同じキーに'y'を代入する物です。3行目におまじないがあり$a['x']への参照を設定しています。これがなければ$a['x']='x',$b['x']='y'となります。コピーオンライトが効いていると言うことです。しかし一見無関係に見える3行目を挿入すると$a['x']='y',$b['x']='y'となります。要素がリファレンスを持ったまま配列をコピーすると該当要素のコピー元とコピー先でエイリアスになってしまうようです。オブジェクトのフィールドにも同じことが言えます。

IDEとアーキテクチャー

PHPでの開発では、以前は秀丸を使っていましたが今はNetBeansを使っています。同じコードを作るのにNetBeansなどのIDEを使用することによって生産性が向上します。ここではさらに踏み込んでIDEの機能を利用することを前提にアーキテクチャを変えた例を紹介します。

以前は、PHPが動的型付け言語なのでクラスメンバーに応じたメンバー名の補完はできない物と思っていました。ですからReflectionパターン考察で触れたように、データベーステーブルの行などのエンティティはクラスで表現するのではなく、項目名をキーに持つ連想配列として扱っていました。

NetBeansではメンバー名をほぼ正確にリストアップし、コード補完してくれます。

1
2
3
4
5
6
7
8
9
10
11
/**
* 関数の説明
* @param type $param
*/
function functionName($param) {
$param-> <i>コード補完有効</i>
$varname1 = new TestClass();
$varname1-> <i>コード補完有効</i>
/* @var $varname2 type */
$varname2-> <i>コード補完有効</i>
}

関数にPHPDoc形式でコメントを書いた場合には引数の型を認識してくれます。エディタで/**<Enter>と入力するとドキュメントコメントの雛形を作ってくれるので、コメント作成の手間を減らしてくれます。右の例で@param type paramtypeの部分を適切な型名に置き換えれば良いのです。

また、コンストラクタ呼び出しの結果を代入した変数も型を認識してくれます。

それ以外の部分で型を認識させたい場合は、/* @var $varname type */の形式でコメントを書きます。

IDEのコード補完を使用することを前提にエンティティをクラスで表現すれば開発効率をもっと上げることができると期待できます。

実際にやってみると次のような効果が出ました。

  • 単純な綴りの間違いや、勘違いによって存在しない項目にアクセスするミスを大幅に減らせた
  • メンバーにコメントを書いておけばコントロールキーを押しながらクリックすることでコメントが表示されるので仕様確認が簡単になった
  • メンバー記述後に定義部分にジャンプできるので定義部の修正が楽になった
  • コメントを書くことによるメリットが大きいのでこまめにコメントを書くようになった

注意点としては、データベースから読み取りや$_POSTなどのリクエストパラメータは連想配列形式になっているので、クラス形式への変換が必要となることです。このような変換は単純作業なのでツールを作成してコードジェネレーションしています。このとき各項目はそのデータ型に合わせて文字列・数値・日付等に型変換を行うことで型の不一致による不具合を避けています。

アーキテクチャを変更することで改めてIDEのありがたさを実感しました。

バージョン管理システム

複数の開発者が作成したコードをマージしたり、仕様が一部異なる複数のプロジェクトに共通の変更を加えたりするときに、手間なく正確に作業するにはバージョン管理システムが役に立ちます。数年前からバージョン管理システムのSubversion(TortoiseSVN)を使ってきましたが、今回Mercirual(TortoiseHg)を試しました。

リポジトリ・コミット・マージ

変更履歴を記録してあるデータストアをリポジトリと呼びます。コミットでその時点のスナップショットをリポジトリに保存します。このときリビジョンが刻まれ、必要があれば簡単に任意のリビジョンに戻すことができます。

バージョン管理で重要な機能がマージです。マージは複数箇所での変更を1つのまとめる機能です。異なる開発者の変更をまとめるときや、あるプロジェクトに加えた変更を仕様が一部異なる別のプロジェクトに適用するときにマージが発生します。ファイルをプロジェクトに追加しても自動的にマージしてくれるのはもちろんのこと、同じファイルを変更した場合にも変更を加えた行が隣り合っていなければ自動でマージしてくれます。隣り合っているか同一行を変更した場合は競合が発生し、競合解消用のエディタによって手動でマージします。

集中型・分散型

Subversionはリポジトリを1カ所で持つ集中型と呼ばれる物で、それに対してMercirualはリポジトリを複数持つことができる分散型と呼ばれる物です。開発者それぞれがコーディングのスクラップアンドビルドするときに自分用のリポジトリを利用すれば他の開発者に影響を与えません。これが分散型のメリットです。Mercurialの他にGit、Bazaarなどがあります。

当方は開発規模が小さいので集中型のSubversionでも十分なのですが、Mercurialに備わっているRebase, Transplant, Splitが仕様が一部異なる複数のプロジェクトを管理するのに便利に使えることがわかりました。

ブランチ

一部仕様の異なる複数のプロジェクトはブランチという機能を使って管理します。メインで開発するブランチをSubversionではtrunkという名前を付ける習慣になっているようです。trunkでの変更を別のブランチにも適用するとき、Subversionではリビジョンの範囲をマージという機能を使います。更新→マージ→動作確認→コミットの順で操作します。どこまでのリビジョンをブランチに適用したかSubversionが把握してくれているので、いちいちマージするリビジョンを指定する必要はありません。ブランチで変更した結果をtrunkに反映する際にもリビジョンの範囲をマージを使います。但し、相互にマージする場合は、相手方のブランチでの変更だけをマージするためリビジョンの範囲を明示的に指定します。マージ結果をコミットしたリビジョンまでマージしてしまうと同じ更新を2度コミットすることになり、競合が発生します。

リビジョンの範囲をマージを使うと2つのブランチがそれぞれの更新を他方のブランチにマージする形でリビジョンが成長していきます。

Mercurial

MercurialのRebaseはこれとはことなりブランチの分岐点を変えることによって共通の変更を他のブランチに適用します。つまりdefault(Subversionのtrunkに相当)とブランチの差違を最後に適用したことにできるのです。逆にブランチに加えた変更をdefaultに適用するときはTransplantによってdefaultに適用したことにして、Splitによってブランチに適用した変更を取り消し、Rebaseします。

リビジョンの成長過程はリビジョングラフによって確認できますが、リビジョングラフがシンプルでわかりやすいものになり、私はこの方法が気に入りました。

Mercurialは日本語ファイル名の扱いに難があり、win32mbcsエクステンションを使用し、一部のDLLを入れ替えなければなりません。

私は下記URLを参考に設定しました。

http://www.asukaze.net/etc/vcs/hg-fixutf8.html

しかし、Transplant実行時に日本語ファイル名が原因でエラーが出てしまいました。当初想定した使い方ができないので今のところSubVersionを継続使用するつもりです。

mercurial 2.0 では上記の不具合が解消されているようです。win32mbcsエクステンションは必要ですがDLLの入れ替えは不要になっているようです。時間を掛けて検証していないので見落としがある可能性があります。次回新たなプロジェクトが立ち上がったら使ってみます。

Git

久しぶりに見返しましたが、この記事を書いたときから状況が変わりました。Visual Studio にも最初から Git がついています。現在ではもっぱら Git を使用しています。

Windows Forms でのアーキテクチャー

当方がデータベースアプリケーションをWindows Formsで作成するときのアーキテクチャーを紹介します。

レイヤーアーキテクチャー

全体としてはModelとViewを分離するDocument-Viewを使用しています。Viewの役割を表示とキーボード・マウスなどのUIイベント処理に限定すると見通しが良くなります。それに伴い画面遷移などをModel側で処理するようになるとModelの役割が多くなり見通しが悪くなってきましたので、Model内の構造を見直す必要がでてきました。

そこでレイヤーアーキテクチャーを採用してModel内を分割することにしました。アプリケーション層・ドメイン層・データアクセス層に分割しています。アプリケーション層とドメイン層の間にサービス層を追加していたときもありましたが、ユーザの確認を差し挟む処理が書きにくいのでサービス層を無くしました。

コンポーネント 要約 コマンド実行 データバインディング その他 Reflectionでの分類
View データ表示、UIイベントの処理
  • UIイベントを受けてコマンド呼び出し。
  • ほとんどのコマンドはModelのメソッドを呼び出す。
アプリケーション層が提供するデータソースを入力・表示部品にバインディング。
  • Modelからの通知を受けて、画面遷移先のViewを生成
  • ボタン使用可・不可の連動、ダイアログの表示
  • その他UIデバイスの更新
メタレベル
Model アプリケーション層 (プレゼンテーションモデル) Viewの状態管理、コマンドの実行、ドメインの集約
  • Viewから呼び出されるコマンドに対応する処理を実行。
  • データ操作を伴わない画面遷移等はアプリケーション層で完結。
  • データ操作を伴うコマンドの具体的な処理はドメイン層に委譲。
  • 削除確認等処理に伴うダイアログ表示はアプリケーション層からViewへの通知。
  • 編集画面ではエンティティの変更追跡(Unit of Work)、トランザクションの管理を行う
データバインディングに必要なエンティティを集約し、データソース取得メソッドを通じて公開。 
  • 新しいModelのインスタンスを作成することで間接的に画面遷移。
  • Viewで使用するボタンの使用可・不可もここで管理
  • Viewへの各種通知
  • 画面毎にインスタンス生成
-
ドメイン層 (ビジネスロジック層) エンティティおよびビジネスロジックの提供
  • ドメインモデルおよびテーブルモジュールでロジックを記述。ルートエンティティのリストを集計する処理はテーブルモジュール
  • リポジトリにクエリーを記述。HQLのような抽象化はしていない。
  • 年次更新・インポート・エクスポートなどのバッチ処理、SQL使用
  • エンティティおよび所有する値オブジェクトがデータバインディングの対象となる。
  • アイデンティティ・マッピングを採用
  • エンティティ・値オブジェクトはコード生成。
  • マスタ類は全件読み込みしてリポジトリにキャッシュ。伝票類は画面毎にその都度インスタンス生成。
ベースレベル
データアクセス層 (パーシステンス層) データベースとの入出力
  • テーブルデータゲートウェイ形式でデータアクセス。
  • アクセス先の決定
  • DAOは不採用
  • 大方の機能はライブラリ化されているので記述量は少ない
メタレベル

レイヤー間の連係

各レイヤー間の連係は下図のようにしています。

データバインディングではViewからドメイン層のエンティティに直接バインディングしています。

コマンド実行では各層で必要があれば下層を呼び出しています。

各層は、Reflectionパターンのベースレベルまたはメタレベルで記述します。ドメイン層ではベースレベルで記述、すなわちエンティティのプロパティをそのまま使用します。Viewやデータアクセス層はメタレベルで記述、Viewでは入力部品やDataGridViewColumnがプロパティを表します。データアクセス層ではカラムオブジェクトがプロパティを表します。

データのロードはリポジトリを通じて行います。データベースの更新はUnit of Work によって行います。XMLやテキストファイルへの保存はリポジトリを通じて行います。

リポジトリにはクエリーを記述しメソッドによって呼び出せるようにします。伝票-伝票明細のような構造を持つ場合、関係表構造(IDataReader)からオブジェクトグラフへの変換(R/O変換)は汎用ライブラリを利用してリポジトリ内で行っています。

画面の継承関係

画面はメニュー・一覧・編集といった風にある程度パターン化しています。それぞれパターンには共通の処理がありますのでこれらをベースクラスに記述し、具象画面は派生クラスに記述します。Viewではもっぱらデザインでの継承階層、アプリケーション層ではメニュー・一覧・編集といった機能による継承階層を持たせています。

これによって各具象画面のコードは少なくなります。単純なCRUDだけですとView側に自分で記述するコードはありません。アプリケーション層もほとんど記述はありません。

画面遷移

画面遷移はModel側のアプリケーション層で管理します。ViewはModelに追従して画面遷移を行います。

下図でCallerModelは遷移元Model、CalllerFormは遷移元View、BaseModelはModelの共通の基本クラス(ここではstaticメソッドを表現)、CalleeModelは遷移先Model、CalleeFormは遷移先Viewです。

遷移元Modelが遷移先Modelの型とオプションを指定してstaticメソッドOpenModelを呼び、それに従ってCalleeModelを作成・初期化します。既に作成済みであれば再利用します。作成済みModelはグローバルなDictionaryによって管理しています。コンストラクタと初期化メソッドを分けているのはメソッドのオーバーライドによって初期化処理を柔軟に実装したいからです。

初期化が終わると、遷移先ModelのCallerプロパティに遷移元Modelを設定し、遷移先Modelでは遷移元ModelのAddCalleeを呼び出します。これを受けて遷移元Modelでは対応する遷移元Viewに対して、CallerAddedによって遷移先Modelが追加されたことを通知します。遷移元Viewは追加された遷移先Modelに対応する遷移先Viewを生成します。作成するViewは一定の規則でModel名からView名を導き出してリフレクションによって作成します。以上で遷移先ModelとViewの作成が完了し、次に遷移先ModelのRunメソッドを実行してModelとしての最初の動作を実行します。通常は対応するViewに対してShowを通知し、表示を促します。

このシーケンスですと画面間のデータの受け渡しは、Model同士で直接行いますのでViewを通じて受け渡すよりもシンプルになります。

参照

対話型システムのアーキテクチャ(MVCの仲間達)

対話型システムのアーキテクチャとして代表的なものにMVCがあります。そしてMVC以外にもさまざまなアーキテクチャがあります。それらのアーキテクチャ間で用語が違っているなど理解しにくかったので、自分が理解している範囲でまとめてみました。仲間達と書きましたが全く異なる視点で設計されているものもあります。

MVC(Model-View-Control)
アプリケーションをControl(入力)-Model(処理)-View(出力)に分割する。Modelをデータと捉えるとControlにビジネスロジックを書くことになりうまく分割できない。命名が誤解しやすい。データバインディング・Observerパターンを使うとModelからViewの更新はControlの補助無しに行われる。Webのサーバ側アプリケーションやWindows Formsでもデータバインディング・Observerパターンを使えない処理ではControlがViewを呼び出す。
Document-View
MVCのMがDocument、VCがViewに相当する。Windows Forms 等ではキーボード・マウス入力の大半を自動で処理してくれるので、独立したControlが必要になることはあまりない。そのようなUIではMVCのVCが一体化したDocument-Viewが向いていると思われる。
PAC(Presentation-Abstraction-Control)
アプリ-ケーションを複数のエージェントに分けて管理する。1つのエージェントはPresentation(入出力)-Control(処理)-Abstraction(データ)からなる。エージェント同士の独立性を保つことを重視していると思われる。エージェント間はControlを通じて細いインターフェースでやりとりする。プラグインを実装する、異なるプロセスで協調動作する、異なるマシンで協調動作するなどの用途に向いていると思われる。
BCE(Boundary-Control-Entity)
アプリケーション開発のロバストネス分析過程で用いられる。Boundary(入出力)-Control(処理)-Entity(データ)に分割する。処理をどのEntityに持たせるべきかを検討する場合にも用いられるので、できあがったソフトウェアの構造がBCEに分離しているわけではない。
MVVM(Model-View-ViewModel)
WPF、Silverlightで用いられている。View(入出力)-ViewModel(View用の処理)-Model(Viewに特化しない処理)。
アーキテクチャ対比表
MVC Document-View PAC BCE MVVM
1 Control View View Presentation Boundary View
2 Model Document Control Control ViewModel
3 Model
4 Abstraction Entity

Windows Formsの開発ではDocument-Viewを使っています。View上のボタン使用可/不可の状態はModelで管理し、ObserverパターンでViewを更新しています。画面遷移はModelが遷移先のModelを作成することで行っています。レイヤーアーキテクチャによってModelはさらに複数の層に分けています。

PHPによるWebアプリケーションではMVCを使っています。Controlはページ毎に持っていて、ページへの要求の解釈、Modelの呼び出し、ビューの呼び出しを行っています。Modelは各種更新処理、表示用のデータロードを行っています。Viewではテンプレートにデータを渡して、HTMLを組み立てます。

どちらにしてもModelにはViewへの参照は持たないもののViewを意識した処理を行っています。

ViewとModelを分離しなかった場合、同じロジックを複数の画面で使いたい場合に、ロジックが入力部品と結びついているため再利用が困難です。最小限ViewとModelを分離するとこのようなことが防げます。

これらのアーキテクチャを採用することで、アプリケーションに秩序が生まれ、どこに何が書いてあるかわかりやすく、生産性・保守性が上がる点がメリットであると考えています。

参照