Angular、React、Aureliaなど、JavaScriptフレームワークが活況ですが、新しいフレームワークを覚えるのも移行するのも大変。ビジネスロジックを抽象化して再利用しやすくする新ライブラリーpeasy-jsの紹介です。
アプリケーションを作成するとき、大切なビジネスロジックを、使っているフレームワーク特有のコードで作ることがあります。たとえば、Angular を使っているなら、ビジネスロジックがいろいろなサービス、コントローラー、さらには実行コードにまで分散してしまうことは珍しくはありません。
バックエンドで動くJavaScriptにも当てはまります。たとえば、Mongoose のようなORM/ODM、またはほかのクロスカッティングコンサーン (横断的関心事)でデータにアクセスするビジネスロジックがあるSails のコントローラーアクションが、あちこち散らばってしまうのも同様です。
こうした組み合わせの結果、再利用、拡張、テスト、新技術への適応や移植が困難なコードになってしまいます。
記事では、アプリのフロントエンド・バックエンドの両方で再利用しやすく、フレームワークが変わっても使える方法でビジネスロジックを作るための、peasy-js ライブラリーの使い方を解説します。
情報開示 :私(Aaron Hanusa)はpeasy-jsの作者です!
フレームワークを使うのをやめるべきか?
いいえ、それどころかフレームワークはサーバー、クライアントの両方にとって大きな利点があります。ここで提案するのは、フレームワークを利用する側からは知る由もないコードの工夫によって、ビジネスロジックを組み合わせ可能なコンポーネントにまで抽象化することです。
ビジネスロジックのコンポーネント化で、簡単にテスト、交換、並べ替え、再利用ができます。コンポーネントは、どのようなJavaScriptクライアント、サーバー、データアクセス技術、フレームワークを使っても、すべてのアプリケーションアーキテクチャーからアクセスできます。
ビジネスロジックを分離する
peasy-jsは中間層のフレームワークです。ビジネスロジックを、組み合わせ、再利用、拡張、テストができるように作成しておけば、気まぐれなアプリケーションのUI、バックエンド、データアクセスフレームワークの変更が簡単にできます。別の言い方をすればpeasy-jsは、SoC(separation of concerns :関心の分離)の原則にのとったコードを生成することで、ビジネスロジックを再利用可能なコンポーネントにまで抽象化するガイダンスを提供するのです。
フレームワーク疲れ
ちょっと待ってください!
何を考えているか分かります。「あーあ、また別のフレームワークか」。はい、peasy-jsは確かにマイクロフレームワークです。しかし、ビジネスロジックのコンポーネント化に乗り出すならば、どのみち独自のマイクロフレームワークを書くことになるのです。
peasy-jsの設計、開発、テストに膨大な時間を費やし、ほぼ想定されうるすべてのワークフローに対応しました。難しくありませんから、習得に少しだけ時間を割く価値はあると思います。
あるいは、peasy-jsが自分には合わないと感じたとしても、peasy-jsパターンを使いながら、自分でビジネスロジック層を実装するためのアイデアを見つけてもらえれるならば、作者としては喜ばしい限りです。
主なコンセプト
peasy-jsで実現できるのは以下のようなことです。
使いやすくて柔軟な、ビジネス/バリデーションルールのエンジン
拡張性・再利用性(ビジネスロジックとバリデーションロジックを、ライブラリに依存したコードやフレームワークから分離)
テストのしやすさ
peasy-jsには4つのコンセプトがあります。以下に、それぞれの概要を簡単に説明します。また、あとでさらに深く説明します。
ビジネスサービス
ビジネスサービス (BusinessService)の実装では、ユーザーまたはプロジェクト側から、コマンドを使ってビジネスロジックの機能が使えるようにする必要があります。これらのコマンドは、CRUD(Create、Read、Update、Delete)機能やほかのビジネスロジック関連機能をカプセル化します。
コマンド
コマンド (Command)は、コマンド実行パイプライン を通じて、初期化のロジック、バリデーションルールとビジネスルールの実行、そのほかのロジック(データプロキシの呼び出しやワークフローロジックなど)の実行を調整します。
ルール
ルール (Rule)は、バリデーションルール(フィールドの長さや必須項目)やビジネスルール(ユーザー認証、価格の有効性チェックなど)を設定するために作ります。ルールはコマンドによって使用されます。複数のルールを合わせて、別のルールの実行結果によって実行するルールも作れます。また、実行結果によって、コードを実行するようにも設定できます。
データプロキシ
データプロキシ (DataProxy)は、データの保存と検索をしていて、以下のもの(に限りませんが)を含むデータ・ストアの抽象化レイヤーとして機能します。
リレーショナルデータベース:SQLite、MySQL、Oracle、SQL Serverなど
ドキュメントデータベース(非SQL):MongoDB、VelocityDBなど
サービス:HTTP、SOAPなど
キャッシュ・ストア:Redis、Azureなど
キュー:RabbitMQ、MSMQなど
ファイルシステム
テスト用のインメモリーデータストア
Peasy-js動作
注 :この項で解説する内容をすべて網羅した、ブラウザーで実行できる単純な例をplnkr に掲載しています。
クライアントのAngularのサービス内で、peasy-jsで書かれたビジネスロジックを利用するとどのようになるかを示す例です。
サンプルA var dataProxy = new CustomerHttpDataProxy();
var service = new CustomerService(dataProxy);
var customer = { name: "Frank Zappa", birthDate: new Date('12/21/1940') };
var command = service.insertCommand(customer);
command.execute(function(err, result) {
if (result.success) {
customer = result.value;
} else {
console.log(result.errors);
}
});
次に、サーバーのExpress.js のコントローラーで同じビジネスロジックを利用した場合、どのようになるかを示します。
サンプルB var dataProxy = new CustomerMongoDataProxy();
var service = new CustomerService(dataProxy);
var customer = { name: "Frank Zappa", birthDate: new Date('12/21/1940') };
var command = service.insertCommand(customer);
command.execute(function(err, result) {
if (result.success) {
customer = result.value;
} else {
console.log(result.errors);
}
});
違いが分かりますか。すばらしいのは、異なるデータプロキシをビジネスサービスに使ったこと以外は、違いがないことです。
データプロキシはデータへのアクセスの抽象化であり、ファイルシステムへのアクセス、データベース、キュー、キャッシュ、インメモリー、HTTP通信の具体的な実装に対応していることを覚えてください。
このように抽象化すれば、SoC、コードの再利用、簡便なテスト環境を実現しながら、望みのシステム構成と設定に基づいたデータプロキシの変更ができます。一見して分かりにくいのは、このアプローチではデータの送信元、送信先にかかわらず、いつもペイロード(データの中身)に同じビジネスロジックを適用している点です。これについては、あとで理解できるでしょう。
再利用する立場になれば、大したことではありません。利用する側の設計やテクノロジーに関係なく、peasy-jsで作られたビジネスロジックの利用は分かりやすいテーマでしょう。
設計といえば、peasy-jsをもう少し深く説明しながら、この方法でビジネスロジックを組むことで楽になるのはどのようなことなのかに目を向けます。
上の図の左から右へみていくと、フレームワーク(Angular, React, Backboneなど)を利用するクライアントアプリケーションがあります。最大限の拡張性を持たせたいなら、ビジネスロジックの実装は、UIフレームワーク側(サービス、コントローラーなど)ではなく、コンポーネント化された自身のコードベースもしくは中間層の側に移すことに注目してください。
次に、この中間層はWebサーバーと通信していることに注目してください。データプロキシがあるおかげです。先のサンプルAならば、ビジネスロジックを利用するAngularのサービスは、CustomerHttpDataProxy をインスタンス化しています。その結果、insertコマンドが実行されれば、供給されたペイロードに対して設定したビジネスルールが適用されます。バリデーションが成功したら、対応するデータプロキシのinsert 関数が呼び出され、設定した顧客のエンドポイントに対してPOSTリクエストが発行されます。
一方で、フロントエンドで使用される同じビジネスロジックが、node.jsアプリケーションでも使われることに注目してください。サンプルBを見ると、このビジネスロジックを利用するExpress.jsのコントローラーは、CustomerMongoDataProxy をインスタンス化しています。しかし、今回はinsertコマンドが実行されると、対応するデータプロキシのinsert 関数はデータベースに対して、MongoDB APIまたはMongooseのようなORDを用いてINSERTを実行します。
最後に、このデータプロキシの実装は一貫して同じインターフェイスなので、アプリケーションをどのようにデプロイしたいかに合わせて、ビジネスサービスにデータプロキシを付加できます。サンプル中のビジネスサービスは、クライアント側のHTTPサービスと交信するデータプロキシを使用しています。しかし、一度リクエストがWeb APIに受け付けられたら、Node.jsにホストされた同じビジネスサービスに対して、データベース、キュー、キャッシュ、ファイルシステムなどと交信するデータプロキシが付加されます。
実装例
これで、高度なpeasy-jsの内容と、なにができるのかが理解できたので、実装例を説明します。
CustomerHttpDataProxy var CustomerHttpDataProxy = function() {
var request = require('request');
return {
insert: insert
};
function insert(data, done) {
request({
method: 'POST',
url: 'http://localhost:3000/customers',
body: data,
json = true
}, function (error, response, body) {
done(error, body);
}
);
};
};
CustomerMongoDataProxy var CustomerMongoDataProxy = function() {
var connectionString = 'mongodb://localhost:12345/orderEntry';
var mongodb = require('mongodb').MongoClient;
return {
insert: insert
};
function insert(data, done) {
mongodb.connect(connectionString, function(err, db) {
if (err) { return done(err); }
var collection = db.collection('customers');
collection.insert(data, function(err, data) {
db.close();
done(err, data);
});
});
};
};
これらのデータプロキシのコード例では、一貫して同じインターフェイスでありながら、実装ロジックを抽象化していることに注目してください。実装ロジックの抽象化によりアプリケーションの拡張が可能になります。データプロキシを入れ替えれば、利用する側のコード(クライアントまたはサーバー)と完全に切り離された真に再利用可能な中間層ができたことが分かります。このデータプロキシのデザインコンセプトこそ、拡張性とテストの容易さを実現するカギなのです。
最後に、簡潔にするためにデータプロキシにinsert関数しか定義しなかったことに注意してください。実際の制作環境ではたいていCRUD機能の実装と、さらなる機能が必要でしょう。ここ でCustomerMongoDataProxyの完全な実装について参照できます。
CustomerService var CustomerService = BusinessService.extend({
functions: {
_onInsertCommandInitialization: function(context, done) {
var customer = this.data;
utils.stripAllFieldsFrom(customer).except(['name', 'address']);
utils.stripAllFieldsFrom(customer.address).except(['street', 'zip']);
done();
}
}
}).service;
上の例では、CustomerServiceの公開されたinsertCommand の初期化ロジックを記述し、データプロキシのinsert 関数が呼び出される前にフィールドをホワイトリスト化しています。ビジネスサービスからアクセスする各デフォルトCRUD機能は、各コマンドに対応するイベントフックが用意されています。これらのメソッドはここ で参照できます。
例では静的なBusinessService.extend 関数を使用しています。返されたオブジェクトのserviceメンバーを通じてアクセスできる、コンストラクター関数を作成しています。ES6(ECMA Script6)の継承もしくはプロトタイプ継承を使うほうがやりやすいと思えば、そちらを使ってもかまいません。ここ に両方のサンプルがあります。
これでビジネスサービスのinsertCommand の初期化ロジックを定義できたので、いくつかのルールを作って組み込みます。
NameRule var NameRule = Rule.extend({
association: "name",
params: ['name'],
functions: {
_onValidate: function(done) {
if (this.name === "Jimi") {
this._invalidate("Name cannot be Jimi");
}
done();
}
}
});
AgeRule var AgeRule = Rule.extend({
association: "age",
params: ['birthdate'],
functions: {
_onValidate: function(done) {
if (new Date().getFullYear() - this.birthdate.getFullYear() < 50) {
this._invalidate("You are too young");
}
done();
}
}
});
両方の例で静的なRule.extend メソッドを使用したことに注目してください。これでコンストラクター関数を作成します。先と同様に、ES6(ECMA Script6)の継承もしくはプロトタイプ継承も使用できます(例はこちら )。
それではCustomerServiceに組み込みます。
ルールを組み込む
var CustomerService = BusinessService.extend({
functions: {
_onInsertCommandInitialization: function(context, done) {
var customer = this.data;
utils.stripAllFieldsFrom(customer).except(['name', 'address']);
utils.stripAllFieldsFrom(customer.address).except(['street', 'zip']);
done();
},
_getRulesForInsertCommand: function(context, done) {
var customer = this.data;
done(null, [
new NameRule("name", customer.name),
new AgeRule("age", customer.birthDate)
]);
}
}
}).service;
上の最後のコードで、ルールをビジネスサービスに組み込み、insertコマンドの実行パイプラインに付加できました。_getRulesForInsertCommand() 関数の実装を追加して実現しました。
例では、どちらのルールも他方の結果にかかわらず実行するようにしました。たとえば、名前のルールのバリデーションに失敗したときでも年齢のルールのバリデーションは実行されますし、その逆も同じです。
peasy-jsのルールのすばらしいところは、極めて柔軟性が高く、想定されるほとんどどのようなシナリオにも対応するように記述・設定ができる点です。たとえばAgeRule はNameRule のバリデーションが成功したときだけ実行する、あるいはその反対にルールを組み合わすこともできます。ルールを使ってデータストアからデータ取得(高負荷になる場合がある)が必要なときに、大変役立ちます。
さらに詳しい情報は公式ドキュメント に掲載しています。
ビジネスロジックのテスト
peasy-jsはSOLID プログラミングの原則に基づいているので、ビジネスサービス、コマンド、ルールのテストがとても簡単です。
NameRule のテストがどれほど簡単かを示します。
it("fails when the supplied name is Jimi", () => {
var rule = new NameRule("Jimi");
rule.validate(() => {
expect(rule.valid).toBe(false);
expect(rule.association).toEqual("name");
});
});
it("succeeds when the supplied name is not Jimi", () => {
var rule = new NameRule("James");
rule.validate(() => {
expect(rule.valid).toBe(true);
});
});
ルールをシンプルかつまとを絞ったものにすることで、再利用しやすくなるだけでなく、テストも大変楽になります。ビジネスサービスやカスタムコマンドをテストする場合にもあてはまります。
テストはそれだけで大きなトピックなので、記事はここで終わりにしたほうが良さそうです。peasy-jsのビジネスロジックは非常にテストしやすく、たくさんのテストサンプルがここ にあることを覚えておいてください。
もっと知りたいですか?
peasy-jsで作った中間層の見本として、オーダー入力・在庫管理の完全なサンプルアプリケーション を用意しています。ビジネスロジックを利用しているのは、Node.jsにホストされた、Web APIでアクセス可能なExpress.jsアプリケーションです。サンプルには簡単に短時間で実行できるように、ドキュメント類も用意してあります。
peasy-jsによって、フレームワークからきれいに分離されたビジネスロジックを書けます。有益な副作用は、幾通りもの方法でコードをデプロイしやすくなることです。さらに、いま使っているフレームワークが古くなっても、新しいフレームワークに移植したり対応させるのが簡単なことです。
※この記事は、Stephan Max が査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者の皆さんに感謝します。
(原文:Write Reusable JavaScript Business Logic with peasy-js )
[翻訳:西尾健史/編集:Livit ]