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の実装が面倒になります。

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

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のありがたさを実感しました。