プロパティにオブジェクトを持つオブジェクトをクローンする

つい最近の開発でハマったポイントだったので、こちらに記載しておきます。

とある変数にオブジェクトを格納した後、その変数を他の変数に代入したい。そんなときがあると思います。
しかしただ代入するだけだと、下記のような挙動をし、思っていたのとは違う結果を生みます。

<?php
class classA {
    public $name = 'classA';
    public $info = '1つ目のクラス';
}

$a = new classA();
$b = $a;
$b->info .= ' 変更をしました';

echo $a->name . ' ' . $a->info . PHP_EOL; // classA 1つ目のクラス 変更をしました
echo $b->name . ' ' . $b->info . PHP_EOL; // classA 1つ目のクラス 変更をしました

(かなり細かい話になるので、ここは飛ばしても良いかもしれません。)
この現象の理由は、$aに入っているのは「オブジェクト」ではなく、「オブジェクトが入っているメモリへのアドレス」だからです。
$bにメモリへのアドレスが代入され、$b->infoの変更は、アドレス先にあるオブジェクトを変更するという意味になります。
$aと$bが指しているオブジェクトは同じものになるので、$bの変更が$aにも反映されているのです。

これを防ぐには、下記の「clone」を用います。

<?php
class classA {
    public $name = 'classA';
    public $info = '1つ目のクラス';
}

$a = new classA();
$b = clone $a; // ココがポイント!!!
$b->info .= ' 変更をしました';

echo $a->name . ' ' . $a->info . PHP_EOL; // classA 1つ目のクラス
echo $b->name . ' ' . $b->info . PHP_EOL; // classA 1つ目のクラス 変更をしました

さて、ここから今回の本題になります。
下記のコードは、先ほどのクラスにオブジェクトのプロパティを追加したものです。

<?php
class ParentClass {
    public $name = 'ParentClass';
    public $info = '親クラス';
    public ChildClass $child;

    public function __construct()
    {
        $this->child = new ChildClass();
    }
}

class ChildClass {
    public $name = 'ChildClass';
    public $info = '子クラス';
}

$a = new ParentClass();
$b = clone $a;
$b->info .= ' 変更をしました';
$b->child->info .= ' 変更をしました';

echo $a->name . ' ' . $a->info . ' ' . $a->child->name . ' ' . $a->child->info . PHP_EOL;
// ParentClass 親クラス ChildClass 子クラス 変更をしました
echo $b->name . ' ' . $b->info . ' ' . $b->child->name . ' ' . $b->child->info . PHP_EOL;
// ParentClass 親クラス 変更をしました ChildClass 子クラス 変更をしました

clone後、$bのParentClassとChildClassに変更を加えています。
すると、なんとParentClassの変更は$aに反映していないのに、ChildClassの変更は$bに反映されています。

ちなみに、var_dumpの結果は以下のようになります。(上:$a、下:$b)
ParentClassは$aと$bで違っていますが、中身のChildClassは同じになっています。

object(~~Class)の後の、[]に注目!

ChildClassもcloneしたい場合、下記のようにします。

<?php
class ParentClass {
    public $name = 'ParentClass';
    public $info = '親クラス';
    public ChildClass $child;

    public function __construct()
    {
        $this->child = new ChildClass();
    }

    // ココがポイント!!!
    public function __clone()
    {
        $this->child = clone $this->child;
    }
}

class ChildClass {
    public $name = 'ChildClass';
    public $info = '子クラス';
}

$a = new ParentClass();
$b = clone $a;
$b->info .= ' 変更をしました';
$b->child->info .= ' 変更をしました';

echo $a->name . ' ' . $a->info . ' ' . $a->child->name . ' ' . $a->child->info . PHP_EOL;
// ParentClass 親クラス ChildClass 子クラス
echo $b->name . ' ' . $b->info . ' ' . $b->child->name . ' ' . $b->child->info . PHP_EOL;
// ParentClass 親クラス 変更をしました ChildClass 子クラス 変更をしました

ParentClassに__clone()というメソッドを追加します。これにより、ParentClassのインスタンスがcloneされるときに、$childもクローンしてねという指示を出すことができます。

var_dumpの結果も、以下の通りです。

「なぜ,あなたはJavaでオブジェクト指向開発ができないのか」を読みました。


オブジェクト指向をより理解するために「なぜ,あなたはJavaでオブジェクト指向開発ができないのか」という本を読みました。
メモ程度の内容ですが、まとめたので共有します~

〇感想
なぜオブジェクト指向で書くかという部分への理解が深まりました。オブジェクト指向でコードを書く目的を理解した上でのプログラミングがよりできると思います。
オブジェクト指向が何かという前に、どういう考えのもとでオブジェクト指向プログラミングをするかというのは重要ですね。
(じゃんけんやカードゲームに例えたらどういう役割なのかとか、非オブジェクトとの比べたときのメリットとか)

<1章>
プログラミングの手順
①コンピュータに行わせたいことを理解する
②理解したことを説明できるレベルまで整理する
③コンピュータに分かる言葉に翻訳する

<2章>
オブジェクト指向って本当に必要なのかについて
プログラミングにおいて最も難しいのは、一度作ったソフトウェアに対して変更や機能追加をしていくこと。
ex) 名前を付与する、数を増やす、仕様を追加する

非オブジェクト指向だと拡張が大変になる!
→拡張性をよくする、変更を素早く、簡単にする

拡張性については、非オブジェクト指向で開発したときに不足している点だと感じました。また、ただオブジェクト指向で開発するのではなく、拡張性を意識した上で開発するとまた開発内容が変わってくるのかなと思います。

<3章>
オブジェクト指向=人間の理解をできるだけ分かりやすくコンピュータに伝える方法
役割分担を明確にするということがオブジェクト指向による考え方の第一歩
役割=クラス担当者=インスタンス(オブジェクト)

属性とは、インスタンス固有の性質(人であれば、それぞれの名前やジャンケンに勝った回数など)
各クラスでそれぞれの対象の属性(性質)を管理させる

カプセル化
→処理が保護される
→外部から見て、どのような処理内容かを気にする必要がなくなる

アクセス修飾子・・・どこからアクセス可能であるかを決める(private, publicなど)

コンストラクタの特徴
インスタンスが生成されたときに自動で呼び出される。
メソッド名がクラス名と同じ
戻り値を返せない

JavaDoc→Javaでドキュメントを自動生成できる

オブジェクト・・・互いにメッセージを送り合うことによってシステムを構成
クラス・・・オブジェクトの役割(属性(フィールド)と操作(メソッド))を定義
属性・・・オブジェクトが固有に持つ性質(例、名前)
操作・・・メッセージをきっかけとしたオブジェクトの振る舞い(メソッド)
インスタンス・・・クラスという役割に対する実態(「プレイヤー」という役割(クラス)に対する「山田さん」という登場人物(インスタンス))

各単語の意味などは分かっているつもりでしたが、誰かに説明しようとするとなかなか難しいものだと思います。積極的に業務中に使っていきたいと思います。

<4章>
クラスの継承
・多くの共通部分と少しの異なる部分がある
・外から見れば同じ扱いだが、中から見れば別の扱いをする

メソッドの上書き・・・オーバーライド

継承したクラス・・・サブクラス
継承元のクラス・・・スーパークラス

<5章>
インタフェース・・・仲立ち、オブジェクトへの操作方法とそれに対応する振る舞いの組み合わせを規定

インタフェースを実装する(implement)
自動車であれば、インタフェースは「右側のペダルを踏むとアクセル、左側を踏むとブレーキ」のような操作と振る舞いになる。

大規模になる場合は、インタフェースで実装することで変更を加える手間が少なく済む。

インタフェースも使いどころを考えながら利用していければと思います。これも拡張性など考慮すると使ったほうがいい場面がある気がしますね。あとは、実際に拡張することを考えると初めにどのようにインタフェースを定義するかも重要に思います。

<6章>
モデリング手順
1仕様を決める
2クラスを抽出
3オブジェクト間のメッセージを考える
4各クラスの操作を洗い出す
5各クラスの属性を洗い出す

モデリングとは、「物事を本質をできるだけシンプルに表現すること」。
モデリングに正解はない。
行きつ戻りつしながらよりよいモデルを練っていくしかない。

ユースケース・・・ユーザがどのように利用するかというシナリオを文章として記述
名詞抽出法・・・概要説明などを書いてみて、その中から名詞を抽出して、その名詞からクラスを選定。
シーケンス図・・・オブジェクト同士による操作の呼び出し関係を図にしたもの
UML・・・統合モデリング言語(Unified Modeling Language)

『モデリングに正解はない』という部分が大事かと思いました。どうクラスの操作や属性を決めるかは悩むポイントですが、やっていく中で改善していければと思います。

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

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

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

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

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

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

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

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