このページの本文へ

脱フレームワーク依存!JSのビジネスロジックを抽象化する最新ライブラリが登場

2016年12月09日 05時00分更新

文●Aaron Hanusa

  • この記事をはてなブックマークに追加
本文印刷
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をもう少し深く説明しながら、この方法でビジネスロジックを組むことで楽になるのはどのようなことなのかに目を向けます。

Sample architecture showing how peasy-js allows you to share business logic between the client and the server

上の図の左から右へみていくと、フレームワーク(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のルールのすばらしいところは、極めて柔軟性が高く、想定されるほとんどどのようなシナリオにも対応するように記述・設定ができる点です。たとえばAgeRuleNameRuleのバリデーションが成功したときだけ実行する、あるいはその反対にルールを組み合わすこともできます。ルールを使ってデータストアからデータ取得(高負荷になる場合がある)が必要なときに、大変役立ちます。

さらに詳しい情報は公式ドキュメントに掲載しています。

ビジネスロジックのテスト

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

Web Professionalトップへ

WebProfessional 新着記事