Nightwatchを解説&導入してみる① 初めの一歩 サンプルコード有

環境:Windows10,  Nightwatchのバージョン:1.7.11

Nightwatchとは

NightwatchというjavascriptのE2Eのテストフレームワークを導入してみます。

Nightwatch公式サイト
https://nightwatchjs.org/

Nightwatchとは、日本語で宿直とかいう意味ですね。夜、見回りをする警備員さんのことです。

E2E はEnd to Endの略です。

End to End は何かといいますと、ここでの意味は、ユーザーがする動作のようなテストを行うことですね。
例えば、ログインのページだと
「ユーザーがログインIDとパスワードを入力欄に入力し、ログインボタンをポチっと押すと、ログイン後のページに遷移して『ようこそ 〇〇様』と表示する。」
という流れをテストすることです。

ユニットテストがプログラムのメソッド単位でテストを行うことに対比して言われることが多いでしょう。

さて、先の例で行きますと
「ユーザーがログインIDとパスワードを入力欄に入力し、ログインボタンをポチっと押すと、ログイン後のページに遷移して『ようこそ 〇〇様』と表示する。」
このテストを人間がいちいち行うのはまったくもって手間ですよね!
しかも、複数ブラウザで行うとかなると、手間×4ぐらいあります。

なので、ここは自動化しようというわけです。

余談ですが、弊社ではこのテストをGhost Inspectorというツールを使ってやっていました。が、 Ghost Inspector はソースコードの品質を高めるために役に立っているかというと、そうではなく、サービスの死活監視的に用いられているので、今回改めてもうちょっとjavascriptのプログラム的なテストフレームワークを導入してみようかなと思った次第です。(意識高く!(`・ω・´))

表題は導入してみる、となっていますが、本サービスに導入するかどうかは今の時点で未定です。(笑) ただ、開発環境としては作ってみたというところですね。

私自身はjavascript界からちょっと離れていました。
(こいつ、いつもこれ言ってんな…。(´ω`))
初心者なので、間違っているところ等ありましたら教えてください。

ではいよいよ、Window10にNightwatchをインストールします。

手順① Nightwatch をインストールするディレクトリを作る

まっさらな洗い立てのシーツのようなディレクトリを作ります。
ここでは、

D:\study\nightwatch

に作りました。

手順② Nightwatch自体をインストール

コマンドプロンプトを開いて、そのディレクトリ内に移動し、

npm install nightwatch

とやります。

すると下記の図のようにいろいろメッセージが表示されてインストールができます。

もし、npmがわからない、npmが動作しない場合は、node.jsとnpmをインストールして使えるようにしてください。
npmとはパッケージマネージャーと呼ばれるツールで、こういうフレームワークをインストールする場合などに使います。ナウなjs界では欠かせない存在なので、躊躇せずにインストールしてみてください。

これで、Nightwatch自体がインストールされました。

手順③ ChromeのWebdriverをインストール

同じディレクトリで

npm install chromedriver --save-dev

とやります。すると、下記のように表示されて、インストールが終わります。

これは何をインストールするのかというと、ChromeのWebdriverをインストールしています。手っ取り早く言うと、これがChromeを動作させてして、テストの時に使ってくれるわけです。

手順④ Selenium Serverインストール

次に同じディレクトリで、次のようにやります。

npm install selenium-server --save-de

すると、上記のようにメッセージが表示されて、インストールができます。

今回は、Selenium Serverというのをインストールしました。 Selenium Server は何かというと、③のChrome DriverはChromeを動作させますが、ほかのブラウザなどを動作させたりします。(現時点ではいらないのかもしれない)

これで、インストールするものは一通り終わりです。

手順⑤設定などを準備する

まだ設定などが必要です。さっきから使っている

D:\study\nightwatch

の中に、node_modulesというディレクトリができていて、ここにウジャッといろいろなモノが入っています。その中のnightwatchというディレクトリに、examplesというディレクトリがあって、そこにサンプルが入っています。ありがたや~


D:\study\nightwatch\node_modules\nightwatch\examples\tests

testというディレクトリの中の、ecosia.js というファイルをコピーして、nightwatchディレクトリの中に、test というディレクトリを作って、そこに貼り付けます。

このようになっているはずです!

ecosia.jsを参考までに貼っておきます。ちな、ecosiaはドイツの検索エンジン?らしいです。

describe('Ecosia.org Demo', function() {

  before(browser => browser.url('https://www.ecosia.org/'));

  test('Demo test ecosia.org', function (browser) {
    browser
      .waitForElementVisible('body')
      .assert.titleContains('Ecosia')
      .assert.visible('input[type=search]')
      .setValue('input[type=search]', 'nightwatch')
      .assert.visible('button[type=submit]')
      .click('button[type=submit]')
      .assert.containsText('.mainline-results', 'Nightwatch.js');
  });

  after(browser => browser.end());
});

これがテストスクリプトです。解説すると、

https://www.ecosia.org/

というサイトを開き、タイトルが ’Ecosia’ で、検索ボックスにnightwatchと入れて検索して、クリックすると、Nightwatch.js という文字列が含まれるページが出てくる、というテストですね。

次の2つのファイルをD:\study\nightwatch に追加します。

global.js

const chromedriver = require('chromedriver');

module.exports = {
    before: function (done) {
        chromedriver.start();
        done();
    },

    after: function (done) {
        chromedriver.stop();
        done();
    }
};

使うWebdriverなどを指定しています。

nightwatch.conf.js

// Autogenerated by Nightwatch
// Refer to the online docs for more details: https://nightwatchjs.org/gettingstarted/configuration/
const Services = {}; loadServices();

module.exports = {
  // An array of folders (excluding subfolders) where your tests are located;
  // if this is not specified, the test source must be passed as the second argument to the test runner.
  src_folders: ["test"],

  // See https://nightwatchjs.org/guide/working-with-page-objects/
  //page_objects_path: 'page-objects',

  // See https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-commands
  custom_commands_path:  '',

  // See https://nightwatchjs.org/guide/extending-nightwatch/#writing-custom-assertions
  custom_assertions_path: '',

  // See https://nightwatchjs.org/guide/#external-globals
  globals_path : './globals.js',

  webdriver: {},

  test_settings: {
    default: {
      disable_error_log: false,
      launch_url: 'https://nightwatchjs.org',

      screenshots: {
        enabled: false,
        path: 'screens',
        on_failure: true
      },

      desiredCapabilities: {
        browserName : 'firefox'
      },

      webdriver: {
        start_process: true,
        server_path: (Services.geckodriver ? Services.geckodriver.path : '')
      }
    },

    

    firefox: {
      desiredCapabilities : {
        browserName : 'firefox',
        alwaysMatch: {
          acceptInsecureCerts: true,
          'moz:firefoxOptions': {
            args: [
              // '-headless',
              // '-verbose'
            ]
          }
        }

      },
      webdriver: {
        start_process: true,
        port: 4444,
        server_path: (Services.geckodriver ? Services.geckodriver.path : ''),
        cli_args: [
          // very verbose geckodriver logs
          // '-vv'
        ]
      }
    },

    chrome: {
      desiredCapabilities : {
        browserName : 'chrome',
        'goog:chromeOptions' : {
          // More info on Chromedriver: https://sites.google.com/a/chromium.org/chromedriver/
          //
          // This tells Chromedriver to run using the legacy JSONWire protocol (not required in Chrome 78)
          w3c: false,
          args: [
            //'--no-sandbox',
            //'--ignore-certificate-errors',
            //'--allow-insecure-localhost',
            //'--headless'
          ]
        }
      },

      webdriver: {
        start_process: true,
        port: 9515,
        server_path: (Services.chromedriver ? Services.chromedriver.path : ''),
        cli_args: [
          // --verbose
        ]
      }
    },

    edge: {
      desiredCapabilities : {
        browserName : 'MicrosoftEdge',
        'ms:edgeOptions' : {
          w3c: false,
          // More info on EdgeDriver: https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/capabilities-edge-options
          args: [
            //'--headless'
          ]
        }
      },

      webdriver: {
        start_process: true,
        // Download msedgedriver from https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/
        //  and set the location below:
        server_path: '',
        cli_args: [
          // --verbose
        ]
      }
    },

    //////////////////////////////////////////////////////////////////////////////////
    // Configuration for when using the browserstack.com cloud service               |
    //                                                                               |
    // Please set the username and access key by setting the environment variables:  |
    // - BROWSERSTACK_USER                                                           |
    // - BROWSERSTACK_KEY                                                            |
    // .env files are supported                                                      |
    //////////////////////////////////////////////////////////////////////////////////
    browserstack: {
      selenium: {
        host: 'hub-cloud.browserstack.com',
        port: 443
      },
      // More info on configuring capabilities can be found on:
      // https://www.browserstack.com/automate/capabilities?tag=selenium-4
      desiredCapabilities: {
        'bstack:options' : {
          userName: '${BROWSERSTACK_USER}',
          accessKey: '${BROWSERSTACK_KEY}',
        }
      },

      disable_error_log: true,
      webdriver: {
        timeout_options: {
          timeout: 15000,
          retry_attempts: 3
        },
        keep_alive: true,
        start_process: false
      }
    },

    'browserstack.local': {
      extends: 'browserstack',
      desiredCapabilities: {
        'browserstack.local': true
      }
    },

    'browserstack.chrome': {
      extends: 'browserstack',
      desiredCapabilities: {
        browserName: 'chrome',
        chromeOptions : {
          w3c: false
        }
      }
    },

    'browserstack.firefox': {
      extends: 'browserstack',
      desiredCapabilities: {
        browserName: 'firefox'
      }
    },

    'browserstack.ie': {
      extends: 'browserstack',
      desiredCapabilities: {
        browserName: 'internet explorer',
        browserVersion: '11.0'
      }
    },

    'browserstack.safari': {
      extends: 'browserstack',
      desiredCapabilities: {
        browserName: 'safari'
      }
    },

    'browserstack.local_chrome': {
      extends: 'browserstack.local',
      desiredCapabilities: {
        browserName: 'chrome'
      }
    },

    'browserstack.local_firefox': {
      extends: 'browserstack.local',
      desiredCapabilities: {
        browserName: 'firefox'
      }
    },
    //////////////////////////////////////////////////////////////////////////////////
    // Configuration for when using the Selenium service, either locally or remote,  |
    //  like Selenium Grid                                                           |
    //////////////////////////////////////////////////////////////////////////////////
    selenium_server: {
      // Selenium Server is running locally and is managed by Nightwatch
      selenium: {
        start_process: true,
        port: 4444,
        server_path: (Services.seleniumServer ? Services.seleniumServer.path : ''),
        cli_args: {
          'webdriver.gecko.driver': (Services.geckodriver ? Services.geckodriver.path : ''),
          'webdriver.chrome.driver': (Services.chromedriver ? Services.chromedriver.path : '')
        }
      }
    },

    'selenium.chrome': {
      extends: 'selenium_server',
      desiredCapabilities: {
        browserName: 'chrome',
        chromeOptions : {
          w3c: false
        }
      }
    },

    'selenium.firefox': {
      extends: 'selenium_server',
      desiredCapabilities: {
        browserName: 'firefox',
        'moz:firefoxOptions': {
          args: [
            // '-headless',
            // '-verbose'
          ]
        }
      }
    }
  }
};

function loadServices() {
  try {
    Services.seleniumServer = require('selenium-server');
  } catch (err) {}

  try {
    Services.chromedriver = require('chromedriver');
  } catch (err) {}

  try {
    Services.geckodriver = require('geckodriver');
  } catch (err) {}
}

テストのプログラムはtestディレクトリにあるよ、などが書いてあります。

手順⑦テストを実行

ハァハァ、いよいよ…。テスト実行です。D:\study\nightwatchで

   .\node_modules.bin\nightwatch -v 

とやります。

おおっ!

OK. 5 assertions passed. (1.818s

と表示されましたね!!

そして、皆さんの目の前で、ブラウザっぽい画面が一瞬開いて検索っぽいことをやっているのが見えたでしょうか?

今回は基本のキでインストールして動作させるところだけで終わってしまいました。


エリック・エヴァンスのドメイン駆動設計を読みました③

前回の投稿

■蒸留
蒸留とは混ざり合ったコンポーネントを分離するプロセス。
モデルとは知識が蒸留されたもの。
今までの章で行ってきたドメインモデルの洗練も蒸留ではあると思いますが、この本の15章の蒸留はここまで洗練してきたドメインモデル群を更に大事なものから優先順位をつけて分離していくことを言っていると思います。
この蒸留の目的は、システム内で最大の価値を付加すべきものをたった1つ抽出すること。
抽出されたものがコアドメイン。
そのコアドメインは当然我々のソフトウェアを特徴づけ、構築する価値のあるものにする。
化学の蒸留と同様に、蒸留プロセスにおいて分離されたかなり価値がある副産物(汎用サブドメインなど)が出来上がる。

・コアドメイン
ドメインに関心があって技術的にも優秀なメンバーがコアドメインに当たるべきとのこと。
ただ技術的に優れていてもドメインに関心がない人が多い傾向にあるためなかなかそんな人はいない。
ただそういう場合はドメインに関心がある優秀なメンバーを集め、ドメインエキスパートが参加するチームを収集し補佐する。
コアドメインの選択はその「システム内で最大の価値を付加すべきもの」なので、システムによって様々。

・汎用サブドメイン
システムを機能させて、モデルを完全に表現するためには欠かせないもので補佐役を果たす。
相当数のビジネスで必要になる概念を抽象化している。
例としては、企業の組織図、財務に関するもの、タイムゾーンなど。

汎用という言葉があるがこれはコードが再利用可能ということではない。
再利用性を考慮していては、蒸留のコアドメインに集中するという動機から外れてしまう。
汎用的な概念の範囲内に収めるということには集中する。

・ドメインビジョン声明文
コアドメインとそれをもたらす価値に関する簡潔な記述を作成する
チームに共通の方向性を与える。
新しい洞察を得たら改訂すること。
ドキュメントって最初は気合い入れて作るけど、改訂を怠りがちになりますよね。。。

・強調されたコア
ドメインビジョン声明文は広い観点から見たコアドメインを識別するもの。
強調されたコアは具体的なコアドメインの要素の識別をするための文書。
これがないと個人の解釈によって識別することになってしまうため、好ましくない。
記述方法として「蒸留ドキュメント」や「コアにフラグを立てる」というものがある。
蒸留ドキュメントはコアドメインとコアを構成する要素間の主要な相互作用を簡潔に記述する。
コアにフラグを立てるというのはUML図などでコアとなる要素に印をつけておくこと。

・凝集されたメカニズム
オブジェクト思考設計にとってカプセル化は標準的なプラクティス。
「何が」(what)と「どのように」(how)を分離するが、「何が」(what)が、「どのように(how)」によって肥大化し、複雑化することがある。問題を解決するためのアルゴリズムが多くなって問題を表現するメソッドがわかりにくくなる。
こういうことが多くなってきたらモデルに問題がある兆候。
この問題を解決するためのアルゴリズムなどを概念的に凝集された部分を切り分けて、フレームワーク化する。

汎用サブドメインも凝集されたメカニズムもコアドメインの負担を軽減したいという願望に基づいているが、汎用サブドメインは表現力豊かであるモデルで、凝集されたメカニズムはドメインを表現しない。
モデルにより提起された面倒な処理の問題を解決するためにある。

・隔離されたコア
モデルをリファクタリングして、補助的な役割を果たすものから分離すること。
そうすることで凝集度が高まり、他のコードへの結合が低くなる。

隔離するために

  1. コアサブドメインを識別する
  2. 関連するクラスを、それに関係づけている概念に由来する新しいモジュールへ移動する。
  3. コードをリファクタリングして、概念を直接表現していないデータと機能を切り離す。
  4. 他のモジュールとの関係を最小限におさえて明確化する。
  5. 1から繰り返す

・抽象化されたコア
モデルにおけるもっとも基本的な概念を識別し、それを別のクラス、抽象クラスまたはインターフェースに括り出す。
この抽象的なモデルは、重要なコンポーネント間をほとんど表現するように設計する。
この抽象的なモデル全体を独自のモジュールに入れる。
ただし、特殊で詳細な実証クラスは、サブドメインによって定義されたモジュールに残す。

■大規模な構造
ようやく森を見る。
森を見るためにそれぞれの木の役割や関係性を明確にしておく。
概念上の大規模な構造をアプリケーションと共に進化させ、場合によって、全く別の種類の構造に変更する。
大規模な構造は一般に、境界付けられたコンテキストを横断して適用できる必要があり、現実の制約にも対応しなければならない。

大規模な構造を適用すべきなのは、モデルの開発に不自然な制約を強いることなく、システムを大幅に明確化する構造が見つけられたときでうまくいかなければ別になくてもよい。

・責務のレイヤ(システムのメタファ、知識レベル、着脱可能なフレームワークは略)
責務駆動設計は大規模な構造に対しても適用される。
責務駆動設計と同じように、上位層は下位層に依存するが、下位層は上位層からは独立するような形。
レイヤ化すると見通しがよくなるかもしれない。
過度なレイヤ化は逆にわからなくなるので注意。
モデルの依存関係を調べ、ドメインに自然な階層が認められたら責務を与える。

■感想
2019年12月、自分は初めてPHPカンファレンスに参加しました。
その初ペチコンで初めて受講した講義が、

MVCにおける「モデル」とはなにか
https://fortee.jp/phpcon-2019/proposal/b6302d4a-a8b8-41a7-ad0c-98704fd18c6c

でした。
正直内容が難しく、後々の講義もこのレベルが続くのか…と絶望していました。
後々、参加者の感想を見ると熟練者の方でもこのトークは難しいレベルだったみたいです。

ただ今回、当時のトークで

(飲食店等において)
ホールの人は「客とオーダーが満たされているか」が関心事であり、
キッチンの人にとっては「料理をどの順番で提供しなければならないかが関心事」

と聞いたことを思い出し、この関心事に注目してモデリングすべきなんだと本を読みながら感じることが出来ました。
誰にとっては何が値オブジェクトで何がエンティティなのかとかですね。
当時わからなかったことが少しでもわかるようになったのは嬉しかったです。

設計の本を読むとプログラマに最も必要なのはコミュ力ではないかと思ってきますね。

どうしてコミュ障に優しい世界じゃないんですか…?

エリック・エヴァンスのドメイン駆動設計を読みました②

前回の投稿

第4部 戦略的設計

システムがだんだん複雑になってくると、巨大なモデルを扱ったり把握する必要が出てくる。
そうした状況下では単一の巨大なユニットとして管理するのは難しいため、分解しなければならない。
ここで、小さなシステムに分解していくときに、陥りがちなのが全体が見えなくなること。
「木を見て、森を見ず」と述べられている。

そうならないように以下の3つのテーマを考察している

  1. コンテキスト
  2. 蒸留
  3. 大規模な構造

■コンテキスト
コンテキストについて見ていく前にそもそもコンテキストってなんだって感じですよね。
androidの開発をやっていても当たり前のように出てきます。
辞書を引いても何とも曖昧でややこしいです。

ロングマン先生に依ると

1 the situation, events, or information that are related to something and that help you to understand it
2 the words that come just before and after a word or sentence and that help you understand its meaning

とあります。
日本語でも文脈、前後関係、事情、背景、状況と訳されたりしています。

少し具体的な例を挙げると、
私がテレビを見ていてそこには芸能人が映っているとします。
そこで近くにいる家族が
「ちょっと手伝って~」
と言ったときに、テレビに映っている芸能人に言っているとは思わないですよね?
手伝えるのはテレビを見ている私の方です。
これはコンテキストに依るものだと思います。

もう1つ挙げると…
「ほげ」
と聞くと何を思い浮かべますか?
プログラミング経験がある人やIT系の人だとサンプルコードなどでよく見かける
「hoge」
が浮かびますよね?
そうでない人だと
「何かのリアクション?」「捕鯨?」
みたいなこと思い浮かべるかもしれません。

商品と聞いて大体の人は商品そのものを浮かべるけど、配送業の人だと中身が入った段ボールのことを思い浮かべるみたいなことを聞いたことがあります。

同じ用語でも、受け取り手や場面などによって認識が変わってくるのはコンテキストに依るものだと認識しています。

コンテキストの内容に入っていきます。

境界づけられたコンテキストによって各モデルが適用できる範囲を定義する。
コンテキストマップによってそれぞれのコンテキストとコンテキスト間の関係性を全体的に概観できる。
継続的な統合によってモデルの統一が保たれる。

・境界付けられたコンテキスト
名前の通りコンテキストはあいまいでなく明示的に境界付ける。

明示的な境界は、チーム編成、そのアプリケーションに特有の部分が持つ用途、コードベースやデータベーススキーマなどの物理的な表現などの観点から設定する。
その境界内では、モデルを厳密に一貫性のあるものに保つ、また外部の問題によって注意をそらされたり、混乱させたられたりするのは避ける。

定義することで得られるものは明確さ。
そのコンテキスト外の人達は、コンテキスト内の事は明確に区切られているのであれば考えなくて済む。
ただし、境界を知っておかなければ、知らずに踏み込んでしまう可能性があるからそれは注意が必要。
接点があるコンテキスト間も同様。

・継続的な統合
境界づけられたコンテキストを定義したらそれを安定させなければならない。
継続的な統合はコンテキスト内で、2人以上での作業をする際には頭に入れておく。

絶えずそのコンテキストチーム内で以下2つの統合を行う。

  1. モデル概念の統合
    ユビキタス言語を継続的に練り上げる。そのためにメンバ間で絶えずコミュニケーションを行う。
  2. 実装の統合
    段階的に行われ、再生可能なマージ、ビルドテクニック
    自動化されたテストスイート
    変更が統合されない期間に対して、妥当な短さの上限を設定するルール

・コンテキストマップ
コンテキストを境界づけても全体的な視点を得られたとは言えない。
境界づけた時点での全体的視点は得られているかもしれないが、それぞれのコンテキスト内で統合を絶えず行っているため、得られているとは言い難い。
変更により境界が明示的でなくなる場合がある。
境界づけられたコンテキスト間のコードの再利用もそういった理由で避けるべき。

全体的な視点を得るためにコンテキストマップを利用する。
コンテキストマップの利用にあたり、以下のルールを守る
境界づけられたコンテキストにそれぞれ名前をつけ、その名前をユビキタス言語の一部にする
現在存在する領域の地図を書く、変換にはそのあと取りかかる
モデル同士の接点を記述して、あらゆるコミュニケーションで必要となる明示的な変換について概略を述べ、共有するものがあれば強調する
マップは常にありのままの状況を表すこと
実際に変更が完了するまではマップを変更しないこと

コンテキスト内であればユビキタス言語が方言化(同じ言葉でもコンテキスト外の人が使っているものとニュアンスの差異があることがある)することもあるが、境界づけられたコンテキストそのものに名前をつけることでどのコンテキストで作業している人でもあいまいにならずに済む。

これらが保たれると以下のより効果的な戦略へと移行できるようになる。

1. 共有カーネル
2つのコンテキスト間(あるいは複数のコンテキスト間)でモデルの一部を共有すること。
カーネルというとそれぞれのモデルの中核を共有するようにとらえるかもしれないが、モデル、コード、モデルの該当箇所に関連するデータベース設計なども含む。
注意点としては、共有している部分は共有していないコンテキスト内の変更に比べて頻度は下げること、他チームの相談なしに行わないこと。

2. 顧客(下流)/供給者(上流)の開発チーム
2つのコンテキストA, Bがあって、BがAからの入力を受け取るなどAがBに何かを要求する場合に顧客/供給者の関係を取ることがある。
A(供給者) → B(顧客)
この場合に、BがAの変更に対して、一方的に受け入れるだけならば簡単だが、Bが拒否権を持っていたりやAに対して何か手続きをさせるような場合は両者が円滑に作業できるようにしなければならない。
そのために…
・顧客の要求が最優先
・供給者チームが顧客を壊してしまわないかと恐れることなくコードを変更できるようにし。顧客チームが上流チームのことを常に気にすること自分の仕事に集中できるようにするには、自動化されたテストがないといけない。

3、4、5は顧客/供給者の関係にありながら顧客の要求に応える動機がない場合、2のことができないので検討する。

顧客の要求に応える動機がないというと攻撃的だが以下のような場合もある
・供給者が多くの顧客を抱えている
・2つのチームが経営階層上で遠く離れている
・昔からの顧客に市場の方向性が変わったため価値が見いだせない
など

3.順応者
一言でいうと上流チームに順応する。
上流チームのモデルに隷従しユビキタス言語を供給する。
下流チームの自由度は低くなるがシンプルになる。
下請けとかこんな感じかなと思う。
上流チームの設計が悪いと下流チームも悪くなるのでその辺りは気を付ける。

4.腐敗防止層
上流のモデルを下流で扱うのが困難な場合に検討する。
上流と下流の間にモデルの変換機能など腐敗防止層を用意してあげる。
そのインターフェースはデザインパターンのファサードパターンやアダプタパターンなどで構成されたりする。
他のシステムとの連携でデータを変換する出入り口とかがイメージしやすい。

5.別々の道
上流と下流の関係だったが、そもそも別々で良い場合など。
これまで学んできた上流と下流の統合方法は全てオーバーヘッドが存在するため、コストが発生する。
よって、それに見合う利益を生み出さないといけない。
利益を生み出せない場合などに、統合が必要なのかを改めて考える。
それでも必要な場合は1~4のどれかを考える。
とりあえず別々の道を検討して無理または面倒ならば何らかの形で統合するとかでいいかも

6. 公開ホストサービス
とあるサービスが他の多くのサービスなどから利用される場合は、共通のサービスを用意する。
手間なので1つ1つについて変換サービスを構築しない。
特定のサービス向けに拡張することはある。
公開APIなど。

7. 公表された言語
統合するお互いのコンテキスト間の変換では共通する言語を利用する。
片方に合わせるとモデルが崩れる恐れがある。
世に回っているドキュメントなどで広く使われているものなどを見つけれるといい。
この辺なんとなく上流に合わせがちになっている気がする…

実際にこれらのパターンをどう使っていくかですが、
新しくプロジェクトを始めるにしろプロジェクトが進行中にしろ
コンテキストマップを描こうとすることが大事になってくると思います。
そのためにはコンテキストの境界を付けること、継続的な統合を行う。
その後は変換できないシステム(外部システムやレガシー過ぎて手がつけられないシステム)に線を引く。

・新しくプロジェクトを始めている場合
作成中のシステム内で継続的な統合が困難になれば共有カーネルを探す。
依存関係が一方向であれば顧客/供給者の開発チームを選択する。

外部のシステムと連携する場合は最初に検討すべきは別々の道。
コストが低いので。
統合が不可欠であるなら、順応者か腐敗防止層かを考える。
出来るだけ腐敗防止層で考えたい。
順応者は心情的によろしくないので。

・プロジェクトが進行中の場合
まず共有カーネルを見つける。
コンテキストの重複した出来るだけ小さいところから始めていく。
テストは必ず作成する。

次に継続的な統合を行い両方を理解した人を増やす。
コンテキスト間でメンバーをローテーションする。

その後にレガシーシステムを段階的に廃止する。
テスト戦略の決定は不可欠で腐敗防止層を経由してレガシーシステムと通信する

公開ホストサービスがあるなら公表された言語が使えないか検討する。

エリック・エヴァンスのドメイン駆動設計を読みました①

難解と言われながら名著とされている「エリック・エヴァンスのドメイン駆動設計」を読みました。

備忘録として自分なりの解釈を入れたりしてメモを残しておきたいと思います。
間違いもあると思うのでご指摘ください。
この記事では3部まで記載しています。

第1部 ドメインモデルを機能させる
ドメインモデルとは何かですが、それぞれドメインとモデルを以下のように認識しています。

ドメイン
プログラム、システムが関心を持つ領域や対象

モデル
物や事象、概念などを現実にあるものや現実で行っていることをベースに必要な部分を厳選して抽象化すること

モデルの必要な部分というのがドメインによって変化するので、ドメインによって精錬されたモデルがドメインモデル。

ドメインモデルを機能させるために…
・知識をかみ砕く
大量の情報から重要な情報を抜き出す、重要な言葉を見つけ出す(1人で行うのはNG) 抜き出すためには何が重要な情報かを見極めるためにその分野の学習が必要になる

・コミュニケーションと言語の使い方
チームで誤解のないように共通の言葉(ユビキタス言語)を使いながらモデル化を行う。
もし名前がなければ付ける。
その名前の物が何をするのか、チーム全体で共有する。
それが共有できないとコードの共有をするのも難しくなる。

・モデルと実装を結びつける
モデル駆動設計を行う。手続き型ではない。

モデル駆動設計
ソフトウェア要素のサブセットがモデル要素と密接に対応している設計。
相互に一致した状態を保つ。

第2部 モデル駆動設計の構成要素
・ドメイン駆動設計がアーキテクチャに求めること
レイヤードアーキテクチャなどドメイン層が分離したもの

・ドメインモデルの構成要素
ドメインモデルはエンティティと値オブジェクトとドメインサービスから構成される。

エンティティ…同一性(IDなどの識別子を持つ)を持つオブジェクト
値オブジェクト…エンティティとは逆で同一性がないオブジェクト。
同じ色で同じマジックが2つあったらどちらを使うのか気にしない

※同じモノでも視点や誰が使うかによって、同一性をもつかどうかは変わるので注意

ドメインサービス…エンティティや値オブジェクトとして扱うと不自然なもの
エンティティと値オブジェクトもドメインサービスとして扱うこともできるので節度を持って扱う

優れたサービスは以下の3点

  1. 操作がドメインの概念に関係しており、その概念がエンティティや値オブジェクトの自然な一部ではない。
  2. ドメインモデルの他の要素の観点からインターフェースが定義されている
  3. 操作に状態がない

第3部 より深い洞察へ向かうリファクタリング
リファクタリングはソフトウェア開発者にとっては良く知られている言葉で、機能の変更をしないようにソフトウェアの再設計を行うこと。
マーチン・ファウラーの著書である「リファクタリング」のような、ざっくり言うとコードをきれいにするのも重要だが、ドメインモデルの場合は、いわゆるクックブックのやり方を当てはめるだけでは済まない。
クックブックを当てはめることは良いことではあるが、当てはめてドメインモデルが改悪されてしまうと意味がなくなる。
適切なドメインモデルを念頭に創造力を持つこと、試行錯誤を繰り返すことがまず第一で、それが外れない範囲でパターンを適用する。

・ブレイクスルー
ブレイクスルーは起こすものではなくて結果的に起こるもの 継続的なリファクタリングをすることによって、コードやモデルが整えられる。
改良するたびに、開発者の視界は明確になってくる
視界が明確になったことにより、洞察のブレイクスルーをもたらす可能性が作り出される

ブレイクスルーの舞台を整えるために必要なこと

  1. 知識を噛み砕き、強固なユビキタス言語を育成するのに集中すること
  2. 重要なドメインの概念を探求して、それをモデルで明示すること
  3. 設計をよりしなやかになるように改良すること
  4. モデルを蒸留すること

・概念を掘り出す

ドメインエキスパートの使う言葉に耳を傾ける 以下のモデルにとって有益になりうる概念を示す手掛かりを見逃さないようにする。

  1. 何か複雑なものを簡潔に述べている用語がないだろうか?
  2. ドメインエキスパートに言葉の選び方を(たぶん、角が立たないように)正されていないか?
  3. あなたが特定のフレーズを使った時に、ドメインエキスパートたちの困惑した表情が消えることはないか?

制約やプロセスなどをモデル概念とすると設計が鋭くなる場合がある。

・より深い洞察へ向かうリファクタリング 重点的に取り組むべきこと

  1. ドメインに馴染む
  2. 常に物事に対して違う見方をする
  3. ドメインエキスパートとの会話を途切れさせない

リファクタリングを行う際はコードが整然としていることで安心しない。 ドメインモデルが適切かを常に考える必要がある。

モデルが適切でなかった場合、良いモデルについて探求する必要がある。 探求するには、より長い時間がかかり、より多くの人の参加が必要となる。 ドメインエキスパートや元々の開発者など、より多くの人とミーティングを行う。

ソフトウェアはユーザーのためだけのものではなく開発者のためのもの。

より深い洞察へ向かうリファクタリングは継続的なプロセス。 暗黙的な概念が認識されて明示的になる。 設計の一部はよりしなやかになる。 そして深いモデルへと突き進みまたリファクタリングを繰り返していく。

FullCalendarを使う

最近、弊社製品ODINに機能アップデートがあり、配送予定をカレンダーで管理できるという機能が追加されました。
ここで使われている FullCalendar について、どのようなことができて、どのように使うのか紹介します。

できるもの

カレンダー上に予定を表示します。
日付欄や予定がクリックされたときには、FullCalendar独自のイベントが発生し、その予定に関連した処理をさせることができます。

カレンダーの表示

<div id=”calendar”>にカレンダーを描画するという処理を行っています。

<div id="calendar"></div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@5.5.0/main.min.css">
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.5.0/main.min.js"></script>
<script type="text/javascript">

    document.addEventListener('DOMContentLoaded', function () {

        let calendarEl = document.getElementById('calendar');
        let calendar = new FullCalendar.Calendar(calendarEl, {
            initialView: 'dayGridMonth'
        });

        calendar.render();

    });

</script>

FullCalendarでは予定の表示方法が複数提供されていて、1つ1つの表示方法を「view」としています。
initialViewに「dayDridMonth」というviewを指定することで下のような見た目になります。

予定の表示とコールバック関数の設定

let calendar = new FullCalendar.Calendar(calendarEl, {

        initialView: 'dayGridMonth',

        events: [
           {
                title: '予定1',
                start: '2021-09-15 10:00:00',
                end: '2021-09-15 12:00:00',
           },
           {
               title: '予定2',
               start: '2021-09-17 21:00:00',
               end: '2021-09-18 02:00:00'
           },
           {
               title: '予定3',
               start: '2021-09-20',
               allDay: true
           }
       ],

       dateClick: function (jsEvent) {
           alert('日付がクリックされました。\n' + jsEvent.dateStr);
       },

       eventClick: function (jsEvent) {
           alert(jsEvent.event.title+'がクリックされました。\n' + jsEvent.event.startStr);
       }
});
日付のクリック
予定のクリック

このクリックイベントを使えば、ユーザーに予定を登録・編集させることができます。

また、今回は「events」オプションに直接予定を記入していますが、JSONを返すスクリプトのURLを指定することでも、予定を設定できます。

document.addEventListener('DOMContentLoaded', function () {

   let calendarEl = document.getElementById('calendar')
   let calendar = new FullCalendar.Calendar(calendarEl, {

       initialView: 'dayGridMonth',

       events: '/getEvents.php',

       dateClick: function (jsEvent) {
            alert('日付がクリックされました。\n' + jsEvent.dateStr);
       },

       eventClick: function (jsEvent) {
           alert(jsEvent.event.title+'がクリックされました。\n' + jsEvent.event.startStr);
       }

});