dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

第24回 Tokyo Atlassian ユーザーグループ に行ってきたメモ #augj

行ってきました。入り口でなぜかノベルティの栓抜きいただきました。

f:id:dackdive:20171011155914j:plain:w320

話聞きながら適当にメモを取ってたので、少し整理しつつ公開します。

なお、今後イベントの告知や参加申し込みは connpass でなくこちらのサイト経由になるそうです。 Atlassian User Groups Tokyo | Where Atlassian users meet and share best practices


アトラシアン ユーザコミュニティについて by Naho Inuyama @ Atlassian

中の人から Atlassian User Programs というユーザー向けプログラムについて紹介。


アトラシアン サミット 2017 USの最新情報レポートと参加レポート by Miyako Sugimoto @ GxP

  • Atlassian Summit
    • 1年で一番大きなイベント
    • 10 回目
    • 3,600 名 42 カ国
  • 基調講演の内容を共有
  • Trello
    • デスクトップアプリをリリース
    • 全 Atlassian 製品と連携(Bitbucket 内に Trello など)
  • Stride: 新しいコミュニケーションツール
    • Hipchat の後継とは言わず。ただ Hipchat はオンプレのみに
    • チャットしながら決定事項とTODOを書いていける
    • フォーカスモード(通知を一時的にオフ)
  • Jira
    • 優先度が Jira プロジェクトごとに設定可能に
    • デザイン変更(ボード上でカラムが作れる)
  • Portfolio for Jira
    • 依存関係のレポート機能に対応
  • (Jira Service Desk とか Data Center とかの話)
  • Confluence
    • Cloud, モバイルのデザインの変更
  • Bitbucket - Jira 連携強化
    • Bitbucket から Jira 課題を表示・編集可能に
  • [new] Identity Manager
    • SAML, 2段階認証
  • Atlassian Teamwork Platform
    • (独立したプロダクトではなく、製品間の統一を図る機能と思われ)
    • 3 つの柱: 人・エレメント・ホーム
  • 人: 製品間でプロファイルを共通に
  • エレメント: チームが一緒に仕事を完遂するときに共通の協働パターンがあることを発見。そこから機能のアイディアに
    • 重要な仕事を取り込む -> ToDo を Cloud 全製品へ適用中
    • 考えや性格を表現 -> リアクションにいいね! だけでなく絵文字が使えるように
    • 仕事と人の紐付け -> ワンクリックで Jira 課題を作成
  • ホーム: 複数製品の情報がホーム上に表示される
  • 2 日めの基調講演は Ask Atlassian 製品情報はほとんどなし
  • ShipIt (ハッカソン)
  • 公式のイベントレポート:Atlassian Summit U.S. 2017 – Product Keynote レポート | Atlassian Blogs
感想

サミットで発表された内容が非常に簡潔にまとまっていてわかりやすかったです。
Atlassian Summit 知らなかったんですが、イベントの様子など聞く限り Dreamforce ぽいなあなどと。

f:id:dackdive:20171011160036j:plain

また Cloud 版・Server 版だけでなく、Data Center 版というのもあるというのは知りませんでした。
懇親会でも詳しくお聞きしましたが、負荷分散とかもやってくれる Server 版のもうちょっとすごいやつ、という理解。利用規模も数千ライセンスとかになってくると Server 版ではつらいらしい。


ニフティで実践してる、アトラシアン 製品のユーザー管理・運用手法 by Takayuki Ishikawa @ Nifty

  • Atlassian Crowd とは
    • SSO とユーザーID管理
    • 各製品のユーザー管理機能を統合したようなイメージ
  • 経緯
    • 昔は Trac を利用、@nifty アカウントでユーザー認証
    • -> LDAP 認証を採用、Crowd にユーザー管理を集約
感想

この後、LDAP と連携して社内のさまざまな要件を満たすためにどういった設定を行っていったか、について説明いただいたのですが
LDAP を使ったことがなく、難しくてわからず。。。

ただ、組織内外において適切な権限設定というのはやっぱりこの手のツールを使う上で非常に重要ですね。 Atlassian 製品は権限まわりの設定項目が多いので自分も全然わかっていないところ。


Deep Automation JIRA by Narichika Kajihara @ eureka

  • Non - Programming で JiRA 操作を自動化したい管理者向けの内容
  • Automation for Jira の説明

(この後 Kajihara さんからコメントいただいて、やはり料金体系が変わって有料になっちゃったみたい)

  • eureka の環境
    • Jira Software Server 7.3.2 + Jira Service Desk
    • 250 ユーザ
  • Automation for Jira: 特定の条件をトリガとしてさまざまな処理を自動化
    • When: トリガー (Issue が作成されたとき)
    • If: 条件
    • Then: アクション

  • ユーザーの条件(プロジェクト外のユーザーだったら、とか)や添付の有無など、Condition は豊富
  • アクション、Send Slack Message などもあって便利
  • 自動化アイディア
    • 承認依頼する時に、承認者にメンションを自動化
    • サブタスクの自動作成
    • 顧客からのコメントがあったらステータスを変更する。アサインを変更する


感想

今、社内では Jira を開発チームだけで使っているので、どうしても使いたいというニーズはないかなと思ったけど、かなり便利なアドオン。無料だったら絶対使ってたなー笑

メンションしてもみんなあんまりメールは見ないので、Slack への通知がサポートされてるのはかなり魅力的。


おわりに

社内では開発チームだけで Jira, Bitbucket, Confluence を使っているので、Atlassian 製品は「開発者向けツール」というイメージが強かったのですが
Jira Service Desk など、サポートチームと連携したりユーザーからの問い合わせ窓口を作ったり...といったところまでカバーしているんだというのは初めて知りました。

冒頭で、「Atlassian 製品はインストールすれば何の説明もなく使えるようなツールではなく、各種設定を理解して自社のフローに合わせてカスタマイズしたり、逆にツールに合わせて自社のやり方を改めるみたいなことも必要」といった旨のことをおっしゃってて(誤解かも)、それは自分もほんとその通りだと思います。
そういう意味だと Jira 使ってるとこも活用方法は会社によってまちまちだったり、うまくいかなくて困ってるところもあると思うので
今後はそういった事例が LT とかでカジュアルに聞けるといいなあ、と思いました。
ウチも色々苦しんでるので助けてほしい。

あと Cloud 版/Server 版どっち使うのが一般的なんだろうというのが疑問だったんですが、懇親会でウチは全部 Cloud 版です、という話をしたら「(あんな重いのに)よく使えてますね〜」みたいなコメントをいただいてお察しという感じ。

[Salesforce]テキスト、テキストエリア、ロングテキストエリア、リッチテキストエリア項目

ちょっとしたメモ。
カスタムオブジェクトのこれらの項目の違いについて。

特にテキストエリア項目とロングテキストエリア項目の違い、あんまりよくわかってなかったので整理しておく。

f:id:dackdive:20171009002620p:plain


最初にまとめ

項目 最大文字数 UI 改行入力 項目履歴管理
テキスト 255 テキストボックス 不可
テキストエリア 255 テキストエリア
リッチテキストエリア 131,072(*1) リッチテキストエリア
(表示行数指定可(*2))
不可
ロングテキストエリア 131,072(*1) テキストエリア
(表示行数指定可(*2))
不可
  • (*1) オブジェクトごとに、ロングテキストエリア項目とリッチテキストエリア項目で使用できる文字数は合計 160 万文字
  • (*2) Classic のみ反映


違い1:UI

f:id:dackdive:20171010013917p:plain

テキストエリアとロングテキストエリアに UI 上の違いはなく、またテキスト項目と異なり 3 つとも改行入力が可能。

なおリッチテキストエリアおよびロングテキストエリアについては、項目作成時に表示行数を指定することができるがこれは LEX には反映されないらしい。
Ideas がある。
# Visible Lines in Lightning Experience - Ideas - Salesforce Success Community


違い2:最大文字数

テキスト・テキストエリアは 255 文字。リッチテキストエリア・ロングテキストエリアは 1 項目あたり 131,072 文字が最大(最小は 256 文字)。
ただしリッチテキストエリア・ロングテキストエリアについては、1 項目あたりの最大文字数のほかに、オブジェクト全体での制限がある。

参考:カスタム項目の制限 | Salesforce Developer の制限クイックリファレンス | Salesforce Developers

各オブジェクトは、ロングテキストエリア項目とリッチテキストエリア項目で 160 万文字を使用できます。

これは、項目を定義するときにも表示される。

f:id:dackdive:20171010021206p:plain

画面上は 160 万文字ではなく 1,638,400 文字となっているので、文字数でなくデータサイズで上限が決まっている可能性あり。
(1,638,400 / 32,768 = 50 と割り切れるので)

また、リッチテキストエリアには太字やリンクなどを入力できるが、データとしては HTML になっているので実際に表示されている文字数と一致しなくなる。

f:id:dackdive:20171010022815p:plain


違い3:項目履歴管理の可否

地味だけど重要な違いとして。
リッチテキストエリア・ロングテキストエリアは最小文字数が 256 文字からだが、255 文字を超える場合は項目履歴管理で元の値と新しい値の追跡ができなくなる。

参考:項目履歴管理 | Salesforce セキュリティガイド | Salesforce Developers

255 文字を超える項目に対する変更は、編集済みとして追跡され、元の値と新しい値は記録されません。

※変更されたことは追跡可能


リファレンス

thread-loaderとcache-loaderでwebpackのビルドを高速化する

はじめに

こちらのスライドを見て。
Webpackのビルド時間を1/3にした話 #gotandajs // Speaker Deck

紹介されている thread-loader も cache-loader も知らなかった!ので、使い方やどういった効果があるのか自分でも調べてみます。

なお、スライド中にもありますが、たしかに webpack 公式ドキュメントの Build Performance でもこれらの loader について言及されていました。


thread-loader

Runs the following loaders in a worker pool.

時間のかかる(expensive)loader の処理を worker pool で実行し、それにより複数スレッドでの並列ビルドを可能にする?ようです。
(worker pool という用語はどこ由来なのかわからなかった)


使い方

thread-loader を他の loaders の一番上に指定する。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve("src"),
        use: [
          "thread-loader",
          "expensive-loader"
        ]
      }
    ]
  }
}


オプション

色々あるみたいなので #Examples の with options の項を参照。


注意

Each worker is a separate node.js process, which has an overhead of ~600ms. There is also an overhead of inter-process communication.

Use this loader only for expensive operations!

worker は独立した Node のプロセスなので、起動のオーバーヘッドはそれなりに大きいらしい。
時間のかかる処理にだけ使うように、とのこと。


その他

prewarming

worker の起動による遅延を回避するために、事前に worker pool を起動(warmup)しておくことができる。

const threadLoader = require('thread-loader');

threadLoader.warmup({
  // pool options, like passed to loader options
  // must match loader options to boot the correct pool
}, [
  // modules to load
  // can be any module, i. e.
  'babel-loader',
  'babel-preset-es2015',
  'sass-loader',
]);


で、結局どういった loader に適用すればいいの?

具体例は挙げられてなかった。
スライドや、リポジトリexample/webpack.config.js を参考にするといいと思う。

example では babel-loader を使った js のトランスパイルと sass-loader & ExtractTextPlugin による SCSS の処理に適用していた。


cache-loader

後続の loaders の結果をディスクにキャッシュする。


使い方

thread-loader と同じく、他の loaders の先頭に指定する。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        use: [
          'cache-loader',
          ...loaders
        ],
        include: path.resolve('src')
      }
    ]
  }
}


オプション

  • cacheDirectory
    • キャッシュファイルの保存先
    • デフォルトは path.resolve('.cache-loader')
  • cacheIdentifier
    • ??? "Provide an invalidation identifier which is used to generate the hashes. You can use it for extra dependencies of loaders."
    • デフォルトは cache-loader:{version} {process.env.NODE_ENV}{version} は cache-loader のバージョン?)


注意

キャッシュファイルの読み書きというオーバーヘッドが発生するので、こちらも適用は expensive loaders だけに留めるように、とのこと。

また https://webpack.js.org/guides/build-performance/#persistent-cache によると

Clear cache directory on "postinstall" in package.json.

とある。


その他

キャッシュの更新タイミングは?

明記されていなくて困った。
スライドでも

ファイルのmtimeでキャッシュしとく

とあるが、たしかに実装を見ると mtime で比較しているような箇所がなんとなくある
mtime は更新日時(Modified Time。参考


所感

どちらも複雑な設定は必要なく、追加するだけでビルドの高速化が期待できるというところは良いが
ドキュメントを読んだだけでは細かい挙動まで理解しきれていないので、いざ使ってみたらかえって重くなった、あるいはキャッシュが効きすぎて更新が反映されない、とかハマる可能性はありそう。

webpack-dev-server を使った Development Build 時と Production Build とで loader の有効無効切り替えは必要なのか?と思ったが
どちらも webpack のドキュメントでは #General という章に書かれており、

The following best practices should help whether or not you are in development or building for production.

なので、その心配はなさそうと判断。

Googleスプレッドシート:フィルタで表示されたセルだけの合計値を計算する

毎回忘れるのでメモ。 SUM でなく SUBTOTAL 関数を使う。

SUBTOTAL - ドキュメント エディタ ヘルプ

適用できる関数は色々あるが、 SUM の場合 9 を指定すれば OK。

=SUBTOTAL(9, A2:A50)

A2:A50 はフィルタしてる領域の全体行。

ExpressとPassport.jsでOAuth2 (4)有効期限切れのアクセストークンをリフレッシュする

Passport.js を使ってみる回その4です。
以前の記事はこちら:


前回時点での課題

前回までで、認証したユーザー情報を MongoDB に保存することができました。
これで一件落着かと思われたのですが、よくよく考えるとこの後認証先のサービス(ここでは Google)の API を利用するとなると
アクセストークンが必要になります。これも保存しておかないといけませんね。

また、アクセストークンには有効期限が定められているものもあります。
その場合は「アクセストークンが期限切れになったら、リフレッシュトークンを使って新しいアクセストークンを取得し、再度 DB に保存する」という処理も入れなければいけません。

調べてみたところ Passport.js ではそのためのライブラリとして passport-oauth2-refresh というものがあるようです。

(あんまスターついてなくて心配。。。)

というわけで今回はまず、アクセストークンとリフレッシュトークンも DB に保存するようにした後、
passport-oauth2-refresh を使ったアクセストークンの更新処理も実装してみます。


ソースコード

PR は https://github.com/zaki-yama/passport-express-oauth2/pull/2


API の実装

本旨とは関係ありませんが、先に今回用意した API のコードを載せておきます。
内部的に Google Tasks API を叩いて Tasklist を取得します。

// routes/api.js
import express from 'express';
import fetch from 'node-fetch';

const router = express.Router();

router.get('/tasklists', (req, res) => {
  console.log('/tasklists', req.user);
  fetch(`https://www.googleapis.com/tasks/v1/users/@me/lists?access_token=${req.user.accessToken}`)
    .then((response) => {
      return response.json();
    })
    .then((data) => {
      console.log(data);
      return res.json(data.items);
    });
});

export default router;

アクセストークンが切れなければこのままでも問題ありませんが、時間が経つと Google Tasks API のレスポンスが 401 になってしまいます。


アクセストークン、リフレッシュトークンを DB に保存する

これは簡単です。
認証時に accessToken, refreshToken は取得できていたので、ユーザーモデルにこれらを追加して一緒に保存してしまいます。

 // models/user.js
 const userSchema = mongoose.Schema({
   _id: String,
   displayName: String,
+  accessToken: String,
+  refreshToken: String,
   image: String,
 });
 // routes/auth.js
-  User.findByIdAndUpdate(profile.id, extractProfile(profile), {
+  const user = {
+    accessToken,
+    refreshToken,
+    ...extractProfile(profile),
+  };
+
+  User.findByIdAndUpdate(profile.id, user, {


passport-oauth2-refresh のインストールと設定

$ yarn add passport-oauth2-refresh

でインストールします。

 // routes/auth.js

 // [START setup]
 import passport from 'passport';
 import passportGoogleOauth2 from 'passport-google-oauth20';
+import refresh from 'passport-oauth2-refresh';

 import User from '../models/user';

 ...
 // along with the user's profile. The function must invoke `cb` with a user
 // object, which will be set at `req.user` in route handlers after
 // authentication.
-passport.use(new GoogleStrategy({
+const strategy = new GoogleStrategy({
   clientID: process.env.CLIENT_ID,
   clientSecret: process.env.CLIENT_SECRET,
   // FIXME: Enable to switch local & production environment.
   ...
     console.log(err, user);
     return done(err, user);
   });
-}));
+});
+passport.use(strategy);
+refresh.use(strategy);

ここもそんなにやることはないですね。元々 passport.use() にストラテジーを渡していたのと同じことを refresh.use() に対してもやります。


アクセストークンが有効期限切れだったら更新する、という処理を実装

最初に紹介した /routes/api.js にアクセストークンが有効期限切れだったときの処理を入れると、以下のようになります。
これは、 passport-oauth2-refresh の Issue に書かれていたコードを参考にしたものです。
Mongoose でのモデルの save 方法などは↑のコードとは違うようなので、そこは直してます。

 import express from 'express';
 import fetch from 'node-fetch';
+import refresh from 'passport-oauth2-refresh';
 
 const router = express.Router();
 
 router.get('/tasklists', (req, res) => {
-  console.log('/tasklists', req.user);
-  fetch(`https://www.googleapis.com/tasks/v1/users/@me/lists?access_token=${req.user.accessToken}`)
-    .then((response) => {
-      return response.json();
-    })
-    .then((data) => {
-      console.log(data);
-      return res.json(data.items);
-    });
+  const user = req.user;
+  const makeRequest = () => {
+    console.log('/tasklists', user.accessToken);
+    fetch(`https://www.googleapis.com/tasks/v1/users/@me/lists?access_token=${user.accessToken}`)
+      .then((response) => {
+        if (response.status === 401) {
+          console.log('response status 401. Retry');
+          // Access token expired (or is invalid).
+          // Try to fetch a new one.
+          refresh.requestNewAccessToken('google', user.refreshToken, (err, accessToken) => {
+            // TODO: Error handling
+            console.log('new access_token', accessToken);
+
+            // Save the new accessToken
+            user.accessToken = accessToken;
+            user.save().then(() => {
+              makeRequest();
+            });
+          });
+        }
+
+        console.log(response.status);
+        return response.json();
+      })
+      .then((data) => {
+        console.log(data);
+        return res.json(data.items);
+      });
+  };
+
+  return makeRequest();
 });
 
 export default router;

fetch を使った場合、ネットワークエラーでもない限りレスポンスは then() に渡されるので、 response.status の値で 401 が返ってきていないかチェックします。
401 で返ってきた場合はアクセストークン有効期限切れの可能性があるので、

refresh.requestNewAccessToken()

で新しいアクセストークンを取得します。
レスポンスの第二引数には新しいアクセストークンが渡されてくるので、あとはこのアクセストークンでユーザーモデルの内容を上書きして保存( user.save() => { ...)すればいいですね。

ただしリトライ数には上限がないので、Issue のサンプルコードと同じように

  • const retry = 2 などを用意する
  • makeRequest() 時にデクリメントする
  • retry <= 0 の場合は 401 画面を出す

とした方がいいと思います。

また、今回は簡単なサンプルコードのため API が一つしかなく、その中にアクセストークン更新処理&リトライを書いてますが、
これはおそらくすべての API で共通な処理となると思うので、独立した関数にする?など工夫が必要そうです。


TODO

エラーハンドリングを諸々省略してるので、もうちょっと安全にするためにはそういった処理も必要そうです。
なんか動作検証してたら UnhandledPromiseException がちょいちょい出てた気がする(けどリロードしたら出なくなる)んですが、 user.save() に失敗しているとかかな...

[Salesforce]承認プロセスの「割り当て先として使用するユーザ項目」について

メモです。
承認プロセスの設定で、「割り当て先として使用するユーザ項目」というのが出てくるけどどういう挙動になるのかわからず。
合わせて、その下にある「XXX 所有者の承認者項目を使用」というチェックボックスについても。

f:id:dackdive:20170926191854p:plain

公式ドキュメントはこちら。
承認プロセスで自動化された承認者の選択


割り当て先として使用するユーザ項目

ざっくりとした説明

ユーザオブジェクトの任意の項目(※)を設定しておくと、続く承認ステップの設定で「ユーザのこの項目に設定した人を承認者にする」ということができるようになる。
※ただし階層関係の項目のみ。カスタム項目を作らない限り該当するのは「マネージャ」項目だけ


もうちょっと具体的に

この項目を設定せずに承認ステップの割り当て先(承認者)の選択画面を開くと、選択肢として選べるのは以下のいずれか。

f:id:dackdive:20170926192536p:plain

  • 申請者が承認者を手動で選択する
    • 文字通り、申請する側が申請時に明示的に承認者を選ぶ
  • 自動的にキューに割り当てる
    • 別途作成しておいたキューを割り当てる
  • 自動的に承認者に割り当てる
    • 特定のユーザを明示的に指定したり、申請するオブジェクトに作成されている参照項目(ユーザ)を使う

ここで、ユースケースとして「申請者のマネージャを承認者に設定したい」というのは割とありそうだが、上記の選択肢では実現できない。
このときに先ほどの項目を使う。

f:id:dackdive:20170926193953p:plain

設定すると上のキャプチャのように、「選択したユーザ項目を使用して自動的に承認者を割り当てる。」という選択肢が1個増える。
このような設定にしておくと、申請時に申請者のマネージャ(人によって異なる)が自動的に承認者に選ばれるようになる。

デフォルトでは「マネージャ」しか選べないが、ユーザのカスタム項目で階層関係型の項目を追加してあげればそちらを利用することも可能。


XXX 所有者の承認者項目を使用

こちらは、申請するレコードの所有者と申請者が異なる場合に承認者の選ばれ方が変わってくる。
ユーザの階層関係項目を使って承認者を選ぶところは同じだが、ON にすると文字通り、承認者はレコードの「申請者」ではなく「所有者」に設定されている内容を見に行く。

なのでたとえば、部長 > 上司 > 部下 という 3 人のユーザがいて、

  • 「部下」のマネージャ項目は「上司」
  • 「上司」のマネージャ項目は「部長」

という設定をしているとする。

この状態で「部下」が所有者になっているレコードを申請するとき、

  • 「XXX 所有者の承認者項目を使用」が OFF の場合
    • 「部下」が申請した場合:承認者は「上司」
    • 「上司」が申請した場合:承認者は「部長」
  • ON の場合
    • 「部下」が申請しても「上司」が申請しても、承認者は「上司」(所有者である「部下」のマネージャ項目を使うため)

という挙動になる。

IDDD本もくもく読書会メモ#4(第6章 値オブジェクト)

第5章エンティティのメモが書けないまま第6章を終えてしまった。
ので、記憶の新しいうちに値オブジェクトの方のメモを書く。

過去メモ
教材

学習メモ

値オブジェクトとは?
  • ドメイン内の何かを計測したり定量化したり、あるいは説明したりするためのオブジェクト
  • 例:姓・名や電話番号、金額や数量など
  • なぜ値オブジェクトを使うのか
    • int 型のようなプリミティブな型ではなく専用の値オブジェクトにすることで、ドメインの業務をプログラムでわかりやすく示すことができる
    • 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 にはこれに加え、プリミティブな型よりも制約を設けることでその値が備えるべき要件を明確にする、といった記述があった気がする
      • 例:数量に int 型を使うと Java の場合 -2^31 ~ 2^31 - 1 までの整数を許容してしまうが、数量にマイナスはない。また要件によっては最大値もあるかもしれないし、その場合バリデーションロジックを記述することで要件が明確になる
エンティティと値オブジェクトの違い、使い分け
  • 違いは 一意な識別子を必要とするかどうか
    • 値オブジェクトは「一意に識別して変更を管理する必要がないモノ」(CodeZine より)
  • 値オブジェクトにすべきところをエンティティにしてしまいがちだが、値オブジェクトにすることで使いやすくテストしやすくなるので、積極的に利用すべき

可能な限り、エンティティよりは値オブジェクトを使ってモデリングすべきだと聞いたら、驚くかもしれない。ドメインの概念をエンティティとしてモデリングしなければいけないとしても、そのエンティティの設計は、子エンティティのコンテナではなく値のコンテナとして組み立てるよう心がけるべきだ。このアドバイスは、単なる気まぐれによるものではない。値型は何かを計測したり定量化したり説明したりするときに使うもので、作成やテストがしやすいし、使うのも最適化するのも保守するのも楽だ。

  • 値オブジェクトを使用するかどうかの判断基準は以下

あるモデル要素について、その属性しか関心の対象とならないのであれば、その要素を値オブジェクトとして分類すること。値オブジェクトに、自分が伝える属性の意味を表現させ、関係した機能を与えること。値オブジェクトを不変なものとして扱うこと。同一性を与えず、エンティティを維持するために必要となる複雑な設計を避けること。[Evans](97ページ)


値オブジェクトの特徴
  1. 計測/定量化/説明
    • 値オブジェクトは、ドメインの何かを計測・定量化・説明した結果である
  2. 不変性
  3. 概念的な統一体
    • 一つの属性値だけでは意味を持たず、それぞれが組み合わさることで適切な説明をできることを「概念的な統一体」と呼ぶ
    • 例:「50,000,000ドル」は「50,000,000」と「ドル」という2つの属性があるが、これらを切り離すと別の意味になる(か、意味をもたなくなる)
  4. 交換可能性
  5. 等価性(値が等しいかどうかを、他と比較できる)
  6. 副作用のない振る舞い


標準型(タイプコード)
  • 区分や種類を示す
    • 例:「電話番号」というユビキタス言語に対して、それが自宅の番号なのか職場の番号なのかそれ以外の番号なのか、といった種類についての説明
  • 現場で役立つシステム設計の原則 に書いてあった気がする
  • Enum を使うことを推奨してる
  • データベースのレコードを値オブジェクトに変換し、それをタイプコードとする手法もある


6.4 値オブジェクトのテスト

https://github.com/VaughnVernon/IDDD_Samples/blob/master/iddd_agilepm/src/test/java/com/saasovation/agilepm/domain/model/product/backlogitem/BusinessPriorityTest.java#L27-L45

public void testCostPercentageCalculation() throws Exception {
                                                                       
    BusinessPriority businessPriority =
        new BusinessPriority(new BusinessPriorityRatings(2, 4, 1, 1));
                                                                       
    BusinessPriority businessPriorityCopy =
        new BusinessPriority(businessPriority);
                                                                       
    assertEquals(businessPriority, businessPriorityCopy);
                                                                       
    BusinessPriorityTotals totals =
        new BusinessPriorityTotals(53, 49, 53 + 49, 37, 33);
                                                                       
    float cost = businessPriority.costPercentage(totals);
                                                                       
    assertEquals("2.7", this.oneDecimal().format(cost));
                                                                       
    assertEquals(businessPriorityCopy, businessPriority);
}
6.5 実装
  • (著者は通常)値オブジェクトの初期化は2つのコンストラクタを用意する:
    1. 状態の属性を設定するために必要なすべてのパラメータを受け取る、「プライマリ」コンストラクタ。属性の初期化にはprivateなセッターを用いる(自己委譲)
    2. 既存の値をコピーして新しいコンストラクタを作るコピーコンストラクタ。シャローコピーを行う
      • ディープコピーもできなくはないが処理が複雑になる&不変な値を扱うときにはインスタンス間で属性/プロパティを共有したところで問題ないはず


疑問とか

「属性」と「プロパティ」は区別して使ってる?(6.1 値の特徴 > 概念的な構造体)
  • CodeZine だと属性(プロパティ)と表記しているが、本書では区別しているっぽい
  • → 属性は値オブジェクトではない標準的な型による値、値オブジェクトになったものはプロパティと呼んでいるんだと理解した
「自己委譲」は一般的な用語?(6.5 実装)

ググっても出てこなかった。文字とおり自己+委譲なので処理を自分自身のクラスの別のメソッドに任せているから?

値オブジェクトのストラテジ/ポリシーにあたる部分?(6.5 実装)

以下がストラテジパターンにあたるというのがよくわからなかった。

public float costPercentage(BusinessPriorityTotals aTotals) {
    return (float) 100 * this.ratings().cost() / aTotals.totalCost();
}
                                                                                     
public float priority(BusinessPriorityTotals aTotals) {
    float costAndRisk = this.costPercentage(aTotals) + this.riskPercentage(aTotals);
                                                                                     
    return this.valuePercentage(aTotals) / costAndRisk;
}
                                                                                     
public float riskPercentage(BusinessPriorityTotals aTotals) {
    return (float) 100 * this.ratings().risk() / aTotals.totalRisk();
}
                                                                                     
public float totalValue() {
    return this.ratings().benefit() + this.ratings().penalty();
}
                                                                                     
public float valuePercentage(BusinessPriorityTotals aTotals) {
    return (float) 100 * this.totalValue() / aTotals.totalValue();
}
                                                                                     
public BusinessPriorityRatings ratings() {
    return this.ratings;
}

ストラテジパターンの説明 や同じ箇所で言及されてる PofEAA のセパレートインターフェース なんかを見る限り、共通のインターフェースを用意してストラテジごとの実装は各クラスに分けて切替可能にする、とかのようだ。

↑の例だと、今は costPercentage() の算出ロジックが1種類しかないから実装をべたっと書いてしまっているけど、別の算出方法が登場した際にはどっかにインターフェースだけ定義して実装とは分離することになるんだろうか。


おわりに

第6章も骨太で 2 回に分けてなんとか読み終えた。
6.2 節の、コンテキストをまたいだ場合にエンティティではなく値オブジェクトとして定義することで責務を少なめに抑える、というのがまだぴんと来ていない。

また 6.6 節については Hibernate というツール(ORM)の具体的な使い方が中心で最後まで読みきらなかったけど、コードが多かったせいか Hibernate がどういうものであるかは感じ取ることができてよかった。

詳しく知りたくなったらこのあたり読めばいいんだろうか。
Hibernateで理解するO/Rマッピング(1):O/Rマッピングの役割とメリット - @IT


次回

10/4(水) 19:00 予定です。connpass ページ作ったら貼る。


資料