ContractS開発者ブログ

契約マネジメントシステム「ContractS CLM」の開発者ブログです。株式会社HolmesはContractS株式会社に社名変更しました。

Vue 3, Nuxt 3 マイグレーションを経験してみて

はじめに

こんにちは。 ContractSでフロントエンドエンジニアをしている村澤です。

先日、一時的に Vue 2.x から Vue 3.x のマイグレーションへ携わる機会があったため、その間の学びを共有します。

ContractS CLMでは、Vue.js を用いて開発をしています。

Vue.jsとNuxt.jsは、Web開発において広く利用されている人気の高いフレームワークです。
技術の進化とともに、それらのバージョンも進化を続けています。Webフレームワークのマイグレーションは、開発者にとって重要なタスクです。本記事では、その必要性と経験について述べます。

Vue 3, Nuxt 3へのマイグレーションがなぜ必要なのか

Vue 2は、2023年にEOL(End of Life)を迎えました。

v2.vuejs.org

これは、Vue 2の新しい機能やセキュリティパッチが提供されなくなったことを意味します。また、Nuxt 2.xの依存関係にあたるライブラリが更新されることも無くなるため、脆弱性が発見された際のパッチ適用が難しくなります。
したがって、品質の維持とセキュリティの確保のために、Vue 3およびNuxt 3へのマイグレーションが必要です。

破壊的変更の学習

Vue 3への移行には、破壊的変更を伴います。これは、Vue 2から廃止された機能、仕様の大きな変更などが含まれます。 その中でも影響が大きいと感じた変更はv-modelの扱いです。
Vue 3 からは以下のような仕様変更がなされています。

バージョン デフォルトプロパティ名 デフォルトイベント名
Vue 2 value input
Vue 3 modelValue update: modelValue

Vue 2では、props.sync修飾子で、子コンポーネントからのemit イベント名をupdate:(prop名)とする仕様がありました。 対してv-modelのデフォルトイベント名は、上記の表のように.syncと形式が異なったため、Vue 3ではv-modelにおいても同様の仕様に統一された形になります。 このような変更について学習し、変更する必要があります。

v3-migration.vuejs.org

プラグイン・ライブラリの選定

Vue 2で使用していたプラグインやライブラリの多くは、Vue 3に対応していないもの、もしくはVue 3に対応するために仕様変更されたものがあります。これにより、移行コストが高くなります。さらに、Vue 3に対応する代替ライブラリが存在しない場合もあります。
このような場合、開発者は自らライブラリを更新するか、あるいは代替手段を模索する必要があります。また、既存のライブラリとの互換性の問題も発生する可能性があります。

例えば、Nuxt 3.8でbootstrap-vue-next@0.14.10を導入すると、ビルドエラーが発生するという問題があります。このような問題に対処するためには、開発者は適切な代替手段を見つける必要があります。

Vue 2コンポーネントの移行コスト

今まで Vue 2で記述されていたコンポーネントを、すべて Vue 3 の記法へ変更する必要があります。 プロダクトの大きさにもよりますが、CCLMの場合300近くのコンポーネントがあり、これをすべて書き換えるとなると膨大な時間がかかることは想像に容易いと思います。
しかし、この効率をあげる方法に画期的なものはなく、現在有力なのはVue 2のプロジェクトをVue 2.7までバージョンを上げ、バックポートされているVue 3の機能を取り入れることです。

blog.vuejs.org

マイグレーション全体を通して

上記に書ききれなかった学びを簡単に記述します。

  • 公式推奨のプラグイン・ライブラリでもアプリケーションに合わないことがある。

    • ex) ストーリー記述としてStoryBookを推奨しているが、Viteでのエイリアス解決失敗とビルドエラーにより、Historeを採用
  • 思い切って既存のプラグイン・ライブラリを別のプラグイン・ライブラリに変更する
    • ex) BootstrapVueからPrimeVueに変更
  • 原因不明のビルドエラーが発生し、解消できない場合はプラグイン・ライブラリのissuesを確認する

さいごに

他にも多くの課題がありますが、今回は主要な課題を取り上げました。 マイグレーションは現在も進行中ですが、上記に上げたように多くの学びが有り、貴重な経験でした。

【Vue.js】Painless なコンポーネント開発のプラクティス

はじめに

こんにちは!ContractS株式会社の北原です。
フロントエンドのリードとして、プロジェクトの技術選定や品質保守等を担当しています。

早速ですが...

みなさまは Vue 3 をお使いになられていますか?
これには、以前の Vue 2 と比較して多種多様な改善がふんだんに盛り込まれています。
2020/09/18 にリリースされてから暫く経っているので、実際の業務や個人で触られた方も多いと思います。

例えば、

  • Composition API の正式採用
  • script setup 構文の対応
  • メモリ使用量削減による負荷軽減

その他色々ありますが、以上のような機能により、従来のメジャーバージョンからご利用になっていた開発者からすると嬉しいものが満載です。

しかし、ただひとつ。 頭を悩ませる要素があるのです。
それこそが本稿を作ったきっかけです。

:deep 機能について

Vue 3 には:deepという擬似クラスとして利用できる機能があります。
これは、コンポーネントから、子コンポーネントの特定要素に対してスタイルを当てたい時に避けては通れない道です。

:deep の具体的な使い方については、以下の公式ドキュメントをご覧ください
ja.vuejs.org

:deep 機能のつらいところ

Atomic Design程やらないまでも、多くのデザインシステムではatomsのように基底コンポーネントを作成し、各コンポーネントで使う形だと思います。

自社で全ての基底コンポーネントをスクラッチで作成し利用すれば、SFC で運用しても自由にスタイルをカスタマイズできるので問題ないですが、多くのプロジェクトでは VuetifyQuasar のようなUIライブラリをご利用になると思います。

また、そのままの見た目で利用することは少なく、UX統一等の理由でスタイルを適用すると思います。
当然ライブラリなので、日々バージョンアップが行なわれていきます。その際にコンポーネントのDOM構造が変化することもあるでしょう。

:deepは前述の通り擬似クラスであり、CSSセレクタである以上、UIライブラリのコンポーネントのDOMを知らないといけません。
方法としては様々あると思いますが、検証ツール等を用いてスタイルを当てたい要素を探し、その要素を一意に特定できるセレクタを作ってスタイルを当てる必要があります。

これを頻繁にメンテナンスするのは高コストで、コンポーネントに新規のバリエーションを持たせたい時にもスピーディな開発を阻害します。

そんなあなたに PrimeVue

PrimeVue は、Vue 3 で利用可能なUIライブラリのひとつであり、90をも超える多彩なコンポーネントと堅牢なFWを提供しています。
今回紹介する機能を抜きにしても、単品として高いアクセシビリティを誇るので、是非以下で触ってみてください。

primevue.org

PrimeVue の Pass Through 機能

Pass Through (パススルー) 機能は、ドキュメントで以下のように説明されています。

The Pass Through props is an API to access the internal DOM Structure of the components.

要するに、このAPIを利用することでPrimeVueのコンポーネント内部のDOM構造へアクセスが可能になるというわけです。
すでに:deepで感じていた pain を解決できる匂いがしますね。

Pass Through の利用方法

PrimeVueで提供される全てのコンポーネントには、ptという名称のpropsが用意されています。
そこに、PrimeVueのDOM毎の命名をkeyとして持つObjectをbindすることで、DOMのカスタマイズが可能です。

これだけだと分かりづらいと思うので、今回は Vue Button Component を具体例として挙げます。

ご覧いただくと、PASS THROUGHタブ内で、画像のようにDOM毎に連番が割振られていると思います。

PrimeVue Button の Pass Through 連番

ここで、4番のラベル部分のスタイルをカスタマイズしたいとします。
そのまま下にスクロールしていただくと、対象の連番に対して命名があると思います。

PrimeVue Button の Pass Through 命名

labelという命名がなされていますね。
これをkeyとした Object を、SFC上で以下のようにbindします。

PrimeVue Button の Pass Through Bind

Tailwind CSS のクラスであるfont-boldを適用してみました。

なんとこれだけで、PrimeVue Button のDOM構造を知らずともスタイルが適用できてしまいます。
画像のようにcomputedを bind することで動的なスタイル変更も可能です。
今回はTailwind CSSを利用しましたが、グローバルなUtility Classとも非常に相性がいいです。

最後に

いかがでしたでしょうか?

:deepで大変な目に遭われた、もしくは今まさに遭われている方もいらっしゃるかと思います。私も同じで、Nuxt 2からNuxt 3へマイグレーションを行なう際にとても痛い目を見ています...笑

もし良さそう!と感じていただけましたら、導入は以下で簡単に行なえますので、是非お試しください!

出典

グループ会社間で行なった勉強会の内容となります。
本稿よりも、もう少し実際の利用例や他機能も交えた解説を行なっていますので、よろしければご覧ください!

非エンジニアがMarketingAutomationを使わずに顧客向けメール配信を完全自動化した話

こんにちは。そしてはじめまして。

ContractS コーポレートサクセス部のぽよさんこと新井です。

僕は職業エンジニアではないのですが、職務の一つとして社内の業務システムの管理者を受け持っており、仕事柄多くのツール(特にSaaS製品)を駆使して業務設計する機会が多く、「あ、組み合わせるとこんな感じの動きができるのね」というネタが出てきます。 そんなネタを紹介してみようと思います。

今回出てくるツール

  • Salesforce・・・CRM/顧客管理ツール。以下SF。

  • Pardot・・・MarketingAutomation(MA)ツール。セールスフォース社のMAでSFと連携/連動している。(※現:Account Engagemant)

  • GoogleAppsScript・・・GWSにも含まれる、Googleのプログラミングツール。以下GAS。

事の発端

当社はContractS CLMというSaaSサービスを提供していまして、お客様のサービス利用契約はサブスクリプションモデル/期間契約となっています。

当社のカスタマーサクセスチームより、「お客様に『あなたの会社の契約更新日はmm月dd日ですよ』のようなメール通知を特定のタイミングでお送りしたい。」とのリクエストをもらいまして、早速話を聞いてみました。

会話する中で決まった要件が下記なのですが、、、

1. 一斉配信メールではあるが、顧客の利用代表者様に適切にメールを届けたい。営業目的ではないため、MAで有している宛先ごとのOptOut設定を問わず送信したい。
 (※OptOut設定・・・メールアドレスの持ち主が、MA等を用いて送信されるメルマガ/営業系メールなどを送られるのを止める設定のこと。メール配信停止、などと表記することが多い。 なおこのケースでは、お客様の契約に係る重要な要件となるため、営業系メールと異なりOptOutに関わらずメールは送信したい。)

2. メール配信タイミングは毎月月初で定期実行。月1回のみでよい。

3. 配信元データはすべてSFの中にある情報を用いて条件設定を行う。元データの所在がわかりやすく、かつ条件式は設計者以外も認識しやすいレベルに噛み砕いたものを希望したい。

4. 対象顧客の抽出条件は契約更新日を用いる。例えば1月月初のメールでは、翌翌々月中(=4月中)が契約更新日となっているお客様を宛先としたい。

5. 業務負荷やヒューマンエラーを回避するために理想は完全な自動化。難しいとしても、手動での毎回の抽出条件変更、宛先リストアップ、メールへの記入は極力避けたい。最初に設定した条件を毎月変更せずに使い続けたい。

 (※ちなみにContractS CLMのサービス利用契約は比較的柔軟に組み合わせられるようにした都合、サービスプロダクト側にユーザの利用契約に関する情報は入れない仕様になっています。)

これらを受け、検討を始めます。

出てきた壁、課題

PardotでOptOut設定の如何に関わらずメール送信すること自体は可能なのですが、上記4の抽出条件を満たす方法があるか、という点が出発点となりました。

SF(Pardot)のレポート抽出は比較的柔軟性が高いので、「相対日付」の条件を用いることを検討したのですが、「翌翌々月」という相対日付がSFでは設定できません。

次に、「翌翌々月」を「(本日から見て)90日後~120日後」と読み替えて相対日付とする案を考えました。ただこれはメール送信する月日によっては月初日と月末日をピッタリ捉えることができないことに気が付き、断念しました、、、。

結局、SF&Pardotで対象顧客を抽出を完結することは難しそうだな、という途中経過に至ります。

ここでこの課題をクリアすべく編み出した方法が、今回ご紹介する内容です。

この方法であれば、一部GASでのコーディング(メール一斉配信部分)は必要となりますが、比較的難易度が低く汎用性が高いものなので、

  • 元データは当初の希望通りSFレコードを用い、レポートでの抽出条件は相対日付で設定。日付が変わったりSFレコードが更新されれば(多少のラグはあるが)自動的に反映される。

  • 対象の抜き出し、リスト整形はGoogleSpreadsheetで柔軟に実行 & 数式を組めばもちろん自動反映。

という形で元データの収集を自動化することができ、かつGASで仕込んだメール一斉配信ツールを定期発火できれば、自動化は万事OK。という算段です。

以降で順を追ってご紹介します!

Step1:Salesforceでレポートを準備

まず、『例えば1月には、翌翌々月(=4月)に契約更新日(今の契約期間の最終日)がくるお客様を宛先としたい。』という難儀な条件を含むSFレポートを作成します。 前述の通り、日付ドンピシャの検索条件は作れないため、下記条件の組み合わせで少し広めに設定します。

一致しない = 翌70日間

AND

一致する = 翌130日間

日付条件の組み合わせ

この組み合わせで、「常に今日より71日先から130日先までの日付を持ったレコードを抽出する」という条件が組めました。

相対日付を2つ組み合わせることで、データの絞り込みを効かせる方法です。
実際には、1月頭であれば、ざっくり3月半ば〜5月上旬の日付を抽出する形ですね。

Step2:Salesforce to SpreadSheetで自動レポート

次に、SFのデータをSpreadsheetにサクッと展開する方法を設定します。

SFのデータを出力、といえば王道は「レポートのエクスポート(.csv/.xlsx)」もしくは「データローダ(.csv)」かと思いますが、今回はSpreadsheetでSFのデータを扱いたいので、
Salesforce Connector を利用します。
↓この機能です

support.google.com

この機能を有効にしておけば、GoogleSpreadsheetにSalesforceからバシャっとデータをエクスポートすることができます。 詳細/機能全体の説明は割愛しますが、今回は、

  • 「Report」でSpreadsheetに取り込む

  • 「Auto Refresh」を設定して定期更新する

の2段構えです。

まずReportで、Step1にて作成したレポートを出力してみます。

Report

※出力イメージ

Spreadsheetでの出力イメージ

SFレポートの内容が、そのままシートに貼り付けられます。

次に、RefreshからAuto Refreshを選択します。

Auto Refreshは、一度Reportをしていると、そのReportを再度Refreshすることができる機能です。

Refresh

これによって、最短4時間ごと更新ですが、「常に最新のSFレポートをSpreadsheetに出力する」を実現しています。

ちなみにですが、この工程はレポートが2つ以上あっても有効です。
(それぞれ別のシートタブにレポートを出力→Auto Refresh設定1つで全レポートを定期更新できる)

検証中は、Auto Refreshは設定せず、一度Reportを実行したあとに見出し以外のデータレコードをダミーに書き替えることをおすすめします。

Step3:Spreadsheetでのデータ整形(QUERY関数)

元データを抽出できるようになったら、データを加工するところはGoogle先生に託します。
Spreadsheetでデータの抽出といえばQUERY関数が優秀です。

=QUERY('★元データのシート名★'!A:C,"select* where B>date'"&TEXT(eomonth(today(),+2),"YYYY-MM-DD")&"' and B<=date'"&TEXT(eomonth(today(),+3),"YYYY-MM-DD")&"'")

という形で、常に『翌翌々月の1ヶ月間に契約更新日が来る』レコードだけを更に抽出するようにします。
Step2でQUERY抽出したシートを、送信リストとして用います。

Step4:GASでメール一斉配信

続いてメール一斉送信です。
参考までに、私が利用しているスクリプトをサンプルで貼っておきます。
動作はSpreadsheet上ですので、コンテナバインドのGASエディターを使うのが良いかと思います。

//メールを一括送信するコード
function sendMail(){
  const spreadsheet = SpreadsheetApp.getActive();
  
  //送信先リストに使うシートをアクティブにしてデータを取得しにいく
  spreadsheet.setActiveSheet(spreadsheet.getSheetByName("★送信先リスト★")); 
  const sheet = SpreadsheetApp.getActiveSheet();
  
  //2行目から最終行までループ処理を行う
  const lastRow = sheet.getLastRow();
  for(let i = 2; i <= lastRow; i++){
 
    //行ごとに1列目を取得
    let account_name = sheet.getRange(i, 1).getValue();
    //行ごとに2列目を取得
    let renewal_date = sheet.getRange(i, 2).getValue();
     renewal_date = Utilities.formatDate(renewal_date, "JST", "yyyy/MM/dd");
    //行ごとに3列目を取得
    let to = sheet.getRange(i, 3).getValue(); 

  //メールのテンプレートがあるシートをアクティブにして内容を取得
  spreadsheet.setActiveSheet(spreadsheet.getSheetByName("★メールテンプレート★")); 
  const templateSheet = SpreadsheetApp.getActiveSheet();

    //B1セルはメールの件名として取得
    const subject = templateSheet.getRange(1, 2).getValue();

    //行ごとにB2セルのメール本文を取得して文章内の{取引先名}をそれぞれ上で定義した変数で置換
    const message = templateSheet.getRange(5, 2).getValue()
    .replace('{取引先名}',account_name)
    .replace('{更新予定日}',renewal_date)

    //メールの送信元を指定
    let options = {
    from: "",
    name: "",
    cc: ""
  };
  options.from = templateSheet.getRange(2, 2).getValue();
  options.name = templateSheet.getRange(3, 2).getValue();
  options.cc = templateSheet.getRange(4, 2).getValue();

    //取得した内容をGmailで送信
    GmailApp.sendEmail(to,subject,message,options);
  }
}

メールテンプレートのシートはこのような形ですね。

メールテンプレートのシートイメージ

ここまで完了すると、「(SFから自動転記をした上で)対象者を更に絞り、Spreadsheetの内容をもとにメールを一斉配信する」が行えるようになります。

Step5:GASのトリガー設定

最後にGASのトリガー設定を行います。

私はトリガー設定を、下記のように設定しています。
このあたりは、他のメール一斉配信のタイミングなどを鑑みて適宜調整いただくのがいいですね。

GASトリガー

終わりに

本件は、メール一斉配信のGAS以外はノーコードで、比較的簡素に仕上がっています。

GASのコードに関しても、汎用性高めなのと、パラメータ部分を変更するだけでおおよそ変化にも追従できる仕様になっていると思います。
こなれてくると、SF元データが正しい前提にはなりますが、他のメール配信系のアクションでも完全自動化させることが可能です。(弊社も順次、完全自動を増やしています。)

GASでのメール一斉配信は、裏側でGAS設定者のGmailアカウントから送信する仕様ですが、
メールが増えてくると、必然的にMail Send Failureとして返ってくるメールも設定者のGmailに返ってくる数が増えてきます。

「自動でメール送ったはいいけどFailureで返ってくる数多いなあ。1件ずつメーラーでアドレス確認して確認依頼を社内に回すの面倒だなぁ」と怠惰な気持ちが芽生えてくるのですが、長くなってしまうのでこのあたりはまた別の機会に、、、。

また、SFのAutoRefreshも、実はこのままだとFail発生時に気付くことができないので、こちらもまた別の機会に、、、。

お読みいただきありがとうございました。もう1本、他のメンバーの記事を見ていただけるととても嬉しいです!

イベントストーミング体験ワークショップに参加した学びと感想

こんにちは。テックリードの友野です。最近、急に寒くなったもので、衣替えが追いついていません。

さて、11/10(金)にUMTP主催のModeling Forum 2023ワークショップ「ドメインモデリングの強力なツール: Event Stormingを体験しよう」に参加してきたので感想と、メモの整理を兼ねてポストします。
ドメインイベントの伝播による整合性担保やその仕組みはContractS CLMで部分的に採用していますが、その領域はごくわずかです。イベントストーミングという言葉そのものは知っていましたが、業務では採用しておらず、経験もありません。今回のワークショップを通じて、多くの学びがありました。

umtp-japan.org

(2023/11/15 追記:UMTP事務局よりmiroキャプチャ利用許可をいただいたので、画像を追加しました)

ワークショップ概要

ワークショップは、イベントストーミングを体験し、慣れることを目的としています。
1チーム4~6人で構成し、イベントストーミングのプロセスに沿って議論しながらmiro上でモデリングをしました。テーマは図書館業務。誰もが知っている/利用したことがある一方で、その業務についてすべてを理解している人はいないことが理由だそうです。図書館司書と利用者、2つのアクターそれぞれの業務を参加者全員(30人!)で分担して整理しました。
具体的には、書籍"Learning Domain Driven Design”(以下、LDDD)で紹介されているステップに沿って(一部省略して)進めていく形式です。ステップバイステップで進めていく中で生まれた疑問点や不明点を都度、講師(Chatwork加藤さん/GMOインターネット成瀬さん)に聞けるという贅沢な時間でした。

図書館業務のユースケースサンプル
図書館業務のユースケースサンプル

イベントストーミングとは

イベントストーミングはブレインストーミングドメインイベント版です。つまり、イベントをとにかく発散させて徐々に収束させていくドメインモデリングの手法です。このアプローチは、ドメイン上の重要な関心ごとであるイベント=出来事(コト)に着目しています。
イベントに着目する理由として、以下があります。

  • イベントが分かれば、そのイベントを生み出したふるまいが分かる
  • イベントはヒトやモノと必ず関係があり、コンテキストがあるので収束させやすい
    • 逆にモノからアプローチすると、複数コンテキストがある場合があり、発散させやすい

イベントストーミングはビッグピクチャー、プロセスモデリング、ソフトウェアデザインから構成されます*1。参加者の知見や認識を揃えながら、業務の流れを整理したいシーンにはフィットしそうです。

イベントストーミングで使う付箋と依存の方向
イベントストーミングで使う付箋と依存の方向

進め方

1. イベントを整理する(ビッグピクチャー)

ワークショップでは、まず以下の流れでイベントを整理しました。

  1. イベントを書き出す
    • イベントは出来事なので動詞の過去分詞形で表現し、この段階では他者との重複は気にしない。
      • 例:利用者を登録した蔵書を貸し出した
  2. イベントを精査する
    • 同じもの・似ているものをまとめる
      • 特に、用語のゆらぎに注意。ユビキタス言語となりうる。
    • イベントはドメインの状態変化を表すため、確認や閲覧のような状態を変化させないものは除外する
  3. イベントを左から右へ時系列に並べる
    • 同時に起きるイベントは縦に並べる
  4. 時系列を順に読み合わせて矛盾がないか確認する(ウォークスルー)
    • 例えば、登録していない情報を突然更新していないか など
    • 足りなければ、この段階でイベントを追加する

※ LDDDに記載されているフェーズについては、ワークショップではスキップしました。

イベントストーミングでは議論が白熱したり、横道に外れたりした場合は、ホットスポットとしてメモを残して議論を先へ進めます。例えば、「発注した本の支払い方法は銀行振り込みかクレジットカード払いか」という論点は図書館業務の整理から見れば、ホットスポットです。

2. イベントからプロセスの流れを見つける(プロセスモデリング

コマンドとアクター/ポリシーをつなげる

イベントが揃ったら次は、そのイベントを生み出すふるまいの整理です。

  1. イベントからコマンドを作る
    • コマンドはリクエストなので動詞の現在形(命令形)として表現する。日本語の場合、体言止めは分かりにくいので非推奨とのこと。
    • 蔵書を貸し出した(イベント)→蔵書を貸し出す(コマンド)
  2. コマンドをリクエストするアクターを配置する
  3. システムが自動的/連鎖的にリクエストする場合はポリシー
    • ポリシーは次のコマンドをリクエストするトリガーのようなもの
    • ポリシーの例:本を入荷した(イベント)->管理番号採番ルール(ポリシー)->管理番号を採番する(コマンド)

コマンドを実行するためのリードモデルを定義する

コマンドを実行するために何かしら情報が必要であり、この情報こそがリードモデルです。システム外のリードモデルも表現しておくのが肝です。

  1. リードモデルを追加する
    • 複数のリードモデルからコマンドを実行する場合もある
      • 例えば、蔵書を貸し出す(コマンド)ために、蔵書(リードモデル)と貸出状況(リードモデル)が必要
  2. どのイベントからリードモデルが作られるのかを紐づける
    • 蔵書を貸し出した(イベント)後に、貸出状況(リードモデル)が更新される
  3. システム外のリードモデルも表現しておく
    • 利用者登録時の身分証明書 など

外部システムを追加する

モデリング対象の業務とは直接関係のない外部システムがある場合は、議論の発散を避けるため明示するだけに留めます。

3. コマンドとイベントから集約を見つける(ソフトウェアデザイン)

最後に一連の流れを俯瞰してみて、コマンドを受け取り、処理結果としてイベントを生み出す集約を見つけます。
当たり前ですが、このステップは機械的には出来ず、コマンドとイベントからどのような概念があるのか議論が必要です。イベントストーミングを用いない、ヒアリング中心のドメインモデリングでも変わらず難しいステップです。集約は名詞で表現するのが一般的なので、その命名に相応しい責務なのか、分割/統合するならライフサイクルは適切かという観点が重要です。

感想

終日議論をし続けて、疲労感と充実感で満たされたワークショップでした。イベントストーミング初体験でしたが、良い点と注意点が見えてきました。

イベントストーミングの結果
モデリングの様子
チームで発見した集約
チームで発見した集約

良い点

議論を進めやすい

モデリングするフレームワークなので当たり前と言えば当たり前ですが、ステップバイステップで参加者の意識・興味を集中できるので、議論が進めやすく感じました。ワークショップの同じチームメンバーとは初対面、かつ図書館業務経験者(ドメインエキスパート)不在でしたが、適度な発散と収束を繰り返しながら、メンバー全員が納得いく集約の定義まで時間内に進めることができました。

集約検討時に前提の認識を揃えやすい

一つ目と似たような観点ではありますが、イベントに着目した業務の流れを参加者全員で作り上げていくため、いざ集約の議論を始めようとするとそこまでの知識が揃った状態で始められます。場合により、発散させたいケースもあるかもしれませんが、こと問題領域を明らかにしたいケースにおいて、参加者の認識が揃った状態で議論が開始できるのはとても強力です。

要求の曖昧さを排除できる

イベントが時系列に並び、ウォークスルーで整合性を担保できるので、ユースケースの確からしさを検証できます。ContractSでは予備設計としてロバストネス分析を一部領域に採用していますが、近しいものを感じました。繰り返し実施することで曖昧さを排除し、不確実性を減らしながら価値検証するアプローチにフィットしそうです。

注意点

効果を最大化するためにはアーキテクチャの制約がある

注意が必要なのは、イベントストーミングの効果を最大化するためにはCQRS(Command-Query Responsibility Segregation)+イベントソーシングの構成が前提となっている点です。
ふるまいの結果として生み出されたドメインイベントが記録され、そのイベントからリードモデルが作られる前提のため、CQRSのようにモデルレベルで分離した構成が必要ですし、何よりイベントを記録していくためにイベントソーシングを採用していないと、イベントをイベントのまま永続化できません。
ワークショップでは成瀬さんがステートソーシングによる実装方式をライブコーディングしてくれましたが、コードからイベントの知識は消えているので、いずれモデルとコードに乖離が発生してアジリティを失う懸念があります。

逆に言えば、CQRS+イベントソーシングの構成であれば、これほど強力なモデリングフレームワークはないとも思います。

終わりに

初体験のイベントストーミングでしたが、とても興味深いものでした。適用するにはアーキテクチャ観点で若干のハードルがあるものの、エンジニア職以外のメンバーと共通言語で会話する最初の一歩として効果がありそうです。幸い、JavaにはAxon Frameworkというイベントソーシングをサポートするフレームワークがあるため、小さく検証することもできそうです。試した結果はまた別記事でまとめようと思います。

*1:必ずしもこれらのプロセスをしなければならない、というわけではないようです。

Lombokを利用した開発環境でGradleからJavadocを出力する

エンジニアの友野です。久しぶりのポストですが小ネタです。

日頃、開発時はIDE上でJavadocを参照していたので気にしていませんでしたが、ふと、現状整理するために俯瞰した全体のJavadocドキュメントが欲しくなりました。Lombokコンパイル時にコード生成するため、Javadoc生成時にはLombokが生成するコードがドキュメント化されません。ちなみに、onMethodのように生成したコードにアノテーション付与などするオプションではそもそもJavadoc生成時にビルドエラーになりました。

Lombokにはアノテーションからコード生成するdelombokという機能があります。つまり、delombokしてからJavadocを生成すればLombokが生成したコードのJavadoc化ができるはず。ということで試してみました。

環境

本記事のサンプルコードは以下の環境で動作確認しています。

JVM: OpenJDK Runtime Environment Corretto-11.0.12.7.2 (build 11.0.12+7-LTS)
Gradle: 6.6.1
Lombok: 1.18.12

プラグインの存在

まず初めに、gradle-lombokというプラグインが存在します。このプラグインは以下の機能を提供しており、これを使えばやりたいことは簡単にできます。

it adds the Lombok dependency to the classpath
it simplifies the Eclipse IDE installation
it offers support for delomboking
(訳)
Lombokの依存関係をクラスパスに追加
Eclipse IDEへのインストールの簡易化
delombokのサポート

しかし、以下のいくつかの理由で、このプラグインを参考に自分で書いてみることにしました。

  • すでにLombokは依存関係に追加済み
  • 開発で使用しているIDEIntellij
  • delombok以外の機能は使わなさそう
  • プラグインのコードを見るとシンプルなJavaExecタスクである
  • このプラグインは開発停止中、不具合対応のみ行う状態である

delombokタスクの実装

JavaExecタスクということは、通常のjavaコマンドによる実行をスクリプトに置き換えればうまくいきます。javaコマンドでdelombokを実行するのは以下の通りです(公式サイトから引用)。srcがdelombok対象のディレクトリ、src-delombokedがdelombok結果を格納するディレクトリです。

$ java -jar lombok.jar delombok src -d src-delomboked

これをJavaExecタスクにすると以下のように書けます。lombok.jarはすでにコンパイルのクラスパスに含まれるので、mainを直接指定しています。argsでdelombokを文字列で渡すことで、上記コマンドラインと同じ結果が得られます。
今回はプロダクションコードのJavadocだけで良いので、ソースにはsrc/main/javaを指定しました。

task delombok(type: JavaExec, dependsOn: compileJava) {
    ext.outputDir = file("$buildDir/delombok")

    classpath = sourceSets.main.compileClasspath + sourceSets.main.output.generatedSourcesDirs
    main = 'lombok.launch.Main'
    args('delombok', 'src/main/java', '-d', outputDir)

    doFirst {
        outputDir.deleteDir()
    }
}

JPAメタクラスコンパイルの依存に含んでいるので、compileJavaタスクに依存させてannotationProcesserの出力先も含めていますが、Javadoc出力のためだけであれば、これがなくともdelombokタスクでエラーが出るだけなので動作には問題ありません(気持ちの問題だけ)。

Javadocタスクの設定

次にJavadocを出力する設定です。javaプラグインをインポートしていればタスク定義はすでにあるので、設定だけ追加します。

apply plugin: 'java'

/* JavadocでJava8以降のタグを出力するためのオプション */
def tagOptions = ["apiNote:a:API Note:", "implSpec:a:Implementation Requirements:", "implNote:a:Implementation Note:"]
/* Javadoc生成時のクラスパス */
def javadocClasspath = sourceSets.main.compileClasspath + sourceSets.main.output.generatedSourcesDirs + files("$buildDir/delombok")

javadoc {
    dependsOn([compileJava, delombok])
    classpath = javadocClasspath
    source = delombok.outputDir
    options.tags = tagOptions
    failOnError = false
}

実行は以下の通りです。

$ ./gradlew javadoc

これでdelombokしたソースからJavadocを出力できました!

今回はjavadocの設定で例示しましたが、特定のクラス群だけフィルタしたい場合などは別のJavadocタスクを定義して、sourceに渡す対象を変えてあげれば可能です。

それでは良いJavadocライフを!

transformプロパティを用いたアニメーション改善

こんにちは、id:j-horikawaです。

昨今のUIはリッチ化が進みインタラクティブなデザインを実現するためアニメーションが多く使われています。

アニメーションを多用すると、コンポーネントのアニメーションがカクつく、もたつくなど処理が重くなりがちです。

今回はアニメーションのパフォーマンスに関する1つの改善方法としてDOMのレンダリングに絞った対応策を探ってみましょう。

今回改善したいアニメーション

f:id:j-horikawa:20210907121241g:plain

要素が左から右へ400px移動するだけのシンプルな実装なので実際に処理が重くなることはないのですが、処理が重くなりアニメーションがカクついているという前提でお話を進めます。

HTML

<div class="animation-cpu">CPU</div>

CSS

.animation-cpu {
  animation-name: position-element;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  animation-timing-function: ease;
  background-color: gray;
  color: white;
  width: 100px;
  height: 100px;
  position: relative;
  left: 0;
  top: 0;
}

@keyframes position-element {
  100% {
    left: 400px;
  }
}

問題箇所の調査

Chromeをお使いの場合、DevToolsのRenderingタブにあるPaint flashingを有効にしてみましょう。

Renderingタブがない場合は下ペインの左にある「︙」からRenderingを選択すると表示されます。

f:id:j-horikawa:20210907121441p:plain

Paint flashingを有効にするとページ上で再描画が発生したエリアが緑色でハイライトされ、DOMのリペイントが行われていることがわかります。

f:id:j-horikawa:20210907121540g:plain

このリペイントは要素の位置が変更されるたびにCPUが使用されペイント処理が走るため、使用箇所が多くなるとCPUの負荷が高くなっていきます。

リペイントによる処理の負荷を軽減させる

今回の左から右へ要素が移動する処理、つまりright, left, top, bottomによる位置の移動にはCPUが使用されるので、GPUを使用する transformプロパティに変更します。

HTML

<div class="animation-gpu">GPU</div>

CSS

.animation-gpu {
  animation-name: transform-css;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  animation-timing-function: ease;
  background-color: gray;
  color: white;
  width: 100px;
  height: 100px;
}

@keyframes transform-css {
    0% { transform: translate(0px); }
    100% { transform: translate(400px); }
}

要素が左から右へ400px移動する処理を transform に置き換えました。

Paint flashingを有効にした状態で比較してみると transformを使用した要素がリペイントされていないことが分かります。

f:id:j-horikawa:20210907121632g:plain

対象ブラウザの使用率によるバランスの良い設計が必要

GPUを使った処理が早いとは言え今回ご紹介した改善方法はアニメーションの描画に関する速度改善です。

GPUを使ったハードウェア・アクセラレーションを行うとメモリの使用率が高くなり端末への負荷が高くなります、改善によって新しいボトルネックが生まれないか慎重に確認を行いながらパフォーマンス改善を行うと良いでしょう。

最後に

ContractSはエンジニア・デザイナーを募集しています。 共にプロダクト作りを通して、社会・組織を良くしていきましょう! 興味がある方はぜひこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

SpringBoot+Slackアプリでメッセージ通知を実装してみる

こんにちは、id:c-terashimaです。
技術書典11で無料配布している「Holmes Tech Book」ですが、多くの方にダウンロードしていただいております!ありがとうございます!!
ドメイン駆動設計やmiroアプリの作成などバラエティに富んだ内容になっていますので、ぜひダウンロードしていただけると幸いです。

techbookfest.org

さて、今回はJava(SpringBoot)からSlackへメッセージ送信を試してみました。

Slackアプリを作る

Slackとアプリのやり取りを行うためのアプリを作成します。作成するSlackアプリからDMが届くことを想定しています。

  1. SlackAPIのアプリページに移動して、「Create New App」をクリックします f:id:c-terashima:20210719152014p:plain
  2. 「App Name」に作成するアプリ名、「Pick a workspace to develop your app in:」に使用するワークスペースを選択し、「Create App」ボタンをクリックします f:id:c-terashima:20210719152450p:plain
  3. 「OAuth&Permissions」にアプリに許可させたい権限を付与させます。今回は「chat:write」を付与します f:id:c-terashima:20210719162232p:plain
  4. 「Redirect URLs」に認証時に呼び出されるエンドポイントを指定します。後述で作成する「/slack/oauth」を指定します。SSL化されたエンドポイントを指定する必要があるのでご注意ください。(画像はダミー) f:id:c-terashima:20210720094057p:plain
  5. 「App Home」に移動して「Display Name」と「Default username」を入力します f:id:c-terashima:20210719163418p:plain
  6. 「OAuth&Permissions」の設定が完了したら、作ったアプリをワークスペースにインストールします f:id:c-terashima:20210719162650p:plain
  7. 「Basic Information」に移動して「Client ID」と「Client Secret」をメモします。この2つはホームズクラウドなどのアプリケーションからメッセージを書き込む際に必要となります

アプリとの認証

サンプルは以下の環境で動作確認をしております。

  • Java8
  • SpringBoot 2.3

Slack SDK

build.gradledependencies に以下を追加してSlack SDKをインストールします。

dependencies {
    implementation 'com.slack.api:slack-api-client:1.7.1'
}

アプリケーションとの認証

Slackユーザとアプリケーションユーザの紐付けを行います。まずは認証用のボタンを表示します。

<a href="https://slack.com/oauth/v2/authorize?client_id=「Client ID」&scope=chat:write&state=「任意のデータ」">
  <img alt="Add to Slack"
    height="40"
    width="139"
    src="https://platform.slack-edge.com/img/add_to_slack.png"
    srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" />
</a>

client_id には作成したSlackアプリの「Client ID」を指定します。
state はSlackからアプリケーションの認証エンドポイントが実行された際に設定される値になります。認証用エンドポイントの呼び元がこのアプリによって実行されたのかをチェックするのに利用します。

次にSlackから呼ばれるエンドポイントを作成します。

@RestController
@RequiredArgsConstructor
@Slf4j
public class SlackController {
    private static final String STATE = "state_id";
    private final String slackClientId = "client_id";
    private final String slackClientSecret = "client_secret";

    @GetMapping("/slack/oauth")
    @SneakyThrows
    public ResponseEntity<Void> oauth(@RequestParam("code") String code, @RequestParam("state") String state) {
        // stateの確認
        if (!Objects.equals(state, STATE)) {
            log.error("stateが一致しません。{}", state);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        try (Slack slack = Slack.getInstance()) {
            // Slackアプリとの認証
            OAuthV2AccessResponse response = slack.methods()
                    .oauthV2Access(req -> req.code(code).clientId(slackClientId).clientSecret(slackClientSecret));

            if (!response.isOk()) {
                log.error("認証に失敗しました。{}", response.getError());
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
            }

            // SlackユーザID、Slackアプリのトークンを取得
            // メッセージを送信するのに必要になるため、DBかファイルに保存しておこう
            String slackUserId = response.getAuthedUser().getId();
            String slackAccessToken = response.getAccessToken();
        }

        // 認証後のリダイレクト先を指定する
        HttpHeaders headers = new HttpHeaders();
        UriComponentsBuilder builder =
                UriComponentsBuilder.fromUri(URI.create("http://localhost:8080/slack"));
        headers.setLocation(URI.create(builder.toUriString()));

        return new ResponseEntity<>(headers, HttpStatus.MOVED_PERMANENTLY);
    }
}

「Add to Slack」ボタンをクリックすると次の画面が表示されますので、「許可をする」ボタンをクリックすることで上で作ったエンドポイントが呼び出されます。
f:id:c-terashima:20210719184234p:plain

Slackへ通知

通知するメッセージはJSON形式で指定します。(Block Kit Builder)https://app.slack.com/block-kit-builder/T3HBJHYRHを利用するとかんたんに作ることができます。
以下のメッセージを送ってみたいと思います。
f:id:c-terashima:20210719185834p:plain

認証を行ったユーザにメッセージを送るソースは次になります。

private String slackAccessToken = "accessToken";
private String slackUserId = "userId";

@SneakyThrows
public void postApprovalRequestMessage() {
    try (Slack slack = Slack.getInstance()) {
        MethodsClient client = slack.methods(slackAccessToken);

        ConversationsOpenResponse openResponse =
                client.conversationsOpen(req -> req.users(Arrays.asList(slackUserId)));
        if (!openResponse.isOk()) {
            throw new Exception("DMを開くことができませんでした");
        }

        String message =
                "[\n" +
                "    {\n" +
                "        \"type\": \"section\",\n" +
                "        \"text\": {\n" +
                "            \"type\": \"mrkdwn\",\n" +
                "            \"text\": \"こんにちは!メッセージを送るよ!!\"\n" +
                "        }\n" +
                "    }\n" +
                "]\n";


        ChatPostMessageResponse messageResponse = client.chatPostMessage(req ->
                req.channel(openResponse.getChannel().getId())
                .blocksAsString(message)
        );
        if (!messageResponse.isOk()) {
            throw new Exception("DMを送ることができませんでした." + messageResponse.getError());
        }

        ConversationsCloseResponse closeResponse = client.conversationsClose(req ->
                req.channel(openResponse.getChannel().getId()));
        if (!closeResponse.isOk()) {
            throw new Exception("DMを閉じることができませんでした");
        }
    }
}

実行するとメッセージが!!
f:id:c-terashima:20210719191910p:plain

まとめ

Slackは情報も豊富でかんたんにDMを送ることが出来ました。
Block Kitのテンプレートを見ていただくとボタンやテキストボックスをメッセージで送ることができますので、ぜひ色々試していただければ楽しいと思います。
参考にさせていただいた資料は下記に載せておきます。

参考資料


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

lab.holmescloud.com

lab.holmescloud.com