Word Press

Word Press のプラグイン開発に取り組んでいます。

Word Press はアーキテクチャーとして MVC を採用していません。プラグイン部分がある程度のボリュームがある場合、MVC
フレームワークを使いたくなります。

そこでマイクロフレームワーク Slim と組み合わせて使うことにしました。

Word Press - Slim の組み合わでは、下記の点に考慮する必要がありました。

  • ショートコードによって、Slim に基づいたコードによって発生させたページを表示できるようにする。
  • カスタム固定ページの表示内容を Slim に基づいたコードによって発生できるようにする。
  • 上記2点によってページを表示する場合、Slim の Response を使用しないで、単に HTML テキストを出力、あるいは Word Press に返却する。
  • Ajax によるリクエストおよびダウンロードは、Slim で処理されるようにリライトおよびリダイレクトを設定し、Word Press で処理されないようにする。

このうち最後のリライトについてははまるポイントがありました。
プログラムのリライトは下記コードの 3 行目のように、index.php を経由してコントローラーとコマンドを引数として渡すことにしました。
index.php を経由する理由は Word Press で定義されている関数を使いたいからです。
一方、画像の URL もカスタマイズしたかったので 2 行目のように Word Press を経由しないで直接リライトを設定しました。

1
2
3
4
5
6
7
8
9
10
11
12
// リライトを設定します。
add_action('init', function (): void {
add_rewrite_rule('my_plugin/images/([^/]+)', 'wp-content/plugins/my_plugin/assets/images/$1', 'top'); //外部リライト
add_rewrite_rule('^my_plugin/([^/]+)/([^/]+)', 'index.php?my_plugin_controller=$matches[1]&my_plugin_command=$matches[2]', 'top'); //内部リライト
});

// Slim 処理系へのリダイレクトを設定します。
add_action('template_redirect', function (): void {
$controllerType = get_query_var('my_plugin_controller');
$commandName = get_query_var('my_plugin_command');
if ($controllerType and $commandName) { doSomething($controllerType, $commandName); } // コントローラーとコマンドの組み合わせを処理するコードをここに記入
});

Word Press では index.php を経由するリライトを内部リライト、そうでないものを外部リライト と呼んでいるようです。
そこで問題となるのが内部リライトと外部リライトで指定書式が異なることです。それぞれ正規表現でマッチした部分の参照方法は下記のようになります。

内部リライト $1, $2, ...
外部リライト $matches[1], $matches[2], ...

これについては気づきにくい部分だと思いました。

私は Word Press のソースコード(class-wp-rewrite.php)を読んで初めてこの違いを理解しました。

Oracle Apex

Oracle Apex を使ったシステム開発、保守に携わりました。

Oracle Apex は Oracle が提供する Rad ツール兼実行環境です。開発者視点で見ますと、PL/SQL を使ってロジックを記述し、画面は Oracle Apex が用意する画面デザイナーを使ってサーバー側およびクライアント側を作りこみします。ドメイン駆動開発とは全く異なる開発スタイルと考えてよいでしょう。

CRUD だけであまりドメインロジックがない画面では素早く開発できますが、ドメインロジックが増えていくと難しいと感じました。Oracle、SQL、PL/SQL、Javascript の知識に加えて Oracle Apex の知識も必要となり、急に難易度が上がるように思いました。

画面制御では一般的な Javascript の知識に加えて、Oracle Apex 用の API を把握する必要があります。

業務で使ったことを機会に研究し、下記のような工夫をしました。

Javascript

Proxy を使用して、Apex 項目に簡単にアクセスする方法を提供する。
Apex の項目にアクセスするためには複雑な記述が必要で、行が増えがちですが Proxy によって Apex の項目をプロパティに持つオブジェクトを定義しました。この機能をはじめ、どの画面でも使える共通ルーチンはShared ComponentsStatic / Application Filesにファイルとして格納しました。
ajax送信するときにのパラメーターに x01, x02 等を使わないで pageItems: ['P1_SEQ','P1_ITEM_CODE'] といった形式で指定する。
pageItems でページに使用している項目を送信できます。クライアント側、サーバー側双方でパラメーター・項目間で詰めなおす必要がなくなります。

PL/SQL

PL/SQL に深く取り組んだのは初めてでした。いろいろと勉強になりました。SQL および PL/SQL で工夫した点を挙げます。

自己結合および EXISTS による同じテーブルを取得するサブクエリーを使わないで、分析関数を使う。
パフォーマンス改善で効果があります。
FROM 句等にサブクエリーをインラインで記述するのではなく共通テーブル式を使う。
SQL 文が見やすくすっきりします。
コンテキストスイッチを減らす。
カーソルを使うと、Oracle エンジンと PL/SQL 間のコンテキストスイッチが頻繁に発生し、パフォーマンスを落とします。その対策として、可能であれば、BULK COLLECT を使ってコレクション型(ネストした表)に格納し、ネストした表を SQL で使ってロジックを実行するようにしました。

Oracle APEX で開発するなら下記サイトは必見です。

参照

静的サイトジェネレーター

当サイトはもともと HTML を手書きしていました。最近は記事が多くなってきたためトップページの目次だけでは探しにくい状況となっていました。タグなどで分類することで探しやすくしたいと思いました。また、HTML を書かなければいけないので記事の作成が少々面倒でした。

そこで、ブログを使って書き直すことにしました。

最初に思い付いたのが WordPress などの CMS でした。当サイトの規模と照らし合わせると仕組みが大げさであると考えて静的サイトジェネレーターにたどり着きました。

その中でも HugoHexo を候補としました。まず Hugo を試しました。タグに日本語を使うと Hugo は正しく URL を生成してくれませんでした。もし、タグから URL にマッピングする機能があれば適宜代替のアルファベットを使ったパスに変えることで回避できるのかもしれませんが、試していません。

次に Hexo を試しました。上記日本語タグ名の問題はありませんでした。

C#, C++ などの文字列に含まれる記号は URL に反映されないので、この 2 つは同じもの扱われます。Hexo にはタグ名から URL にマッピングする機能がありますので、C#CSharp, C++CPlusPlusとマッピングすることでこの問題を回避できました。

テーマは既定の Landscape を使いました。単純なつくりになっているのでカスタマイズしやすかったです。

テーマを直接上書きしています。git のサブモジュールとして管理しているので、もしアップデートされたテーマを適用したくなった時も何とかなるのではと考えています。

Visitor - Guide パターン

デザインパターンの中でも複雑な Visitor パターンについていくつかの改善を実践しましたので紹介します。TypeScript で実装しました。

Visitor パターンを使おうとした動機

ドメインモデルにおいて、エンティティがプロパティとして別のエンティティや別のエンティティを要素に持つリストを持つことは、よくある構造ではないかと思います。これらは再帰構造を持ちます。

再帰構造を持つエンティティを扱うユティリティとして次のような機能を作ろうとしました。

  • コピー
  • シリアライズ
  • デシリアライズ
  • 構造のパスに基づいてエラーをエンティティやプロパティに紐づけ
  • オブジェクト構造のどこかでエラーが発生していないか検査

これらの機能を作るとき、再帰構造を巡回するコードを書く必要がありますが、すべて類似の処理になりますので共通化したいと考えました。

過去に試した経緯

構造を巡回することから Visitor パターンが利用できるのではないかと考えて、過去に何度か試したことがあるのですが、今までうまくいっていませんでした。その理由を改めて整理すると下記の通りです。

オブジェクト構造のたどり方が微妙に異なる
コピー、シリアライズでは構造を末端までたどりますが、それ以外の処理では必ずしもすべてたどりません。 エラーがあるかないかだけの検査であれば、エラーが一つ見つかればその時点で残りの要素を巡回する必要がなくなりまが、Visitor パターンでは巡回を中断するのが困難です。
処理に付加的な情報が必要
例えば、コピーではコピー先のオブジェクト、親からとの関連(ルートなのか、親のプロパティなのか、リスト要素なのか)が必要になります。これらを共通化するのか難しかったです。
オブジェクト構造側に Visitor を受け入れる仕組みが必要
Visitor を受け入れるために、エンティティやリストに Accept メソッドが必要になります。エンティティのプロパティも巡回するとなると各言語で用意されている文字列、数値などのプリミティブな型が使えなくなります。

改善内容

そこで今回改めて次の条件を満たす仕組みを考えてみました。

  • 巡回方法を柔軟に選択できること
  • 途中で巡回を中断できること
  • 求める機能に応じて付加的な情報を柔軟に追加できること
  • オブジェクト構造側は修正しないで導入できること

以上を実現するため、次のように改善しました。

  • Visitor - Guide パターンを適用
  • Visitor に渡す付加的なパラメーターを追加
  • Visitメソッドの戻り値をGenerator(C#ならEnumerable)にする

Visitor - Guide パターン

VisitorパターンにはVisitor側で巡回経路を決める場合と、オブジェクト要素側で巡回経路を決める場合があるようです。

Visitor 側で巡回経路を決める場合、Visitor が実現する機能に従って巡回方法を変えられるメリットがあります。Visitor は巡回と具体的な処理の両方を受け持つことになります。
それに対して、オブジェクト要素側で巡回経路を決める場合、Visitor 側は巡回を受け持つ必要はなくなりますが、巡回経路が固定化されます。

そこで、これらを解決するため第3の要素として Guide を考えました。

Guide は巡回方法を決めて Visitor を案内します。Visitor は Guide の案内に従ってオブジェクト要素を訪問します。 VisitorとGuideは1対1で機能します。VisitorがGuideの guide メソッドを呼び、guide はオブジェクト構造の下位要素を巡回しVisitorのオブジェクト要素に対応する visit メソッドを呼びます。末端の文字列、数値などの要素に達した場合、Visitor は Guide をそれ以上呼び出しません。 つまり、VisitorがGuideを呼び出すのは再帰構造のルートまたは中間ノードを訪問するときだけです。

上記のような方法を Visitor - Guide パターンと名づけました。

このパターンによって責務が明確化しました。巡回方法は Guide が、実現する機能は Visitor が受け持ちます。異なる巡回方法が必要になった時は Guide を変えます。

但し、中間ノードで guide を呼ばない、visit が呼び出されても処理をしないと言った選択肢が Visitor 側にありますので、今のところ異なる Guide が必要になるケースは出てきていません。

中間ノードで guide を呼ぶ前に処理をすれば幅優先探索に、guide を呼んだあとに処理をすれば深さ優先探索になります。

また、オブジェクト構造側に手を入れる必要がありません。

Visitor に渡すパラメーターの追加

Visitor でもっとも単純なパターンではVisit メソッドにはオブジェクト要素のみを渡しますが、実現しようとする機能によっては、情報が不足します。そこで、下記の2つのパラメーターを設けました。

index
上位のオブジェクトからプロパティとしてアクセスされるならプロパティの情報、リストの要素であれば index
param
付加的な情報を表すパラメーター、コピーであればコピー先オブジェクト。木構造を dump 表示する Visitor であればインデントの深さなど

戻り値の Generator 化

Visitor の戻り値を Generator にした直接の理由は、巡回処理を途中で中断できるようにするためです。

コピーでは Visitor は処理した中間結果を処理が進むにしたがってコピーした要素を Generator に積み上げていきます。guide 呼び出しから戻った時点で積み上げた結果をコピー先オブジェクトに反映します。

一方、オブジェクト要素をたどってエラーがあるかどうか検査するケースでは、Visitor は訪問した要素が正常であれば Generator に要素を追加せず、エラーが見つかれば Generator に要素を追加します。呼び出しルーチンは巡回を開始して一つだけ要素を待ち、要素が返ればエラーありと判定し2個目の要素を待たないことで巡回を中断できます。すべて巡回が終わっても Generator が要素が返さなければエラーなしと判断できます。

欠点

オブジェクト構造側を修正しないで実現したため、Visitor パターンのメリットの一つであるダブルディスパッチによる型判定の除外はできません。

型判定をなくす理由は、型判定がプログラムのあちらこちらに散らばっていると、オブジェクト構造に新しい種類が加わった時、改修が困難となるためだと理解しています。

現状では Guide 内に型判定が残っています。もともとの動機は巡回を繰り返し記述しないことでした。それは実現できていますのでこの欠点はこれ以上対処しないことにします。

サンプル

下記は Visitor - Guide パターンによりオブジェクトを巡回する基本的なコードです。

IObjectNodesVisitor
Visitor が実装すべきインターフェースを表します。
IObjectNodesGuide
Guide が実装すべきインターフェースを表します。
ObjectNodesGuide
Guide の基本実装をカプセル化します。
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
export type ObjectNode = object | any[];
export type ObjectNodeIndex = string | number;

export interface IObjectNodesVisitor<TResult, TParam = undefined> {
visitObject(target: object, index?: ObjectNodeIndex, param?: TParam): Generator<TResult>;
visitArray(target: any[], index?: ObjectNodeIndex, param?: TParam): Generator<TResult>;
visitValue(target: any, index?: ObjectNodeIndex, param?: TParam): Generator<TResult>;
}

export interface IObjectNodesGuide<TResult, TParam = undefined> {
guideObject(target: object, param?: TParam): Generator<TResult>;
guideArray(target: any[], param?: TParam): Generator<TResult>;
}

export class ObjectNodesGuide<TResult, TParam = undefined> implements IObjectNodesGuide<TResult, TParam> {
private visitor: IObjectNodesVisitor<TResult, TParam>;

private* callVisit(index: string | number, value: any, param?: TParam): Generator<TResult> {
if (value instanceof Array) {
yield* this.visitor.visitArray(value, index, param);
} else if (typeof value !== 'number' && typeof value !== 'string' && !(value instanceof Date)) {
yield* this.visitor.visitObject(value, index, param);
} else {
yield* this.visitor.visitValue(value, index, param);
}
}

public* guideObject(target: object, param?: TParam): Generator<TResult> {
for (const [name, value] of Object.entries(target)) {
yield* this.callVisit(name, value, param);
}
}

public* guideArray(target: any[], param: TParam): Generator<TResult> {
for (const [index, item] of target.entries()) {
yield* this.callVisit(index, item, param);
}
}

constructor(visitor: IObjectNodesVisitor<TResult, TParam>) {
this.visitor = visitor;
}
}

下記は オブジェクトをダンプする機能を持つ Visitor を表します。

ObjectDumpVisitorParamはインデントの深さを格納するパラメーターです。各 Visitor はジェネレーターを返しています。これらの戻り値をつなぎ合わせるとダンプ結果を表す文字列を取得できます。

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
import {IObjectNodesVisitor, ObjectNodeIndex, ObjectNodesGuide} from "./ObjectNodesVisitor";

class ObjectDumpVisitorParam {
public depth: number;
public getNestedParam(): ObjectDumpVisitorParam {
return new ObjectDumpVisitorParam(this.depth + 2);
}
constructor(depth: number = 0) {
this.depth = depth;
}
}

class ObjectDumpVisitor implements IObjectNodesVisitor<string, ObjectDumpVisitorParam> {
public guide = new ObjectNodesGuide<any, ObjectDumpVisitorParam>(this);

private buildRow(param: ObjectDumpVisitorParam, index: ObjectNodeIndex | undefined, body: string): string {
const indent = ' '.repeat(param!.depth);
const path = index !== undefined ? `${index}: ` : '';
return `${indent}${path}${body}` + '\n';
}

public* visitObject(target: object, index?: ObjectNodeIndex, param?: ObjectDumpVisitorParam): Generator<string> {
if (!param) param = new ObjectDumpVisitorParam();
yield this.buildRow(param, index, '{')
yield* this.guide.guideObject(target, param.getNestedParam());
yield this.buildRow(param, undefined, '}');
}

public* visitArray(target: any[], index?: ObjectNodeIndex, param?: ObjectDumpVisitorParam): Generator<string> {
if (!param) param = new ObjectDumpVisitorParam();
yield this.buildRow(param, index, '[')
yield* this.guide.guideObject(target, param!.getNestedParam());
yield this.buildRow(param, undefined, ']');
}

public* visitValue(target: any, index?: ObjectNodeIndex, param?: ObjectDumpVisitorParam): Generator<string> {
if (!param) param = new ObjectDumpVisitorParam();
yield this.buildRow(param, index, target);
}

public static dumpObject(target: Record<string, any>): string {
return Array.from(new ObjectDumpVisitor().visitObject(target)).join('');
}

public static dumpArray(target: any[]): string {
return Array.from(new ObjectDumpVisitor().visitArray(target)).join('');
}
}

const target = {
id: 1,
name: 'test',
children: [
{
detailId: 1,
detailName: 'detail1',
},
{
detailId: 2,
detailName: 'detail2',
},
]
}

const result = ObjectDumpVisitor.dumpObject(target);
console.log(result);
参照

TypeScript でパラレル継承階層

以前に、パラレル継承階層を実装するとダウンキャストが避けられないとの記事を書きましたが、TypeScript を使うとある程度対応できることがわかりました。

ダウンキャストをなくすためには下記の2つの機能が言語に必要となります。

  • 継承可能な型引数をサポートすること
  • ジェネリックワイルドカードをサポートすること

このうち、継承可能な型引数については TypeScript で型引数の既定値を指定できるので、イメージしていたものとは異なりますが、これを利用すれば部分的に対応できることがわかりました。

中身と入れ物の関係がパラレル継承階層になりがちなので、その例を下記に示しました。

中身は BaseContentDerivedContentDerivedContent2 と継承します。一方、入れ物はこれに対応して、BaseBoxDerivedBoxDerivedBox2 と継承します。

BaseBoxは中身を参照するcontentプロパティを持ち、これをTContent型としています。入れ物の継承に従ってこの型引数も制約の型をより継承した中身の型へと変えていきます。このようにすることで、contentへの代入は型安全が保たれます。また、サンプルプログラムでは省略していますが、各入れ物クラスの中でcontentのハンドリングの際にダウンキャストは発生しません。

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
class BaseContent {
}

class BaseBox<TContent extends BaseContent = BaseContent> {
public content: TContent | undefined;
}

class DerivedContent extends BaseContent {
public prop1: number = 0;
}

class DerivedBox<TContent extends DerivedContent = DerivedContent> extends BaseBox<TContent> {
}

const derivedBox = new DerivedBox();
derivedBox.content = new DerivedContent();
derivedBox.content = new BaseContent(); //TS2741: Property prop1 is missing in type BaseContent but required in type DerivedContent

class DerivedContent2 extends DerivedContent {
public prop2: number = 0;
}

class DerivedBox2<TContent extends DerivedContent2 = DerivedContent2> extends DerivedBox<TContent> {

}

const derivedBox2 = new DerivedBox2();
derivedBox2.content = new DerivedContent2();
derivedBox2.content = new DerivedContent();//TS2741: Property prop2 is missing in type DerivedContent but required in type DerivedContent2
derivedBox2.content = new BaseContent();//TS2739: Type BaseContent is missing the following properties from type DerivedContent2: prop2, prop1</code></pre>

次に対応できない部分について説明します。

1
2
3
4
5
6
7
const baseBox: BaseBox = derivedBox2; //本来なら型引数が異なるので代入互換性はないはずだが代入できる。
baseBox.content = new BaseContent(); //アップキャストした参照に代入すると不正な代入が可能になる。

//型安全のためには、ジェネリックワイルドカードでないと代入できないようにする必要がある
//const baseBox: BaseBox<? extends BaseContent> = derivedBox2;
//ジェネリックワイルドカードを使った場合、下記の代入はエラーになる。
//baseBox.content = new BaseContent();

上記のコードで、DerivedContent2からBaseBoxへ継承関係にあるので、代入できると考えがちです。実際、TypeScript はこれを許可しています。しかし、継承に従って型引数が変わっているので、本来は代入互換性がないはずなのです。サンプルではダウンキャストを全く使っていないにもかかわらず、型安全でない状況が生まれます。

これを型安全にするためには前の記事でもふれたようにジェネリックワイルドカードの機能が必要です。コメント内にもしワイルドカードが使えたらどのようなコードになるかを書いています。

問題はもう一つ残っていて、入れ物の継承で中身の型である TContent が継承するたびに別物になっていることです。これも同じ型引数のまま継承させる必要があります。

言語を開発するほどの技術力と財力がありませんので、このような言語が開発されたらいいなと、願っているだけです。

参照

PHPトレイトでオーバーロード

PHP では、継承元クラスの関数を派生先でオーバーロードできません。

例えばの下記 1. の記述はエラーになります。

オーバーロードを実現するためにトレイトを使用する方法が考えられます。

2. ではトレイトを use するクラスにトレイトが持っている関数と同じ名前の関数を定義すると、元の関数が上書きされることを利用しています。但しこの記述では元の関数を呼び出せません。元の関数を呼び出すには、3. のように元の関数を呼び出す関数でラップして、呼び出す関数を上書きします。3. のように元の関数を呼び出す関数でラップして、呼び出す関数を上書きします。

1. オーバーロード不可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BaseAccess
{
public function save($data){
// $data の処理
}
}

class ConcreteAccess extends BaseAccess
{
//再定義できない
public function save($data, $detail) {
// $data の処理
// $detail の処理
}
}
2. トレイトで関数を上書き
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
trait BaseAccess
{
public function save($data){
// $data の処理
}
}

class ConcreteAccess
{
use BaseAccess;

//エラーにならない
public function save($data, $detail) {
// $data の処理
// $detail の処理
}
}
3. 元の関数を呼び出す
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
trait BaseAccess
{
protected function saveCore($data) {
// $data の処理
}

public function save($data){
$this->saveCore($data);
}
}

class ConcreteAccess
{
use BaseAccess;

public function save($data, $detail){
$this->saveCore($data);
// $detail の処理
}
}

Slim

Laravelを採用したシステムを開発した件は別記事で説明済みですが、今回はマイクロフレームワークSlimを使ったシステム開発について紹介します。

Laravel ではなく Slim を使用した理由は次の通りです。

  • ターゲットとなるサーバーの仕様が若干古く、Laravel の最新バージョンを使用できない
  • 他のサイトと同居させる環境で Laravel がインストールできるかどうか確信を持てない
  • 現行システムからの乗り換えとなり、データベース設計が Laravel 向きではなかった(複合主キーあり)
  • システム規模が小さく、Eloquent のような O/R マッパーがなくても、PDO で問題なく実装できる
  • 当方が必要とする機能がコンパクトにまとまっている

下記機能を使用しました。

  • ルーティング機能
  • DI 機能
  • コントローラーをサポートする機能、RequestResponse
  • 権限弁別の為 Middleware を補助的に使用

当初はフレームワークなしで開発しようと思っていたのですが、上記機能を一つ一つインストールするよりもコンパクトなフレームワークがよいと思い直し、Slim にたどり着いたのです。

使ってみて次のように思いました。

  • 機能が小さいので学習コストが小さい。
  • Eloquent では ActiveRecord によるを記述が強制されるが、Slim ではそもそも O/Rマッパーがないためそのような制約はなく、素直にドメイン層とデータアクセス層の分離して記述できた
  • 実行に必要なコードが 10MByte 程度、前回紹介した Laravel によるシステムでは 120MByte なのでサイズは 1/12、但しシステム規模が違うので単純に比較できない

Slim での開発を終えて、DI 機能に若干の不満点はありましたが、全体としては実装が面倒な部分を肩代わりしてくれて助かった印象を持ちました。

また、今年に入ってドメインロジックの多くをクライアントサイドに記述するスタイルに移行しているので、フレームワークを変更する負担はあまり大きくなかったです。

参照

クライアントサイドアーキテクチャー

Laravelを採用したシステム開発では同時にクライアントサイドのJavaScriptのアーキテクチャーも見直しました。

React, Vue.js

まずは、クライアントサイドのフレームワークではReactVue.jsなどが知られていますのでこれを研究しました。

React のチュートリアルを進めていくと、激しくリファクタリングを行う必要があり、開発の負担が重そうに感じました。

Vue.js はあまり複雑ではなく開発しやすそうに思いました。しかしながら、Model と View が分離しているようで分離していないので、シングルモデル - マルチビューが実現しにくいと思いました。

独自フレームワークの作成

そこでレイヤーアーキテクチャーによって Model と View を分離するフレームワークを新たに作成しました。View(アプリケーション層を含む)+ドメイン層+データアクセス層に分けています。React や Vue.js が実現している1文字入力するたびに反応する Reactive な機能は実現できていませんが、当方の開発範囲ではフィールド毎に反応する機能があればよいと考えています。

いくつか割り切った部分があります。

ECMAScript 6 を使用しています。現在では ECMAScript 6を前提としても問題ない状況になってきていると判断しました。また、jqueryの使用をやめました。

サーバーとのデータ交換は JSON で行うと限定し、FORMによる GET, POST をサポートする機能を実装しませんでした。レンダリング対象となるデータは JSON でクライアントに送信し、クライアント側で DOM を構築するようにしました。

データアクセス層のオブジェクトの取得は依存性注入を行いたかったのですが JavaScript では若干困難ですので諦めました。

バインディングによる Model と View の連係

Model と View との連係は双方向データバインディングによっています。

下記のように、HTML 上の要素に属性を記述することでバインディングを指定しています。

1
2
3
4
5
6
7
8
9
<dl>
<dt>注文番号</dt>
<dd><b>data-property="orderNo"</b> data-input-type="static"></dd>
</dl>
<dl>
<dt>顧客名</dt>
<dd><input type="text" <b>data-property="customerName"</b> data-uneditable-behavior="disabled"/>
<span class="error-message" data-property-error="customerName"></span></dd>
</dl>

上記コードでdata-nodeはプロパティの親となるエンティティや埋め込みバリューを指定し、data-propertyはプロパティを指定しています。

独自のテンプレート構文を導入するのではなく pure HTML で実装することで、デザイナーとの分業がスムーズになります。お納めしたシステムをお客様のデザイナーが更新しています。

サーバー側に Laravel を採用すれば Blade ファイルとなるのですが、JavaScript ファイル読み込みのためにタグを使っているのみなのでデザイナー様も問題なく扱っておられます。

算出プロパティ

Vue.js を調べるうちに 算出プロパティ という機能に興味を持ちました。

この機能は計算式を書いておけば必要なときに計算式が自動的に実行されるものです。

算出プロパティはangularJSで既に実現されていたようです。

以前に当方も独自に同様の機能を作成したことがあるのですが、計算式と合わせて計算のトリガーとなるプロパティ名を設定しなければならないもので、あまり便利とは言えませんでした。
一方 Vue.js の算出プロパティでは計算式さえ設定すればよく、非常に便利なものでした。仕組みを調べると、計算式を評価するたびに依存元を分析・記録するようになっていました。
パフォーマンス的な点が心配になりましたが、そもそも依存元の値に変化がなければ計算式を評価しないので問題は少ないだろうと結論付けています。

早速、同様の機能を当方の Model にも実装しました。採用してみると大変便利で依存性のことをほとんど意識しなくてよく、宣言的に計算式を書きさえすればよくなりました。また、オブジェクトの境界を越えて依存性をたどってくれる点も便利です。

算出プロパティ実装時に工夫した点はリストのようにメソッド呼び出しによって内部の値を変えるケースにも対応したことです。例えば、明細を集計した金額を表示するといったケースにも対応できます。

次のように記述できます。このコードとデータバインディングを組み合わせることによって明細が入力されたら常に合計を計算できます。※コピペで使えるものではありません。

1
2
3
4
5
6
7
8
9
10
11
12
13
export class Order extends Data.ObservableObject {
/**
* 初期化します。
*/
initialize() {
const totalAmountExpression = () => {
return <b>Enumerable.from(this.orderDetails).sum(item => item.amount)</b>
} //合計金額
this.defineProperty('totalAmount', DataType.Number, totalAmountExpression) //合計金額
this.defineInternalChangeProperty('orderDetails',
new Data.ListChangeDetector(),
new Data.ObservableList({createItem: () => new OrderDetails(this)})) //受注明細リスト
}

今回の開発とは関係がありませんが、算出プロパティを C# にも移植し、便利に使っております。

仮想 DOM

React、Vue.js では仮想 DOM によってレンダリングの負担を軽減しています。当方の開発したフレームワークでは仮想 DOM を採用していません。Model が変化すればバインディングによって同時に DOM も変化します。この点でパフォーマンスの心配がありました。

動作させてみると DOM が変更されたらすぐにブラウザーがレンダリングするのではなく、一連のスクリプトが終了してからレンダリングしているようです。

仮想DOMのパフォーマンスについて Vue・React・Angularのパフォーマンス比較検証 という興味深い記事がありました。リンク先のテストではいずれの場合も仮想DOMを使った場合パフォーマンスが落ちています。但し、ほぼすべての要素が変更される条件でのテストなので仮想DOMのメリットが出にくいです。

当方は Firefox でテストしてみたところそれでも仮想DOMの方が早かったです。

Firefox では仮想DOMが早く、Chrome では実在DOMの方が早いという結果から、Chromeでは逐次的にDOMに変更が加えられてもパフォーマンスが落ちないように対策されていると言えます。

以上のことから対象ブラウザーやスピードに求められる条件にもよりますが仮想DOMなしで実用になると考えています。

不変オブジェクト

React では仮想DOMの変更箇所を効率的に特定するために各オブジェクトを不変としています。

不変オブジェクトの場合、参照が同じであれば配下の構造は変更がないと解釈できるので構造を末端までたどる必要がないのです。

オブジェクトのプロパティを変更する場合は、該当するプロパティに変更後の値が設定された新しいオブジェクトを作る必要があります。新しいオブジェクトを作ればそのオブジェクトを所有するオブジェクトも連鎖的に作成する必要があります。オブジェクトグラフはツリー構造になっていますので、変更を末端から最上位まで連鎖的に伝える必要があります。

この作業は負担になりますので、immer など不変でない操作を不変操作に変換するライブラリを使って負担を軽減できます。

当方の仕組みでは、可変オブジェクトを使用していますのでこのような配慮は不要です。また対応する DOM 要素はバインディングによってピンポイントに結合されているので、変更箇所を特定するためのオーバーヘッドはありません。

開発したフレームワークを採用した結果

開発したフレームワークを採用した結果、下記のメリットを感じました。

  • Model, View に分離することですっきりとした構成になった。特に今回の開発で同じエンティティを2箇所以上に表示すると言った要件がたまたまあったが、これに難なく対応できた。
  • サーバーとのデータ交換を JSON のみにしたことで HTML の POST, GET に関連する複雑な仕様に悩まされることがなくなった。
  • 算出プロパティによって記述量が減った。要件変更にも対応しやすくなった。
参照

Laravel

遅まきながらLaravelを採用したシステムを開発しました。開発を通じて感じた利点・欠点を紹介します。

HTMLとデータのバインディングはクライアント側で行いました。ですので Laravel が備えているBladeについては最小限の機能しか使っていません。

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

  • 情報がSymfonyに比べて豊富にある。
  • クエリービルダーおよびEloquentのモデルで記述できるクエリーがDQLに比べて書きやすい。
  • 配列をラップしたCollectionのメソッド充実している。
  • Eloquent モデルがメモリーを圧迫することに対して対策が用意されている。
  • データベーステーブルの生成、テストデータの生成機能が用意されている。これを活用することで開発中の早い段階でバグに気が付くことができた。

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

  • Eloquent が ActiveRecord であるためドメイン層とデータアクセス層の分離ができない。データアクセス関係のメソッドが静的メソッドになっているのでコンストラクタインジェクションにより生成したDIオブジェクトを利用できない。
  • 複合主キーがサポートされない。例えば伝票に紐づく伝票明細テーブルは伝票番号・伝票明細番号の組み合わせで表されるサロゲートキーの他に単独カラムで一意となるサロゲートキーを付けなければならない。
  • データベース・ドメイン間の変換をカスタマイズしにくい。埋め込みバリュー(値オブジェクト)に対するサポートが中途半端。(toArrayメソッドの対応)
参照

分散アプリケーション

このところ立て続けに ASP.NET Web Service(asmx) を使った分散アプリケーションに携わりました。

クライアントサーバー型アプリケーションの形態で、サーバーに置いたデータベースにクライアントプログラムからアクセスするものがあります。それに対して、分散アプリケーションでは、サーバーでもプログラムを実行しクライアントプログラムからの要求に対してサーバー側プログラムが応答を返します。
分散アプリケーションでは、複数のデータベース操作を1つの要求にまとめられます。例えば明細付き伝票の保存、月次更新などをそれぞれ1回の要求にまとめられます。このためクライアントサーバー間の通信量・通信頻度を減らせます。

当方は今まで上記の内分散でないアプリケーションだけを作成してきました。今回は ASP.NET Web Service ですでに構築されているシステムへの機能追加でした。

今回、取り扱った感想としては構成が複雑になるということです。

その原因の一つに、プロクシタイプの自動生成があります。サーバー側プログラムで提供するサービス(いわゆるメソッドの集合)を作成してから、クライアントプログラムで Web参照の更新 を実行すると、サービスを呼び出すメソッドが生成されます。
このとき、引数や戻り値に対応する型がプロクシタイプとして作られるのですが、ここでつくられた型はサーバープログラムで定義されたものとは異なります。
サーバーでエンティティ類を定義してセッター・ゲッターを記述しても、クライアントには反映されず、サーバーと同等のロジックが処理されません。

後継の WCF では同じ型を参照していればプロクシタイプが作成されず、参照された型が直接使われます。

WCF はさらに gRPC にとって変わられようとしています。.proto ファイルから自動生成すると、ASP.NET Web Service と同様プロクシタイプが生成されますので、これを防ぐためには下記のような工夫が必要になりそうです。

コードファースト ASP.NET Core gRPC