Holmes開発者ブログ

契約マネジメントシステム「ホームズクラウド」の開発者ブログです

スクラムチームに振り返りbot導入してみた

こんにちは。 株式会社Holmesでスクラムマスターしてます、id:k_kubouchi です。

Holmesの開発はアジャイルでのスクラム開発で行っています。 スクラム開発のイベントには「スプリントレトロスペクティブ」というスプリントを振り返るイベントが存在します。 振り返りの種類は色々とあり、チームによってやり方は様々かと思います。 振り返りたい内容をレトロ内で出し切れたらいいのですが、スプリントを振り返る際に「何かあったんだけど思い出せない」みたいなシチュエーションも中にはあるのではないでしょうか。 そこの一助になればと思い、都度振り返りを投稿できる SlackBot を導入することにしました。 それでは、以下の項目に沿って導入手順を解説していきたいと思います。

※前提として、振り返りは KPT と呼ばれる振り返りのフレームワークを想定しています。

※2020年から SlackBot の設定手順が変更されたようなので、そちらをベースに解説していきます。

1. Slack App の設定(SlackBot)

まず、SlackBot を導入したいワークスペースに管理者権限アカウントでサインインします。 URLはこちらになります。 サインイン後、画面右上にある Create New App をクリックしてください。

f:id:k_kubouchi:20201110093623p:plain

設定ダイアログが表示されるので、 App Name に好きな bot 名を、 Development Slack Workspace に導入先の Slack ワークスペースを設定してください。

f:id:k_kubouchi:20201110093807p:plain

次に、権限の設定をする必要があるので、最初の画面の Basic Information クリックで表示されるメニューから、Add features and functionalityPermissions と遷移してください。

f:id:k_kubouchi:20201110093818p:plain

Scopes の真ん中ぐらいにある Add an OAuth Scope をクリックし、以下2つの権限を追加してください。(選択するだけで選んだことになってます)

  • chat:write
  • chat:write.public

f:id:k_kubouchi:20201110093834p:plain

続いて、 Basic Information から App Display Name に行き、 Slack 上で表示する App の内容を設定していきます。

f:id:k_kubouchi:20201110093848p:plain

設定項目はそれぞれ以下の通りです。

  • App name:Slack App の名前
  • Short description:Slack 上での bot の説明
  • App icon & Preview:Slack 上での bot アイコン
  • Background color:Slack 上での bot 背景

最後に Incoming Webhooks から、以降に登場する Google Apps Script 上で使用する URL を取得します。 左メニューの Incoming Webhooks をクリックし、 Webhook URLs for Your Workspace に表示されている URL をコピーしておいてください。

f:id:k_kubouchi:20201110093902p:plain

以上で Slac App の設定は完了です。

2. GASの設定(Google Apps Script)

Slack に投稿した内容を自動的に Googleスプレッドシートに溜めていくような仕組みにしたいです。 そのためには Google Apps Script を使う必要あるので、まずはそちらを準備していきます。

Google Drive+ NewGoogle Sheets を選択します。

f:id:k_kubouchi:20201110093913p:plain

新規スプレッドシートが表示されたら ToolsScript editor を選択します。

f:id:k_kubouchi:20201110093926p:plain

編集画面が表示されるので、適当なプロジェクト名を左上のタイトルをクリックし設定します。 編集エディタ上に今回は以下コードを挿入することとします。

var KEEP = 'KEEP';
var PROBLEM = 'PROBLEM';
var TRY = 'TRY';

function doPost(e) { // SlackからのPOSTリクエスト時に発火する
  switch(e.parameter.text) { // start、end、K: P: T: で場合分け
    case 'start':
      start();
      break;
    case 'end':
      end();
      break;
    default:
      registerKpt(e.parameter.text, e.parameter.user_name);
      break;
  }
}

function start() { // 開始宣言
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]; // 先頭のシートを取得
  if ((sheet.getName().match(/^[\d]{4}\/[\d]{2}\/[\d]{2} [\d]{2}:[\d]{2}$/) !== null) || (sheet.getLastRow() !== 0)) { // 重複した開始宣言は排除
    postSlack('すでに今スプリントの `KPT` は始まってるなっしー!');
    return;    
  }
  var date = new Date(); 
  sheet.setName(Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy/MM/dd hh:mm')); // シート名を現在時刻に変更
  postSlack('今スプリントの `KPT` 開始なっしー!');
}

function end() { // 終了宣言
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheets()[0];
  var lastRowNum = sheet.getLastRow();
  var keepArray = [];
  var problemArray = [];
  var tryArray = [];
  var reviewArray = [];
  if (lastRowNum === 0) { // 0行の場合は終了できない
    postSlack('登録された `KPT` がないなっしー...');
    return;
  }
  var rows = sheet.getRange(1, 1, lastRowNum, 3).getValues();
  rows.forEach(function(row) { // 行ごとにKPT4要素でまとめる
    switch(row[0]) {
      case KEEP:
        keepArray.push(row[1] + ': @' + row[2]);
        break;
      case PROBLEM:
        problemArray.push(row[1] + ': @' + row[2]);
        break;
      case TRY:
        tryArray.push(row[1] + ': @' + row[2]);
        break;
      default:
        break;
    }
  });
  
  var date = new Date();
  var now = Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy/MM/dd hh:mm'); // 終了時点の時刻を取得
  postSlack( // まとめて投稿
    '今スプリントの KPT なっしー!みんなお疲れなっしー!\n' +
    '```\n' + 
    sheet.getName() + ' ~ ' + now + '\n\n' + 
    '# KEEP\n' + keepArray.join('\n') + '\n\n' +
    '# PROBLEM\n' + problemArray.join('\n') + '\n\n' +
    '# TRY\n' + tryArray.join('\n') +
    '\n```'
  );
  ss.insertSheet(0); // 新たな空シートを先頭に追加
}

function registerKpt(text, userName) { // KPT登録処理
  postSlack('登録してるなっしー....');
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
  var categoryCell = sheet.getRange(sheet.getLastRow() + 1, 1); // 挿入する行の1列目を取得
  var contentCell = sheet.getRange(sheet.getLastRow() + 1, 2); // 挿入する行の2列目を取得
  var userCell = sheet.getRange(sheet.getLastRow() + 1, 3); // 挿入する行の3列目を取得
  var message = processMessage(text); // 投稿を要素と内容に分ける
  if (message === null) { // 形式違いは排除
    postSlack('形式が違うなっしー。 `K:`、 `P:`、 `T:` から始めるなっしー!');
    return;
  }
  categoryCell.setValue(message.category); // 書き込み
  contentCell.setValue(message.content);
  userCell.setValue(userName);
  postSlack('スプシに書いといたなっしー!');
}

function processMessage(text) {  // 投稿を要素と内容に分ける
  var match = text.match(/^([K|P|T]):(.*)$/m);
  if (match === null) {
    return null;
  }
  
  var getText = text;
  switch(match[1]) {
    case 'K':
      getText = getText.replace('K:', '');
    case 'P':
      getText = getText.replace('P:', '');
    case 'T':
      getText = getText.replace('T:', '');
    default:
  }
  
  return {
    category: convertCategory(match[1]),
    content: getText,
  };
}

function convertCategory(category) { // プレフィックスから4要素を判断する
  switch(category) {
    case 'K':
      return KEEP;
    case 'P':
      return PROBLEM;
    case 'T':
      return TRY;
    default:
      return null;
  }
}

function postSlack(text){ // Slackへの投稿
  // ここに先程コピーしていた Slack Incoming WebHook の URL を指定してください
  var url = "https://hooks.slack.com/services/~~~~~~";
  var options = {
    "method" : "POST",
    "headers": {"Content-type": "application/json"},
    "payload" : '{"text":"' + text + '"}'
  };
  UrlFetchApp.fetch(url, options);
}

コードを設定できたら保存し、メニューの Publish を選択します。 設定ダイアログの中身を入力していきますが、この時以下のことに注意してください。

  • Project version: は毎回保存時は必ず New を選択してください
  • Who has access to the app: ですが、必ず Anyone, even anonymous で設定してください

以上で Google Apps Script の設定は完了です。

3. Slack App と GAS の連携(Slack Outgoing WebHook)

Slack の機能である Slack Outgoing WebHook を設定することにより、 GAS を発火させることができます。 その手順を説明していきます。

まず、 https://SlackのWorkspace名.slack.com/apps にアクセスし、検索フォームから Outgoing WebHooks を検索し、選択してください。

f:id:k_kubouchi:20201110105029p:plain

遷移したら Slack に追加 を選択し、 Outgoing Webhook インテグレーションの追加 をクリックしてください。 そうすると設定画面になるので、追加していきます。 今回は KPT 振り返りの内容を投稿できるようにしたいので、以下のようにそれぞれ設定していきます。

  • チャンネル : 投稿で使用するチャンネルを設定
  • 引き金となる言葉 : 投稿内容に含まれるどの文字を検知して発火させるか指定(今回だと KPT
    • K: : Keep 内容投稿用
    • P: : Problem 内容投稿用
    • T: : Try 内容投稿用
    • start : スプリント単位で振り返り内容をスプレッドシートにまとめたいので、その始まり用のトリガー
    • end : スプリント単位で振り返り内容をスプレッドシートにまとめたいので、その終わり用のトリガー
  • URL : GAS の URL を指定

f:id:k_kubouchi:20201110093958p:plain

以上で、 GAS と Slack の連携設定は完了です。

4. チームでの運用ルール

以下のルールを元に、チーム内で運用しています。

  • スプリントの始まりと終わりにスプレッドシートのシートをスプリント単位で分けたいので、スクラムマスターが start end を投稿
  • KEEP の内容は K: を頭につけて投稿
  • PROBLEM の内容は P: を頭につけて投稿
  • TRY の内容は T: を頭につけて投稿

上記ルールに沿って投稿すると、bot が返信してくれるので、地味に面白かったりします(笑)。 (参考サイトがふなっしーを使って運用されてたので、そこに引きずられる形でボットの画像や口調も合わせて設定してみてます)

f:id:k_kubouchi:20201110094011p:plain

5. まとめ

SlackBot での振り返りを導入したことにより、以下のような感想をチームメンバーからもらうことができました。

- 思いついたタイミングで気軽に投稿できる
- 気づきを忘れなくなった
- スプリント単位で手軽に管理できる

短い期間のスプリントだと、効率良く少しずつ改善を繰り返していく仕組みが大事なので 少しの自動化も開発の一助になるのならと思い、今回導入してみましたがそれなりの価値があったのではないかと思います。

今後もメンバーの手助けになるような仕組みをどんどん導入していき、 円滑にスプリントを回していけるよう精進したいと思います!

ここまで読んでいただきありがとうございました!!

参考記事

Holmesはエンジニア・デザイナーを募集しています 興味がある方はぜひこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

ファシリテーション第1回(学ぶ理由と必要なスキル)

Holmesでスクラムマスターの役割を担っているid:tomoya_misudaです。

ファシリテーション協会のセミナーで参加で学んだことや実践したことを数回に分けて書きたいと思います。 第1回は、ファシリテーションについてや学ぶキッカケなどまとめます。

www.faj.or.jp

学ぶキッカケ

スクラムマスターの役割を担う前は、開発メンバーとしてプログラムを実装しておりました。Holmesのプロダクト初期から関わっており、仕様や実装は誰よりも理解している自負がありました。しかし、メンバーが増えることでチームメンバーを支援する役割が必要になって来ました。その際にスクラムマスターの役割を担うことを組織から提案を頂きました。開発メンバーとして、スクラムマスター になったらと考えることもあり、やってみようと考えました。

スクラムマスターの役割を担っていく中で、「KPTをやっても特定のメンバーのみ意見を出す。」「プランニングとゴールの認識があっていない。」など数々の課題を抱えてました。その際に各イベントでやるべきことが出来ていないと感じてました。もっとメンバーにイベントの意味や方向性などを持ってもらうことを考えました。 会議やイベントの進め方を調べる中でファシリテーションの言葉に辿り着きました。

ファシリテーション

「人々の活動が容易にできるよう支援し、うまくことが運ぶよう舵取りすること。」をセミナーの冒頭で学びました。会議は、意見の対立やそれぞれの個性から言い出し にくいことが多いと思います。そこを引き出す、意見の対立をまとめる、決めたことが課題とあっているか参加者に確認すると私は捉えました。

ファシリテーションのスキル

f:id:tomoya_misuda:20200816131424p:plain
ファシリテーションのスキル

4つのスキルで構成されます。次回からは、各スキルについて実践を交えながら伝えたいと考えます。

まとめ

数回の記事を通して、ファシリテーションとは、実践の大変さ、良かったことを伝えることができたらと考えます。また、同じ悩みをみんなで共有できたらと考えます。ありがとうございました。

Holmesはエンジニア・デザイナーを募集しています 興味がある方はぜひこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

Gaugeを使ってMarkdownで書いたテスト仕様を動かしてみる

こんにちは。Holmesでエンジニアをしている山本です。

社内でATDDの話題が出たことがありました。受け入れテストと言えばCucumber、というイメージがあったのですが、他にないか調べたところ、Gaugeという、Markdownを仕様としてテスト実行できるツールがあったので、試してみました。

実行環境

名前 バージョン
OS Windows 10 Pro 64bit バージョン2004
Gauge 1.1.5
Intellij IDEA Ultimate 2020.2.3
Visual Studio Code 1.51.1
JDK Amazon Corretto-11.0.9.1

Gaugeの概要

テスト仕様(CucumberにおけるFeatureファイル)をMarkdownで記述し、それと紐づけたテスト(CucumberにおけるStepファイル)を実行できる、テストフレームワークです。

概要は、Gaugeによるe2eテスト - Speaker Deckに詳しいです。

※スライド内でGaugeのURLが https://getgauge.io/ となっていますが、現在では https://gauge.org/ です。また、ライセンスがGPL3.0との記載がありますが、2020/4月末に変更され、v1.1.5の時点ではApache License 2.0になっています。

テストとして実行可能な言語は、v1.1.5の時点でC#, Java, JavaScript, Python, Rubyの5種類が選択です。また、Visual Studio Codeでのプロジェクト作成時には、TypeScriptも選択できました。

なぜGaugeを試そうと思ったのか

現状、ユニットテストとインテグレーションテストの区別なく、Spockを用いたテストを記述しています。

ユニットテストでは、依存クラスをモック化したテストを書けるのですが、インテグレーションテストを書こうとすると、データの事前準備など、テスト以外の記述がどうしても増えてしまいます。

前処理/後処理の共通化などを行っていますが、単一のテストクラス内では共通化できているが、複数のテストクラスを見比べると同じような処理が書かれていたり、あるメソッドでは共通処理を使っているが別のメソッドでは個別に初期化処理を行ったりと、標準化が難しい状態です。

こうした状態の改善のため、テストの仕様と実装を切り分け、テスト仕様を自然言語で記述できれば、データ作成などの共通化が、仕様と実装が混在している現在よりも容易に行えるのではないかと思います。

また、弊社ではSeleniumを利用したリグレッションテストをOPSチームの方々が記述してくれています。それらについても、テストの仕様と実装を切り分けていれば、共通のテスト仕様を元に、実装を切り替えることができるのではないかと考えました。

これらはCucumberでも達成可能だと思いますが、Cucumberのテスト仕様に用いるGherkin構文を覚えるよりも、ルールがあるとはいえMarkdownを利用できたほうが学習コストが低くなると思い、今回はGaugeを試してみようと思います。*1

環境構築

公式としては、Visual Studio CodeをEditorとして、開発元が同じブラウザ操作自動化ツールのTaikoを用いたJavaScriptによるテスト実装を推奨しているようです。

弊社では標準開発環境としてIntellij IDEAを使用しているため、今回はIntellij IDEAをIDEとして、Java+SeleniumによるE2Eテスト実装を試してみます。

こちらの記事を参考としました。

Gaugeのインストール

公式のインストールガイドにて、OS、プログラミング言語IDE/Editorを選択すると、それ以降のページの記述が変わる仕組みです。

v1.1.5時点では開発環境としてVisual Studio Codeしか選択できないようです。

ひとまず、Windowsインストーラーをダウンロードして、Gaugeをインストールします。

Intellij IDEAにGaugeプラグインを追加

インストールガイドでのIDEには表示されていませんが、Intellij IDEAにもGaugeプラグインがあるため、インストールします。*2

Intellij IDEAからGaugeプロジェクトの作成

Gaugeプラグインをインストールすると、Intellij IDEAの File > New > Project にて、Gaugeプロジェクトを作成可能となります。

  1. SQL Support にチェックせず Next
  2. Project name, Project location, Project SDK をそれぞれ設定し、Finish
    • SDKとしては、JDKしか選択できませんでした

作成されたプロジェクトは、MavenプロジェクトでもGradleプロジェクトでもない、素のJavaプロジェクトになっていました。また、依存性などにSeleniumが使われておらず、単純なユニットテストの実行のみとなっていました。

いくつか外部ライブラリが読み込まれていますが、どこで設定しているか確認すると、プロジェクトルートに プロジェクト名.iml が作成され、 jarDirectory として file://$USER_HOME$/AppData/Roaming/gauge/plugins/java/0.7.13/libs を読み込んでいました。

このファイルをVCSに追加して使い回す場合、Windowsでしか動かせなさそうなこと、またSeleniumがデフォルトの依存性に含まれていなかったことから、Intellij IDEAでのプロジェクト作成を断念し、Visual Studio CodeからMavenプロジェクトを作成することにしました。

Visual Studio CodeにGaugeプラグインを追加

Visual Studio CodeGaugeプラグインをインストールします。

Visual Studio CodeからGaugeプロジェクトの作成

公式のプロジェクト作成手順を元に、Javaを実装言語とした、Gaugeプロジェクトを作成します。

Javaでもプロジェクトの種類が複数ありますが、今回は java_maven_selenium プロジェクトを選択、作成しました。

作成されたプロジェクトの pom.xml を見ると、Seleniumが依存性に含まれていました。また、先ほどはディレクトリを参照していたライブラリ関連も、 com.thoughtworks.gauge:gauge-java:0.7.13 として依存性が追加されていました。

ただし、デフォルトでは文字コードの指定がされておらず、Windows環境ではファイルをMS932で読み込もうとするため、project 直下に、 以下の記述を追加しておきます。

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

Gaugeの実行確認

JavaでのGaugeの実行にはJDK11以上が必要ということで、環境変数 PATH にJDK11のbinを通し、 mvn test を実行するか、Visual Studio Code上で specs/example.spec を開いて Run Spec をクリックし、実行できることを確認します。

テストがパスし、 reports/html-report/index.html が作成されれば成功です。ファイルを開くと、以下のレポートが表示されます。

f:id:h-yamamoto_holmescloud:20201129162426p:plain
初期状態でのレポート

Intellij IDEAでのプロジェクトインポート

Visual Studio Codeを使って、プロジェクト作成はできました。改めてIntellij IDEAでプロジェクトを開いて、Mavenプロジェクトとしてインポートします。

インポートした後、 specs/example.spec を開くと、Gaugeプラグインをインストールしていれば、Markdownの見出し部分に、通常のテストクラスと同様の実行ボタンが表示されます。

これをクリックし、テストが実行されることを確認しました。

実装

いよいよ実装です。テスト仕様をMarkdownで、テスト実装をJavaで記述し、それぞれをマッピングしていきます。

記述方法の詳細は、公式ドキュメントを参照してください。

今回は、先ほどの記事こちらの記事を元に、GoogleまたはBingを開き、「Holmes開発者ブログ」で検索し、検索結果ページのタイトルを確認してみようと思います。

テスト仕様の記述

specs/ ディレクトリ配下に拡張子 .spec として、Markdownでテスト仕様を記述していきます。

他にも拡張子 .cpt として、再利用可能な仕様を「コンセプト」として記述できるようですが、今回は使用しません。

今回は、以下のようなMarkdownを記述し、 specs/search.spec として保存しました。*3

# 検索エンジンの検索結果ページのタイトルを確認する

Tags: search

   |url                    |inputSelector       |title                           |
   |-----------------------|--------------------|--------------------------------|
   |https://www.google.com/|#tsf input[name="q"]|Holmes開発者ブログ - Google 検索|
   |https://www.bing.com/  |#sb_form_q          |Holmes開発者ブログ - Bing       |

## 検索エンジンを開き、「Holmes開発者ブログ」で検索し、検索結果ページのタイトルを確認する

Tags: successful

* 検索エンジンのURL <url> を開く
* 検索文字列入力欄 <inputSelector> を取得する
* "Holmes開発者ブログ" で検索する
* 検索結果ページのタイトルが <title> であることを確認する

h1 タグが見出し、 h2 タグがシナリオとなります。

見出しとシナリオの間にテーブルを記述すると、データテーブルとして、パラメーター化テストが可能です。

シナリオ配下に順序なしリストを記述すると、それぞれがテストのステップとなります。注意点として、Markdownでは順序なしリストの記述に *|+|- のいずれかを使用できますが、Gaugeでは * のみ有効のようです。

ステップに " で囲んだ部分は静的パラメータ、 <> 囲んだ部分は動的パラメータとなります。動的パラメータには、データテーブルのヘッダ名を記述することで、その値が設定されます。このとき、値の末尾の空白は、自動でトリミングされます。

見出しと最初のシナリオの間にステップを記述すると、コンテキストステップとして、各シナリオの実行前の共通処理を記述できるようですが、今回は使用しません。

Intellij IDEA上でも、Gaugeプラグインをインストールしておくと、実装のないステップはエラーとなっていました。

テスト実装の記述

テスト仕様のステップに対応するテスト実装を記述します。

Javaの場合、テストメソッドに com.thoughtworks.gauge.Step アノテーションを付与し、ステップとマッピングします。

WebDriver インスタンスは、 driver.Driver.webDriver に設定されているので、明示的に生成する必要はありません。デフォルトでは、 ChromeDriver が設定されます。

以下のテストクラスを、 src/test/java/search/SearchSteps.java に記述しました。アサーションライブラリとしては、AssertJがデフォルトで依存性に含まれているため、それを使用しています。

package search;

import com.thoughtworks.gauge.Gauge;
import com.thoughtworks.gauge.Step;
import com.thoughtworks.gauge.datastore.ScenarioDataStore;
import driver.Driver;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import static org.assertj.core.api.Assertions.assertThat;

public class SearchSteps {

    private final WebDriver driver = Driver.webDriver;

    @Step("検索エンジンのURL <url> を開く")
    public void openSearchEngine(String url) {
        driver.get(url);
    }

    @Step("検索文字列入力欄 <inputSelector> を取得する")
    public void getSearchTextInput(String inputSelector) {
        WebElement searchTextInput = driver
                .findElement(By.cssSelector(inputSelector));
        ScenarioDataStore.put("searchTextInput", searchTextInput);
    }

    @Step("<searchText> で検索する")
    public void searchTextInput(String searchText) {
        WebElement searchTextInput = (WebElement) ScenarioDataStore
                .get("searchTextInput");
        searchTextInput.sendKeys(searchText);
        searchTextInput.sendKeys(Keys.chord(Keys.ENTER));
    }

    @Step("検索結果ページのタイトルが <title> であることを確認する")
    public void checkSearchResultPageTitle(String title) {
        String pageTitle = driver.getTitle();
        Gauge.writeMessage("検索結果ページのタイトル: %s", pageTitle);
        assertThat(pageTitle).isEqualTo(title);
    }
}

JUnitのテストクラスのようですが、依存性にJUnitは含まれていないため、テストクラスへの @Test などのアノテーションは不要です。

各テストメソッド間での値の受け渡しには、それぞれライフサイクルの異なる DataStore が利用可能です。今回はシナリオ内での値の受け渡しに、 ScenarioDataStore を利用しています。

また、 Gauge.writeMessage メソッドで、レポートにメッセージを追加することができます。

@Step で記述したテキスト内のパラメータ数と、メソッドの引数の数に不一致があるとエラーとなるなど、テスト実装でもGaugeプラグインをインストールしておけば、Intellij IDEA上でチェックが行われるようになっていました。

実行と結果の確認

mvn test を実行すると、 example.spec に続いて、 search.spec の実行が行われます。実行順は、ファイル名の昇順のようです。

reports/html-report/index.html を開くと、追加したテストの結果を含んだレポートが出力されています。

f:id:h-yamamoto_holmescloud:20201129162120p:plain
テスト追加後のレポート

左フレームのスペック名を選択して、表示を切り替えることができます。タグ付けしておくことで、検索も可能です。

データテーブルを使用した場合、テーブル内容が表示され、行をクリックすることでテスト結果の表示を切り替えることができます。

f:id:h-yamamoto_holmescloud:20201129163306p:plain
Bingの行選択時

Gauge.writeMessage で出力したメッセージも確認できました。

感想

テスト仕様の記述については、単純なMarkdownのため、非常に書きやすいです。Cucumberなどをしっかりと使ったことがなく、概要だけ調べての感想になりますが、Gherkinに比べると、導入に際してのハードルは低いと思います。

いっぽう、テスト仕様の表現力という意味だと、「given/when/then」、日本語では「前提/もし/ならば」といった記述をキーワードとして行えるGherkinのほうが高いと感じました。今回の記述量程度であれば、IDE/Editorのコード補完やコンパイルエラーの有無で管理できますが、GaugeでもGherkinと同様、テストの準備では「前提」を使用したり、前処理の実装はこのファイルに記述するなどの取り決めをしておくと、テストが増えてもメンテナンスがしやすくなると思います。

実装については、公式でIDE/EditorとしてVisual Studio Codeしか選択できなかったため、やや不安がありましたが、Visual Studio Codeでプロジェクトさえ作ってしまえば、Intellij IDEA + Gaugeプラグインで問題なく行えました。今回はサンプルコードのため、テスト実装内でSeleniumを使っていろいろと操作を行いましたが、保守性を高めるためには、Page Object Patternに基づいたクラスを別途記述し、Gaugeはそのオーケストレーションに専念したほうがいいと思います。

また、テスト結果のレポートについては、エラーが起こった場合は自動でスクリーンショットが添付されるなど、非常に見やすいと思います。*4

※Cucumberでのレポート出力については、ちょっと調べた限り公式ではCucumber Reports Serviceが推奨されていたり、Allure Frameworkなどサードパーティ製ライブラリがあるものの、デフォルトでどんなレポートが出力されるかが分からず、比較はできませんでした。

Gauge自体はSeleniumに依存するものではないため、ブラウザ操作を伴うテスト以外でも、受け入れテストやリグレッションテストなど、繰り返し行うテストの自動化およびレポート出力に使えそうだと感じました。

最後に

Holmesではエンジニア・デザイナーを募集しております。ご興味がある方はこちらからご連絡ください。

lab.holmescloud.com

lab.holmescloud.com

*1:なお、私自身Cucumberをきちんと勉強したがないので、あくまでイメージになります

*2:以前のバージョンでは、IDE/EditorとしてIntellij IDEAを選択できたようです

*3:当初は検索ボタンのクリックまでCSSセレクターでやろうとしたのですが、Googleの検索ボタンにはIDが振られておらず、冗長なので断念しました

*4:ただし、Seleniumを使わないテストの場合、スクリーンショットを取っても意味がないため、調整する必要がありそうです

プロダクトにドメイン駆動設計を適用するためにはじめたこと

こんにちは。最近Slackのカスタム絵文字作りにハマっている友野です。Holmesでサーバーサイドエンジニアをしています。

Holmesが提供するホームズクラウドは、今年8月にサービスローンチ3周年を迎えました!

これまでの支持に感謝し、これからも長く使ってもらえるようにプロダクト改善に取り組んでいます。そのひとつとして、ドメイン駆動設計(以下、DDDと表記します)適用に関する取り組みについてご紹介します。似たような状況や同じ課題を持つ誰かの一助になれば幸いです。

背景と現状

ホームズクラウドPMF(Product Market Fit:プロダクトマーケットフィット)を経て、サービスシェア拡大のためにトレードオフスライダーをスピードに全振りしていました。Spring Bootを利用した三層+MVCアーキテクチャを採用し、ローンチ以来、機能追加・改善を繰り返し行ってきました。

日本ではCLM(Contract Lifecycle Management:契約ライフサイクルマネジメント)領域はまだ深く浸透しておらず、認知度向上のためにはとにかく市場評価する必要があり、スピード最優先にする戦略・アーキテクチャ採用は間違っていなかったと思います。

一方で、コアドメインである契約関連機能はその仕様の複雑さも相まって、コードの複雑性も増加の一途を辿り、メンテナンスコストが上がってきました。サービスローンチから3年をふりかえり、市場変化に合わせて「変更に強いプロダクト」を作ることがCTOはじめメンバー全員の共通認識になり、DDD採用を決める後押しになりました。

まずはじめたこと

採用を決めたものの、DDDとは何だろうか?というメンバーが多い状態から第一歩を踏み出すために、有識者の力が必要でした。

DDD Community JPを主宰している松岡さんに声をかけ、力を貸してくれるようお願いしたところ、快く引き受けてくれた上にライブモデリングとライブコーディングまでしてくれる運びとなりました(感謝しかありません)。それまでにメンバーは松岡さんのYouTubeで基礎知識をインプットし、モブワークに臨みます。

テキストベースのコミュニケーションをはさみながら、モブワークを2回ほど実施した後のメンバーの感想は以下の通りです。

  • モデルクラスにロジック集約してると動くドキュメント感あってよい
  • 正しい状態しか作れないと処理がシンプルになる!
  • 1クラスだけ見ればいいのは楽だし安心
  • ドメインやモデルが定義されていないソフトウェアからDDDを始めるには? 再設計/モデリング?

不安に思いながらも、多くのメンバーがその効果と威力に期待を膨らませ、モチベーションを高めていきました。

戦略的モデリング

先日の記事で触れたように、社内で勉強会を重ねた上で、仕様の共通理解と深掘りを行うためにドメインモデリングに取り組み始めました。具体的には、弁護士資格を持つ社員をドメインエキスパートとしてスクラムのリファインメントに招き、モデリングを実施しています。ドメインモデリング自体初めてのメンバーが多いため、以下の書籍を参考にモデリングの進め方ガイドを作り、適宜ふりかえりを交えながら進めています。

little-hands.booth.pm

現在リモートワーク中心での勤務体系のため、議論はオンラインホワイトボードmiro上で行い、モデリングをした後にバージョン管理のためにPlantUMLでまとめています。

そして、戦術的な設計

仕様の複雑性への対策として、ドメインモデリングだけでも一定の価値はあると考えています。これに加えて、戦術的な設計領域*1 まで踏み込むことでさらにDDD採用の効果を高め、変更に強い状態を作ることを目指しています。

しかし、既存の資産、かつ稼働しているサービスにおいて、どこから手をつけるか非常に悩ましい課題です。最初はドメインモデリングした成果を活かすために、集約をコードで表現することからスモールスタートすることに決めました。

採用するパターン2つ

集約をコードで表現するために以下の2つのプラクティスを採用します。

  • ドメインモデルを反映したオブジェクトを置くパッケージの作成
  • 既存テーブル構造に依存しないRepository+Adapterパターン

ドメインモデルを反映したオブジェクトを置くパッケージの作成

まず何より、ドメインモデルを反映したドメインオブジェクトを置く場所を決めます。既存のパッケージ構成では、Spring MVCやSpring Data JPAコンポーネントに合わせ、controllerserviceを中心に、entityrequestなどのモデルのパッケージを切っています。serviceパッケージのクラスには、データモデルが渡されて手続き的に処理されるコードがあるため、ドメインオブジェクトを配置するのは適切ではありません。ドメインオブジェクトを置くdomainパッケージを作ることにしました。serviceパッケージのクラスの修正は最小限に留め、ビジネスルールに関する操作をドメインオブジェクトに移譲する状態をゴールとしています。

  • serviceパッケージ内のクラスが担う責務
    • 全体の流れを構成する責務
    • データの取得、永続化(を依頼する)責務
  • domainパッケージ内のクラスが担う責務
    • ビジネスルールに基づく判断/加工/計算の責務

いわゆる三層アーキテクチャ+ドメインオブジェクトのパターンで、これは以下の書籍を参考にしています。

現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法:書籍案内|技術評論社 gihyo.jp

既存テーブル構造に依存しないRepository+Adapterパターン

一方で、すでに稼働しているサービスではDB構造を大きく変更することは難しく、リスクも伴います。ホームズクラウドでは、データ永続化のライブラリとしてSpring Data JPAを採用しており、Repositoryインタフェースはあるものの、直接JPARepositoryを実装している*2のが現状です。つまり、JPAエンティティ(DDDのエンティティではないのでJPAをつけています)を渡さないと永続化できません。これでは既存のDB構造に強く依存してしまい、ドメインオブジェクトの構造自体が既存構造に引きずられかねません。

Repositoryインタフェースにドメインオブジェクトを渡したいので、JPA用のAdapterクラスを実装して、JPAエンティティへの変換と永続化責務を移譲することにしました。これで、既存資産のJPAエンティティへの依存を切ることが出来ました。DDDの文脈でのコンテキストマップにおいて腐敗防止層という考え方がありますが、これを既存概念(データモデル)に適用した形です*3

f:id:a-tomono:20201027000355p:plain
Repository以降で既存資産との変換を行う(※図はサンプル)

では、このAdapterクラスをどこに置くかというと、また新しくinfrastructureパッケージを追加しました。前節では三層アーキテクチャ+ドメインオブジェクトと記載しましたが、ここに関しては部分的に”依存関係逆転の原則”に従い、ヘキサゴナルアーキテクチャ(ポート&アダプター)の構造になっています。

ふりかえり

設計変更の効果と開発プロセスへの影響度合い、実装イメージの共有を目的として一部追加機能で実装を始めた段階です。結果が出るのはまだ少し先になりそうですが、既存のデータモデルに引きずられず、ドメインモデリングした結果をうまくコードで表現できていると思います。また、追加機能の実装を中心的に推進してくれたメンバーからは以下のようなコメントをもらっています。

  • 既存のテーブルに依存せず、本当に必要なものだけをドメインモデルで表すという考え方はとてもシンプルで分かりやすい。実際に出来上がったドメインモデルを見ると、必要な情報が思っていたより少なくて驚いた。ドメインモデルがシンプルなので処理の流れもシンプルになるという好循環。
  • Repository+Adapterの実装パターンのおかげで、既存のテーブルを気にせずドメインモデルをコードに起こすことに集中できた。
  • まだ理解しきれていない部分も多くあり、詰まる部分もあるが、実践していく中でドメイン駆動設計の良さに気づけることもあるので継続していきたい。

最も複雑、かつコアドメインである契約領域への適用は、ロードマップに従って順次進めていきます。並行して、本記事で触れていないプラクティスについても有効性を確認しながら適用していこうと考えています(少なくとも、ドメインサービスはかなり悩んだ/悩んでいるので、別途記事にできればと思います)。

まとめ

戦略的なドメインモデリングと並行して取り組んでいる、コードで表現する最小限のパターンをご紹介しました。本記事で触れた以外にも多くのプラクティスがありますが、すべてをすぐにプロダクトへ適用するのは難しいものです。なにより既存資産、既存サービスへの影響を考えながら、手段と目的を履き違えぬよう注意深く進めなくてはなりません。

◯◯をしたからDDDやってます!もなければ、△△していないのならDDDとは呼べない!というのはないと考えています*4

ドメイン知識を育てながら、愚直にオブジェクト指向プログラミングをするのが、一番のDDD成功への近道と言えそうです。

最後に

Holmesでは、今後もコアドメインに注力し、変更に強いプロダクト構築を通じて、お客様の契約業務に関する課題の解決とCLM市場拡大を進めていきます。 DDDに関して知見や興味があり、一緒にHolmesの目指す世界観を作りたい方、是非力を貸してください!

Holmesでは現在エンジニアを募集しています。 興味がある方は是非こちらからご連絡ください!

lab.holmescloud.com

*1:集約、値オブジェクト、エンティティ、ドメインサービス等のプラクティスやアーキテクチャなどを指します。詳細は以下の書籍をご参照ください。

エリック・エヴァンスのドメイン駆動設計(牧野 祐子 牧野 祐子 今関 剛 今関 剛 今関 剛 和智 右桂 和智 右桂 Eric Evans)|翔泳社の本 www.shoeisha.co.jp

*2:厳密にはJPARepositoryインタフェースを継承していて、Springが暗黙的に実装していますが、分かりやすさを優先しています。

*3:念のため補足すると、既存資産が腐っているという意味ではありません。念のため…。

*4:さすがにドメインを無視してDDDだ!はないとは思いますが…

WebシステムのXcodeによるモバイル確認についてまとめました(デザイン、動作)

こんにちは、Holmesでエンジニアをしている柳沢です。

今回はモバイルのデバッグ方法について書こうかと思います。 皆さんはどのように確認していますか?

多数の人はWebブラウザに搭載されているデベロッパーツールを利用していると思います。
バイスを選択できたり、縦横比をいれるだけで簡単に確認できるのはいいですよね。
ただ、JavaScriptの挙動やデザインなどを見るとなると物足りないと思うはずです(レスポンシブの動作を見るにはまだ良いのですが、デザインがかなり異なる場合やジェスチャ操作、カメラ、ファイル選択、シェアAPIのエミュレートなど)。
やはり実機、、でも揃えられるわけないじゃん!
ということで、iOSXcodeAndroidAndroid Studioに落ち着くわけです。

XcodeAndroid Studioはそもそもネイティブアプリ開発用のIDEです。ですので開発したものがモバイルでどうなっているのかなんて当然わかるわけです。
その開発用のIDEの機能の一部を使用し、シミュレートするのです。これは頼り甲斐がありますねー 。

そこで需要の高いiOSXcodeでのデバッグを少し詳細に書いてみようと思います。

操作

現在、XcodeMacOSバージョン10.15.4以上でないとインストールできないようです。
https://apps.apple.com/jp/app/xcode/id497799835?mt=12
上記よりApp Storeにいきダウンロードします。

起動するとプロジェクトを作る画面が表示されますが無視して、ウィンドウにあるアプリケーションメニューよりXcode → Open Developer Tool → Simulatorを選びます。

シミュレーター
シミュレーター

初期ではiPhoneが表示されますが、File → Open Simulatorで切り替えることができます。

デバイス切替
バイス切替

ipadシミュレーター
iPadシミュレーター

Safariを起動して開発よりシミュレーターを選択するとデバッグできたのですが、
現在ではSafari Technology Previewという最新の技術を試験実装したプレビュー版に統合されたようですね。
こちらをダウンロードして起動すれば今までと同じ操作、Develop → Simulator → デバッグしたいページを選択で見ることができます。

safari technology preview
safari technology preview

*先にSafariを立ち上げていると開発にシミュレーターが出てきませんので再起動しましょう

リモートデバッグ
リモートデバッグ

所感

ネイティブアプリ開発用ツールなので実機のように挙動やデザインが確認できることはすばらしいですね。 ただ容量が結構必要なことと、動作が重いのは難点ですかね。。

それではまた!

最後に

Holmesはエンジニア・デザイナーを募集しています!
興味がある方はこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

WebDavでWord直接編集をしてみる

こんにちは、尿管結石を再発させてしまったid:c-terashimaです
数年ぶりに味わう痛みはやはり辛いですね。。皆さんも健康に気をつけて楽しいエンジニアライフを送っていきましょう

文章を書くにはWordを利用されている方が多く、契約書を作成するにも今使っているWordをそのまま使いたいというご要望をいただくことがあります
Wordを直接編集したら弊社ホームズクラウドへ登録される仕組みが可能か検討したところ、WebDavを使えばイケるのではないか?ということで早速試してみました

環境

次の環境で動作確認を行っています

  • Java8
  • SpringBoot
  • SpringSecurity
  • Word for Mac

TomcatWebDavを起動させる

ホームズクラウドはSpringBootで動作しているため、SpringBoot(Tomcat)上でWebDavを動作させる必要があります
TomcatWebdavServletクラスが用意されていますのでこちらを使っていきます

@SpringBootApplication
public class Application extends SpringBootServletInitializer {
    public static main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        final TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.setDocumentRoot(new File(documentRoot));
        return factory;
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean() {
        // WebDavエンドポイントの設定
        ServletRegistrationBean bean = new ServletRegistrationBean(new WebdavServlet(), "/webdav/*");

        bean.setName("WebDav");
        bean.addInitParameter("debug", "1");
        bean.addInitParameter("listings", "true");
        bean.addInitParameter("readonly", "false");

        return bean;
    }

    @Bean
    public StrictHttpFirewall httpFirewall() {
        StrictHttpFirewall firewall = new StrictHttpFirewall();
        firewall.setAllowedHttpMethods(Arrays.asList(
                "HEAD", "DELETE", "POST", "GET", "OPTIONS", "PATCH", "PUT",
                "PROPFIND","PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK"
        ));
        return firewall;
    }
}

servletContainerメソッドでドキュメントルートをしており、Wordファイルが配置されるパスとなります。実際運用する場合は、クラウドストレージにファイルを格納することになります
servletRegistrationBeanメソッドでServletクラスの登録とエンドポイントを設定しています。ファイルアクセスする際に使われます
WebdavServletクラスにパラメータを設定する必要があります

  • debug:debugログの出力を制御
  • listings:ファイルリストの参照を制御。これをONにしないとファイル操作ができない
  • readonly:ファイルの書き込みを可能にする

WebDavでは独自のHttpMethodを利用するためSpringSecurityを利用していると弾かれてしまうため、登録します

メソッド 機能
PROPFIND プロパティの取得
PROPPATCH プロパティの変更
MKCOL コレクションの作成
COPY コレクションを含むリソースおよびプロパティの複製
MOVE コレクションを含むリソースの移動
LOCK コレクションを含むリソースのロック
UNLOCK コレクションを含むリソースのロック解除
OPTIONS 既存のメソッドと同じ
GET 既存のメソッドと同じ
HEAD 既存のメソッドと同じ
POST 既存のメソッドと同じ
PUT リソースの作成
DELETE コレクションおよびそのコレクションに含まれるリソースの削除

サーバ側の設定はこれだけです

ブラウザからアクセス

Office URI スキーマでブラウザからWebDavにアクセスを行いWordを開きます

Office URI スキーマとは

このドキュメントでは、オフィス生産性向上アプリケーション用の Uniform Resource Identifier (URI) の形式を定義します。このスキームは、Microsoft Office 2010 Service Pack 2 以降でサポートされており、これには Microsoft Office 2013 for Windows および Microsoft SharePoint 2013 などの製品が含まれます。また、Office for iPhone、Office for iPad、および Office for Mac 2011 でもサポートされます。

Office URI スキーマ | Microsoft Docsより引用

URIスキーマの仕様に則って ms-word:ofe|u|http://localhost:8080/webdav/test.docxにアクセスするとWordを起動するかを問うダイアログが表示されますので Microsoft Word.app を開くをクリックしてください。WebDavに公開されているWordが開きます
そのまま、編集し保存を行うと自動でサーバにアップロードされます

f:id:c-terashima:20201008190029g:plain

まとめ

SpringBoot(Tomcat)でWebDavは結構かんたんに実装することができました
ただ、最低限の動きを検証しただけなのでセキュリティなどには十分注意してください
ホームズクラウドにWordの直編集機能が組み込まれるのは近い!?







Holmesはエンジニア・デザイナーを募集しています!
興味がある方はこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

PlantUMLでドメイン駆動設計のモデリングを実装する(Nizi Project編)

こんにちは!株式会社Holmesでエンジニアをしている平田です。

Holmesでは、現在、プロダクト開発にドメイン駆動設計を取り入れようと、社内で勉強会の開催や各メンバーが勉強したことを共有しあったりしています。
ドメイン駆動設計に取り組むにあたって、大切なことのことの一つにより良いモデルを作成する、モデリングがあります。
今回はそんなモデリングをPlantUMLで行う方法について、一例をご紹介できればと思います。

※本記事のモデリング手法は以下の書籍を参考に行なっております。

PlantUMLとは

PlantUML(github)とは、オープンソースUMLダイアグラム作成用の言語です。
本記事ではVisual Studio Code拡張機能を用いています。(PlantUMLのセットアップは拡張機能の説明をご覧ください)

なぜPlantUMLを用いるのか

モデリングを行う方法はオンライン・オフライン問わずいくらでも存在すると思います。
弊社でも、オンラインホワイトボードのmiroを使ったりしています。
その中で、PlantUMLを使う利点としては、プレーンテキストで記述できるため、Gitなどのバージョン管理システムで管理できることではないでしょうか。 PlantUMLで記述したモデルをバージョン管理することによって、コードレビューのプロセスに乗せたり、変更を追っていくことができるようになります。

ユースケース

まずはユースケースを作成します。
今回は、Nizi Projectドメインとして作成します。

Nizi Project(ニジプロジェクト)は、韓国大手事務所JYPエンターテイメントとソニーミュージックによる共同ガールズグループプロジェクトである。

(Wikipediaより)

このオーディションに合格したメンバーはNiziUとして2020/12/02にデビューします。(ついでに覚えて帰ってね)

全体像

f:id:k-hirata:20201007012400p:plain
ユースケース

@startuml NiziProject
actor プロデューサー
left to right direction
rectangle {
    プロデューサー -- (キューブをあげる)
    プロデューサー -- (テストを進める)
    プロデューサー -- (パート1脱落者を決める)
    プロデューサー -- (パート2脱落者を決める)
    プロデューサー -- (合格者を決める)
}
@enduml

たったこれだけでユースケース図を作成することができます。
記述内容の詳細を説明します。

@startuml,@enduml

これは開始と終了をそれぞれ表しています。

@startuml
@enduml
actors
f:id:k-hirata:20201008223846p:plain
actor プロデューサー

actor プロデューサーで人の形をした図が表示されラベルを設定することができます。
:プロデューサー:と記述することもできます。

usecases
f:id:k-hirata:20201008223950p:plain
:プロデューサー: -- (キューブをあげる)
f:id:k-hirata:20201008224037p:plain
left to right direction

プロデューサー -- (キューブをあげる)ではユースケースとactorとの関連を表現しています。--と記述することで関連を線でつなぐことができます。
ユースケースusecase キューブをあげると書くこともできますが、今回は定義と関連付けを同時に行いたかったので(キューブをあげる)と記述しています。
left to right directionは関連の方向を指定しています(作成したい図に合わせて調整してみてください)。

rectangle
f:id:k-hirata:20201008231932p:plain
rectangle { :プロデューサー: -- (キューブをあげる) }

rectangle {}はブロック内に記述することで枠に囲われたように表現することができます。見た目やスコープのために記述しています。

ドメインモデル図

ドメインモデル図は冒頭に紹介した書籍に沿ったルールで作成します。

  • ルールを吹き出しに記述する
  • オブジェクトの関連を記述する
  • 多重度を記述する
  • 集約の範囲を記述する
全体像

f:id:k-hirata:20201008011105p:plain
ドメインモデル図

@startuml NiziProject
skinparam PackageStyle rectangle

package 候補者集約 {
    object 候補者 {
        候補者ID
        パート1脱落
        パート2脱落
        合格
        オーディションID
    }
    object 名前 {
        姓
        名
        ニックネーム
    }
    object ペンダント {
        パート1キューブ
        パート2キューブ
    }
    object キューブ {
         キューブ名
    }
}

note left of ペンダント
    * ペンダントにはそれぞれ4つのキューブをはめることができる。
    * パート1用キューブは重複できず決められた4種類、
      パート2用キューブは最大4つはめることができる。
end note

note bottom of キューブ
    * キューブは5種類存在する。
    * パート1キューブ(「ダンス」「ボーカル」「スター性」「人柄」)
    * パート2キューブ
end note

note right of 候補者
    * パート1で脱落した候補者はパート2キューブを獲得できない
    * パート2で脱落した候補者も以降、パート2キューブを獲得できない
end note

名前 "1" -left-* "1" 候補者
ペンダント "1" -down-* "1" 候補者
キューブ "0..8" -down-* "1" ペンダント

package オーディション集約 {
    object オーディション {
        オーディションID
        オーディション名
        現在テスト
    }
    object テスト {
        テスト名
        パート
        順序
    }
}

note right of オーディション
    * テストの順番は決められている。
end note

note right of テスト
    * テストは1オーディションにつき、パート1に4回、パート2に4回ある
    * テストで獲得可能なキューブは決められている。
    * パート1テスト1~3は決められたパート1キューブを1つ、
      テスト4ではパート1キューブ最大4つ獲得できる。
    * パート2テスト1~3まではパート2キューブ1つ、
      テスト4ではパート2キューブ最大4つ獲得できる。
end note

候補者 "1..n" -down-> "1" オーディション
テスト "8" -down-* "1" オーディション
テスト "1..4" -down-> "1..4" キューブ

@enduml

記述内容の詳細を説明します。

packages
f:id:k-hirata:20201008233415p:plain
package 候補者集約 {}
f:id:k-hirata:20201008233433p:plain
skinparam PackageStyle

skinparam PackageStyle rectangleはパッケージのスタイルを変更するために記述しています。様々なスタイル変更を施すことができるので、興味がある人は調べてみてください。

package 候補者集約 {}集約を表現しています。ブロック内に記述することで集約内であることを表現できます。

objects
f:id:k-hirata:20201008233704p:plain
object 候補者 { 候補者ID }
f:id:k-hirata:20201008234121p:plain
package 候補者集約 { object 候補者 }

object 候補者 {}はオブジェクトです。ブロック内には属性を記述することができます。

notes
f:id:k-hirata:20201008235304p:plain
note left of ペンダント: コメント
f:id:k-hirata:20201008235322p:plain
改行できる

吹き出しでルールを記述することができます。以下は改行の表現で、end noteまでの間に文章を記述し、改行が反映されます。また、インデントが揃っていれば、吹き出しはインデントされませんが、インデントがずれている場合、最もインデントが少ない行をスタートとしてインデントが反映されます。改行が不要な場合、note left of ペンダント: コメントと簡潔に記述することもできます。

note left of ペンダント
  改行
  できる
end note
direction
f:id:k-hirata:20201008235710p:plain

left吹き出しをオブジェクトに対してどの方向に表示するかを指定できます。(left、right、top、bottom)

relations
f:id:k-hirata:20201009000912p:plain
名前 "1" -left-* "1" 候補者
f:id:k-hirata:20201009002700p:plain
名前 "1" -right-* "1" 候補者

名前 "1" -left-* "1" 候補者は多重度とオブジェクト間の関連を表現しています。
-left-*は関連になりますが、--の間にleftと入れることで関連を表示する方向を決めることができます。(left、right、up、down)

label

f:id:k-hirata:20201009002806p:plain
候補者 "0..n" -down-> "1" オーディション
終端の*はいくつかの種類があり、始端と終端両方に記述が可能です。ドメインモデル図では*または>しか登場しません。
関連付けるオブジェクトと関連付けの間に""(ダブルクォート)で囲んで文字を記述することで多重度の表現ができます。

振り返り

私自身、この記事で初めてモデリングを行いました。

ドメインモデル図を見ていただらいたらわかるように、テストのルールが多かったり、キューブがテスト・候補者の双方に深く関連しているように見えます。
今回は、「キューブをあげる(候補者がキューブを受け取る)」ユースケースを鑑みてこのようなモデリングを行いましたが、ユースケースや今後の展開によっては、キューブ集約やテスト集約といった形に変更していくことも考えられます。

モデリングは最初に作成したら終わりではなく、コード同様、常に変更し、より良いモデルに改善していくことが重要です。
モデルをPlantUMLで記述してバージョン管理していくメリットが活きてきますね!

最後に

いかがでしたでしょうか。
PlantUMLを用いたドメイン駆動設計のモデリングが容易にできることがわかっていただけたと思います。
是非活用していただければ幸いです。

Holmesではエンジニア・デザイナーを募集しております。ご興味がある方はこちらからご連絡ください。

lab.holmescloud.com

lab.holmescloud.com