ContractS開発者ブログ

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

ArchUnitがDDDのモジュール実現にちょうど良かった話

この記事は Holmes Advent Calendar 2020 - Qiita 11 日目の記事です。


こんにちは。エンジニアの友野です。最近、Quartoにハマっており、チーム内普及に勤しんでいます。

先日、ドメイン駆動設計(以降、DDD)初学者にありがちな

  • 「これはどこに置くんだっけ?」
  • 「このクラスからドメイン層呼んでいいんだっけ?」

というような不安を払拭するために良い方法はないものかと悩んでいたところ、前回のJJUGでArchUnitを知りました。 早速プロダクト適用してみたら、結構良かったのでまとめておきます。

これから例示するコードは以下の動作環境*1で作り、動かしています。

JVM: OpenJDK Runtime Environment Corretto-8.252.09.1 (build 1.8.0_252-b09)
Gradle: 4.10.3
Kotlin: 1.3.41
Spock: spock-spring:1.3-groovy-2.4

ArchUnitとは

ArchUnitは、Java/Kotlinで書かれたアプリケーションのアーキテクチャを検証するためのライブラリです。パッケージやクラスなどの依存関係チェックができます。詳しくは公式サイトをご参照ください。

www.archunit.org

Holmesでは一部アプリケーションをKotlinで書いているので、JavaだけでなくKotlinもテストできるのは嬉しいですね。 詳細なセットアップ手順やAPIは読みやすいドキュメントが公式から提供されているので、割愛します。

www.archunit.org

Why ArchUnit

改めて整理するとやりたいことは、以下の2点です。

  • 参照関係を明確化して責務分離を進める
  • データモデルなどの既存資産とドメインモデルを分離する

パッケージ構造や参照関係のような決め事を守るために、最も簡単な始め方はレビュープロセスで担保することです。 しかし、ご存知の通り、この方法は強制力がなかったり、見逃しがあったり、適切に運用するのは難しいです。

仕組みで守る方法として、gradleでサブプロジェクト化して依存の方向をdependenciesで定義することも考えられますが、新規プロジェクトならいざ知らず、既存資産があるとこのアプローチに舵を切るのは若干尻込みしてしまいます。

そのちょうど中間として、現在の構造を大きく変えずに、かつテストコードによって比較的強い制約として表現できるArchUnitはまさに我々のような状況には最適と言えます。

セットアップ

今回対象としているアプリケーションはkotlinでプロダクションコードを書き、テストコードはgroovy(spock)で書いています。 それぞれのバージョンは冒頭で記載した通りです。 テストフレームワークにspockを採用しているので、build.gradleの依存には無印のものを追加します。

dependencies {
    // other dependencies...
    testImplementation 'com.tngtech.archunit:archunit:0.14.1'
}

spockテストクラスも書いておきます。

class ArchitectureSpec extends Specification {

    final def BASE_PACKAGE = "com.example"
    final String[] LEGACIES = [
        "..entity..",
        "..repository..",
        "..service.."]
    final def CONTROLLER = "..controller.."
    final def USE_CASE = "..usecase.."
    final def DOMAIN = "..domain.."
    final def INFRASTRUCTURE = "..infrastructure.."

    final JavaClasses importedClasses =
            new ClassFileImporter().importPackages(BASE_PACKAGE)

    // ここにテストケースを書く
}

これで準備が整いました。

守りたいアーキテクチャ

先日の記事で触れた三層+ドメインオブジェクトアーキテクチャから試行錯誤をして、今のところオニオンアーキテクチャに近い形に落ち着いています。

tech.holmescloud.com

f:id:a-tomono:20201210231738p:plain
簡略化したアーキテクチャ構成図

参照関係の明確化

ユースケース層はコントローラ層から参照され、ドメイン層はユースケース層およびインフラストラクチャ層から参照されます。

def "ユースケース層はコントローラ層からのみ呼び出される"() {
    given:
    ArchRule rule = classes()
        .that().resideInAPackage(USE_CASE)
        .should().onlyBeAccessed().byAnyPackage(CONTROLLER, USE_CASE)

    expect:
    rule.check(importedClasses)
}

def "ドメイン層はユースケース層、インフラストラクチャ層からのみ呼び出される"() {
    given:
    ArchRule rule = classes()
        .that().resideInAPackage(DOMAIN)
        .should().onlyBeAccessed().byAnyPackage(
            USE_CASE,
            DOMAIN,
            INFRASTRUCTURE,
        )

    expect:
    rule.check(importedClasses)
}

これを実行すると…、テストに失敗しました。

...
    at com.example.ArchitectureSpec.ドメイン層はユースケース層、インフラ層からのみ呼び出される(ArchitectureSpec.groovy:47)
Caused by: java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package '..domain..' should only be accessed by any package ['..usecase..', '..domain..', '..infrastructure..']' was violated (9 times):

既存資産のモデル変換は、そのモデル自身にさせていたため、インフラストラクチャ層以外から参照していることを思い出しました。 これは既存資産を活かすことを優先した結果でもあるので、広義でのインフラストラクチャとして扱います。既存資産のモデルを参照元に追加して再実行、テスト成功しました。

final def LEGACY_MODELS = "..entity.."

def "ドメイン層はユースケース層、インフラストラクチャ層からのみ呼び出される"() {
    given:
    ArchRule rule = classes()
        .that().resideInAPackage(DOMAIN)
        .should().onlyBeAccessed().byAnyPackage(
            USE_CASE,
            DOMAIN,
            INFRASTRUCTURE,
            LEGACY_MODELS, // 追加
        )

    expect:
    rule.check(importedClasses)
}

既存資産とドメインモデルの分離

新しいドメインモデルを定義しているので、既存資産に依存はさせたくありません。つい、既存サービスクラスをDIすればもっと早く実装できるのに、という誘惑に駆られますが、これはファットなドメインサービスを生み出し、結果としてドメイン貧血症を引き起こします。なによりビジネスルールが散在したままです。 これを避けるためにテストケースを追加します。

def "ドメイン層は既存資産を利用しない"() {
    given:
    ArchRule rule = noClasses()
        .that().resideInAPackage(DOMAIN)
        .should().dependOnClassesThat().resideInAnyPackage(LEGACIES)

    expect:
    rule.check(importedClasses)
}

これで最低限の労力でやりたいことは満たせたと思います。

ふりかえり

今回のテストを書いていく中で、偶然にも、コントローラがドメインオブジェクトを参照している実装を発見できました。レビュー済みの箇所ではありましたが、やはり見落としはあるようです。 テストでアーキテクチャの制約を表現できるので、構造を頭に入れつつも、個々の機能実装に集中できる感覚がつかめました。

現在はコントローラからのドメインオブジェクト参照は禁止していますが、IDクラスなど、制約を部分的に緩くするパターンも今後考えられます。これは、例えば、特定のインタフェースを実装した値オブジェクトのみ許可する、というようなテストで簡単に実現できそうです。 ただし、なぜこの制約があるのかはしっかりと共有しないとルールを守ることがゴールになり、目的と手段が入れ替わってしまう懸念もあります。

まとめ

スタートしたばかりのDDDにArchUnitで適度な制約を設けて、開発者がモデリングとそのコード表現にフォーカスできている状態に一歩近付きました。ArchUnitはユーザーガイドがよくできているので、色々読みながらプロダクト適用していこうと思います。

Holmesでは、今後もDDDを活用してプロダクトを成長させ、契約に関する顧客課題解決を通じた価値提供をしていきます。 興味がある方はご連絡下さい。

lab.holmescloud.com


明日は、同じチームでスクラムマスターをしている吾郷さんによる「スプリントレビュー改善の記録」です 。

*1:2019年に開発したサービスの環境で試しているため、バージョンは少し古いものです

目標管理制度を一年間運用する中での取り組みと学び

この記事はHolmes Advent Calendar 2020 10日目の記事です。

こんにちは。HolmesでVP of Engineeringとして開発組織の組織づくりや採用を担当している守屋です。
最近はもっぱら子供のクリスマスプレゼントに頭を悩ませています。

この記事では約一年間運用してきた評価制度、特に目標管理の運用について、実践を通して学んだことを時系列に沿ってまとめてみたいと思います。大きく分けると制度の導入をした2020/1Q(1-3月)、定着と拡大を図った2020/2Q-3Q(4-9月)、暗黙知言語化を図った2020/3Q-4Q(9-12月)に区切って記述しています。
制度運用にフォーカスしていますので、制度策定のステップについては触れていません。
アーリーステージでの評価制度と給与決定ロジックについて書かれていた、All Star SaaS Blogの金田宏之さんのウェビナー記事にインスパイアされ、このテーマを選びました。

想定される読者の方

主に開発組織での目標管理・評価制度の運用に悩まれている方はもちろん、チームや組織の方向性が揃わず力が分散してしまっているな、と課題を感じている方のちからになれればと思って書きました。

せっかちな方へ

学んだことを先にざっと書きます。

プロセスをぶらさないこと

始めたことは最後までやる。
制度の運用は挫けそうになることばかりですが、プロセスを守っていなければ公平感を感じてもらうことは決してできません。
期間や対象業務など、意思決定したスコープに対してはぶらさずにやりきることが最初の一歩だと痛感しました。

主観を磨き続けること

評価を完全に定量化し、主観が一切入らない状態にすることは、現時点においては方法が十分には確立されていないと思います。
それゆえ、一人ひとりの評価に直接関わる評価者の主観を磨き続けることが大切だとわかりました。
例えば「Aさんの等級に対して、業務Xの取り組み姿勢、最終成果は期待値に達していると判断できるか?」といった基準は、一次評価者同士での主観の磨き合いを抜きにしては納得感のある評価となりえません。

HRM(Human Resource Management)の知見を最大限取り入れる

口当たりの良さを求め、フレームワークや仕組みを導入した当初からベストプラクティスを捻じ曲げてしまうことの危険性については、システム開発に通ずるところ*1があると思います。
迷ったときには先達の知恵を惜しみなく使い、まずは型を守ることを強く意識しました。
また個人的には数年前ワークショップで同席させていただいた人事コンサルタントの坪谷邦生さんとの出会いをきっかけに、HRMの知見に触れることができました。
結果的に坪谷邦生さんの著書『図解 人材マネジメント入門 人事の基礎をゼロからおさえておきたい人のための「理論と実践」100のツボ』は、この一年間で最も繰り返し開いた本になりました。

www.amazon.co.jp

導入(2020/1Q)

この目標管理・評価制度導入以前は、全社でOKRによる目標設定を行っていましたが、評価制度は存在しませんでした。
評価制度を導入するにあたって、OKRについては、Google re:workの簡潔な定義に明記されている通り、評価ツールではありません。
目標管理と評価制度で別のツールを運用することはこの時点ではまだ現実的でないと判断し、他社の制度などを参考にしつつ、MBOの思想をベースとして目標管理・評価制度を導入しました。

制度の理解醸成と導入の背景説明

最初に取り組んだのは、個々のメンバーへの制度そのものと導入背景に対して腹落ちするまで対話をするということでした。
それまで取り組んでいたOKRの運用については、フォーマットの自由度が非常に高く、成果やコンピテンシーのバランスについてもメンバーごとに大きなばらつきが許容されている状態でした。
しかし、評価制度と一体になった目標管理制度に移行するため、目標の中で成果と各コンピテンシーそれぞれに対する目標設定が必要となり、目標設定の難易度が上がったと感じるメンバーが多くいました。
当然業務時間を圧迫する要因となるため、何のためにこの制度を導入して運用するのか、一人ひとりに理解してもらう必要がありました。
このときは一人あたり約一時間時間を確保し、制度の背景についての説明や具体的な運用イメージを膨らませてもらい、そこから目標設定に入っていきました。

最初の個人目標の設定での悩みどころ

実際に運用する目標管理のシートは、多くの企業で採用されているものと近いものでしたが、初めて経験するメンバーも多く設定に苦戦しました。
特に下記のようなポイントで悩みが多く出ました。

  • 成果目標の定量
  • 個人のビジョンと組織のビジョンがアンバランスな目標
成果目標の定量

成果目標については基本的には定量的な目標値を設定することにしています。(例外もあります)
ありがちではありますが、KPIが自己目的化してしまわないように、定量目標を設定することの意義を伝えていきました。

KPIが自己目的化した例を挙げると、例えば組織目標として「ユニットテストカバレッジをXX%に上昇させる」という目標を設定します。
この組織目標からブレイクダウンして、「個人が該当期間にコミットしたコードにより、カバレッジをxポイント向上させる」といった目標をメンバーが立てたとします。
この目標を追いかけると、業務上の重要性に関係なくテストコードの記述量を増やす方向に行動を起こしやすくなります。
本来組織目標としてテストカバレッジを設定する背景としては、コードの変更容易性を高めることや、testabilityを意識することでコードをcleanにしていきたいというモチベーションが強いと思います。
しかし単純に既存のコードへのユニットテストを増やすことだけにフォーカスしてしまうと、変更容易性が逆に下がってしまうという結果にもなりかねません。
そこで、個人目標としては「チームの作業ブランチからmasterブランチへのマージタイミング」といった意味ある変更の単位で、「CIで計測されるテストカバレッジを最低でも下げない」といった形で定量目標を設定します。
これを実現するためにTDDのスキルを習得したり、チーム内のコードレビューを行うといった行動目標を合わせて設定します。

個人のビジョンと組織のビジョンがアンバランスな目標

前出の坪谷さんの著書では、下記の通りに表現されています。

人間性の尊重と業績向上を同時実現するのが本来のMBOです*2

特に中途採用で、前職においてノルマ管理的な使われ方で目標設定を行われていた場合に、「個人のビジョンについて考えたことがなかった」というケースも少なくありません。
その場合、過去のキャリアの棚卸しやそのときに感じたことの言語化に伴走し、個人としてのビジョンを描くことから始めました。
その上で、個人のビジョンといま時点で持っているスキル、組織が個人に期待する役割を加味して目標のバランスを調整していきました。
(たまたま私の前職の上司がリクルート出身だったこともあり、慣れ親しんでいたWill Can Mustのフレームに寄っています。)

定着〜拡大(2020/2Q-3Q)

Holmesが導入した目標管理制度は評価は半年、四半期で中間評価・再設定を行う制度です。
このため、この期間には中間の目標の立て直しと下半期の目標設定が行われました。
特にこの期間では組織の人数が1.5倍に増えたこともあり、全員分のフォローを一人で行うことが難しくなりました。
そこで一次評価を担うメンバーの拡充と業務の委譲を進めていきました。

コンピテンシーの解釈についての明文化

この評価制度で定義されているコンピテンシー項目の等級に対する定義は、全社で共通の記述になっているため、開発組織のメンバーからすると実際の業務シーンを想像しにくいという課題がありました。
そこで、等級に対する定義を開発組織の業務に近づけた解釈を明文化し、目標設定前に全メンバーに共有しました。
その結果下期の目標設定では、年初の目標設定と比較して格段にスムーズに目標設定ができるようになりました。
具体的には、目標を最終的に承認しているCTOが一人あたりのメンバーとの目標設定に費やした時間が、年初の半分に短縮されました。
全員の目標設定スキルの向上と一次評価者の尽力に感謝しかありません。
特に各人の中で、評価制度のコンピテンシーの各項目への理解が醸成されてきたことが大きく寄与しました。
しかし、この時点で一次評価者同士やメンバー間で、その醸成された理解をすり合わせたり明文化する取り組みはまだできていませんでした。

暗黙知言語化(2020/3Q-4Q)

3Qから4Qにかけての中間評価、四半期目標の設定に際して、一つのチャレンジを設定しました。
本人と一次評価者で練り上げる目標の精度向上を目的に、目標の承認を行うCTOを一人あたりが割り当てられる時間の上限を設定しました。
このチャレンジを成功させるために一次評価者の共通認識づくりに取り組みました。
結果的にこのチャレンジには成功し、その後もコンピテンシーに関わる様々な暗黙知言語化することに取り組みました。
下記はその一例です。

MBO本来の意味と目的

振り返ってみると制度運用開始時点ですり合わせすべきことでしたが、その時点では考え及ばず、遅ればせながらこの時点で言語化を行いました。
過去の私自身も含め、MBOというとExcelで作られた目標管理シートをイメージされる方は非常に多いと思います。
しかしMBOを提唱したドラッカーは、Management by Objectives and Self-controlと定義しており、MBOを「目標と自己管理によるマネジメント」というマネジメントの哲学として位置づけています。
そのため、マネジメント哲学としてのMBOの共通認識を作るため、下記のように明文化し、一次評価者と認識のすり合わせを行いました。

  • MBOは目標を手がかりに自らの仕事をマネジメントできる状態にすることが目的
    • 定められた目標に照らし合わせることで、自らの成果を評価できる状態を目指す
    • self-controlに依る、各人の目標への強い動機づけが引き出されることを目指す

また、self-controlを最大限発揮するために下記のようなパターンを洗い出しました。

  • 本人が目標の設定に深く関わる
    • 一方的に課されるノルマのようなものではない
    • 会社・組織の課題を本人が腹落ちしている
  • プロセスを守る
    • 目標を達成することで評価されることが保証されている
    • 画一的なプロセスで制度が運用される
  • 身近な目標
    • 本人が確信を持って設定した目標であること
    • 面談や1on1のときだけ思い出すような目標ではないこと

一次評価者の役割

一次評価者の役割としては、下記の点にしぼりました。

  • メンバーを正しく見る
    • 事実を正確に把握する
    • 信頼関係を築く
  • 組織の未来を本気で考える
    • 未来を自分の言葉で語る
    • メンバーの疑問を払拭する
  • 主観を磨き続ける
    • 公平感を担保するために、一次評価者同士の主観のすり合わせる

おわりに

長文にお付き合いいただきありがとうございました。
これを書いているまさに今、期末の評価と来季の目標設定に組織全体で取り組んでいます。
まだまだ試行錯誤の最中ですが、約一年間で人も組織も大きく成長できるということを体感できたことが個人的に最大の学びでした。
「それ違うっしょ」というツッコミや、「めちゃわかるわー」といった共感などコメントやメッセージなど、なんらかフィードバックいただけるととても嬉しいです。

開発組織は超絶積極採用中です

ピープルマネジメントに関心があるエンジニアやデザイナの方はもちろん、「マネジメントとかマジ興味が湧かない」という方も、Holmesの目標管理・評価制度では一人ひとりの個性やなりたい姿を全力で応援しています。
少しでも興味が湧いたらぜひ一度カジュアルにお話しさせてください。

lab.holmescloud.com

lab.holmescloud.com

第2回 場のデザイン

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

引き続きファシリテーション協会のセミナーで学んだことを紹介します。 今回は、場のデザインです。会議が終わらない、みんなに発言してほしい、和やかに進めたい、厳格に伝えたいなど、会議の準備に悩んでいる方に読んでもらいたいです。

f:id:tomoya_misuda:20201206113459j:plain
場のデザイン

www.faj.or.jp

場のデザインとは

参加者が主体性を持って会議に参加できるようにすることです。そのために、事前準備として5つの項目を決めます。また、机椅子の配置なども会議によって変更します。参加者が話しやすい場を設定することが重要です。

事前に決める5つの項目

  1. 目的:何のために実施するか。

  2. 目標:会議の終了時、どうなりたいか

  3. 進め方:時間配分やワークの方法

  4. メンバーと役割:参加者と参加者の役割

  5. ルール:会議の約束

上記の5つを事前に決めて、会議を実施できるとベストです。しかし、準備に時間が取れない場合は、目標まで決めておきたいです。 スクラムは、スクラムガイドに5つの項目を決めるために必要なことが記載されてます。参考になると思います。

例としてチームのレトロスペクティブ(振り返り)を紹介します。

※レトロスペクティブ(振り返り)
1.目的:品質と効果を高める方法を計画する。
2.目標:メンバーのチームのやりたいこと、やって欲しいことを把握する
3.進め方:ドラッカー風エクササイズ
4.メンバーと役割:開発者、プロダクトオーナー、スクラムマスター 、スクラムマスター は時間、ワークの進捗を管理する
5.ルール:他者が発表している時は傾聴する

ドラッカー風エクササイズは、弊社の吾郷さんが書いた記事を参考にしました。 tech.holmescloud.com

場の雰囲気をデザインする

机と椅子の配置を決めたり、参加メンバーの緊張をほぐすためにアイスブレイクも有効です。また、WEB会議でコミュニケーションのやり方を合わせることも重要です。

机と椅子の配置は、2つのパターンを使い分けてます。基本的は、下図のパターン1を利用してます。一緒に参加していることをイメージすることができます。パターン2は、司会や進行に徹する時に利用します。机と椅子の配置で場の雰囲気を作ることもできます。

f:id:tomoya_misuda:20201206115142j:plain

WEB会議の場合は、コミュニケーションのレベルを参加者で合わせることです。参加者が多く集まっている場があると、人数が多い箇所や司会役がいる箇所がメインとなり話し合いが行われます。また、カメラがONできない場合は、全員カメラをOFFにする工夫をしてもいいと思います。やはり、face to faceの方がコミュニケーションは円滑に進みます。

まとめ

参加者が主体性を持つには、目的や目標が必要です。今、議論していることに一貫性がないと他の業務や違う話題に変化します。また、タイムボックスがうまく行かないこともあります。しっかりとした目標と成果があれば、その時間を設定することに意味があります。会議毎に5つの要素を設定して改善していこうと思います。

最後に

ブログを読んで頂き、ありがとうございます。 Holmesはエンジニア・デザイナーを募集しています。
興味がある方はぜひこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

顧客課題をプロダクトバックログに変換するプロセス-Alpha

株式会社Holmesで2020年10月よりプロダクトオーナーをしているid:w-miuchiです。

プロダクトオーナー(以下PO)としてプロダクトバックログをどういう観点、手順で作成すべきか考えました。この作成のプロセスを記載いたします。
まだまだ試行段階であることをご了承頂ければと思いますm( )m

About Product Backlog

弊社では現在、アジャイルスクラムを適用し開発を行っております。
スクラムガイドのPOの欄にはプロダクトバックログについて以下の記述があります。

プロダクトオーナーは、効果的なプロダクトバックログ管理にも責任を持つ。たとえば、
- プロダクトゴールを策定し、明示的に伝える。
- プロダクトバックログアイテムを作成し、明確に伝える。
- プロダクトバックログアイテムを並び替える。
- プロダクトバックログに透明性があり、見える化され、理解されるようにする。
https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-Japanese.pdf

ここにある「透明性」「見える化」「理解される」プロダクトバックログを作成するために、
プロセス化を行い、同じ観点で作られたプロダクトバックログとすることが目的です。

About Epic

弊社では顧客課題、解決すべき課題を記載したドキュメントをEpicと定義しています。
Epicに関しては以下も参考になるかと思います。

www.atlassian.com

Epicはプロダクトマネージャー(以下PM)が管理しておりPMからPOに伝達されています。

今回記載するのはこのEpicからプロダクトバックログに変換を行う際のプロセスになります。

Epic to Product Backlog

f:id:w-miuchi:20201207140259j:plain

User

登場人物の特定を行います。
弊社プロダクトの主な利用者は、法務部や事業部、営業部となります。どの部署のどのポジションの人が利用するか、ペルソナから抽出します。
ただし、機能に特化した改善の場合は必ずしも行う必要がない場合もあります。

f:id:w-miuchi:20201207140545j:plain

Flow

課題周辺に関して、業務や運用のフローを作成します。
アクション単位で作成したり機能単位で作成したり、Epic内容によって様々です。
全体の流れを理解するのを目的としています。
(ここでのフォーマットは決めていません) 詳細はプロダクトバックログアイテム(以下PBI)で記載します。

f:id:w-miuchi:20201207140852j:plain

Data

課題に対して関連するデータの抽出を行います。
利用者数や関連機能の利用回数などを調べ、機能に対するプライオリティ付けの参考とします。
実際には欲しいデータが必ずしも抽出できないこともあり、今後の課題になっています。

Use Case

ここまでのUser, Flow, DataからUse Caseを作成します。
Use Caseは機能ではなく、利用者の課題ベースで考えます。
この理由としては、実装に自由度を持たせることが目的です。あくまでも課題を解決することがゴールであることを意味します。

例) 「リマインド機能を付ける」ではなく「利用者の確認のために再度通知をする

実際にやってみるとUse Caseを作成する際はFlowUse Case毎に分割していくことになりました。

Image, Wireframe

また同時にImageWireframeを用意します。
例えば、以下のような粒度のWireframeになります。

f:id:w-miuchi:20201207170226j:plain

弊社のようなWebアプリケーションの場合、画面イメージが必要となります。ここではデザイナーに協力頂き作成を行います。
どこまで詳細に明示するかは都度判断していますが、あくまでも実装される画面ではなくイメージであることを重要としています。
ただし、明確にUIを改善するという顧客課題であれば詳細に作る必要もあるかと考えます。 このプロセスはFlowの作成と並行して行うこともありました。

Agreement

ここまでをMVP(Minimum Viable Product)としてPMと合意をします。
あとはMVPをPBIに分割し開発チームとリファインメントを行うという流れになります。

To close

以上が現在定義しているプロダクトバックログの作成プロセスとなります。
もちろん完成ではなく今後も迷いなくプロダクトバックログを作成できるようにプロセスを改善していきたいと考えています。
今後プロダクトオーナーを目指す方の参考になればと思います。

Spring BootでWebSocketを試す

この記事は Holmes Advent Calendar 2020 - Qiita 5 日目の記事です。


こんにちは、Holmesでサーバサイドエンジニアをしているid:c-terashimaです
12月は年末ということもありより忙しい月かと思いますが、いかがお過ごしですか?
私は技術書典10の執筆にPHPカンファレンスの当日スタッフ、子供(二人)の誕生日と慌ただしい日々を送っています。。

今回は spring-boot-starter-websocket を使ってWebSocket(STOMP)を試してみたいと思います

WebSocketとは

Web上において双方向通信を低コストで行う仕組みのことです
Webアプリケーションサーバから任意のタイミングでクライアントに情報を送信することが可能で、チャットアプリみたいに多数のクライアントにメッセージを通知する場合に双方向通信が必要になります

環境

次の環境で動作を確認しております

  • Java11
  • Gradle
  • SpringBoot 2.4.0

依存関係

build.gradleのdependenciesに spring-boot-starter-webspring-boot-starter-websocketを追加します
それ以外は必要に応じて追加してください

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

WebSocketConfig

アプリケーションでWebSocketを有効にするための構成クラスを追加します

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket")    // ①
                .setAllowedOrigins("chrome-extension://ggnhohnkfcpcanfekomdkjffnfcjnjam")  // ③
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");    // ②
    }
}

①で指定している /websocket はクライアントがコネクションを貼るためのエンドポイントで、②は通知を購読する(subscribe)ためのエンドポイントになります
クライアントは /topic を購読しておくことで、サーバからの通知を受け取ることが可能になります
今回クライアントはChrome拡張機能Apic - Complete API solution - Chrome ウェブストアを利用します
クロスオリジン対策として拡張機能のホストを③に登録します

メッセージ通知

/hello APIをコールすると購読しているユーザにメッセージを通知します

@RestController
@RequiredArgsConstructor
public class Controller {
    private final SimpMessagingTemplate simpMessagingTemplate;

    @PutMapping("/hello")
    public String hello(@RequestBody Parameter parameter) {
        String message = "hello " + parameter.name;
        simpMessagingTemplate.convertAndSend("/topic", message);   // ①
        return message;
    }

    @Data
    static class Parameter {
        private String name;
    }
}

SimpMessagingTemplate#convertAndSendメソッドで /topicを購読しているユーザにメッセージを送信しています

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

curlを実行するとMessageがリアルタイムで表示することができました
かんたんに双方向通信を実装することができましたね!


明日はスクラムマスターをしているid:misudaさんです

IntelliJでVueコンポーネントを書くときに知っておきたい機能3選

この記事は Holmes Advent Calendar 2020 - Qiita 4 日目の記事です。


ここ数日、アンプ購入をきっかけにギター熱が再燃している田中です。

JetBrains 社の提供する IntelliJ IDEA 等の製品は強力な補完を行うことができる高機能な IDE です。

言語ごとに細かなセッティングができ、痒いところに手が届くのでファンも多い IDE かと思います(私もその一人です)

一方で高機能ゆえに、「使いこなせていないなぁ」と感じる方も多いのではないでしょうか。

Holmes では Vue.js を利用しているので、IntelliJ IDEA で Vue.js を書くときに知っておくと良いことを紹介したいと思います。

前提

当たり前ですが公式の Vue プラグインは入れておきましょう!

f:id:s_tanaka:20201204094014p:plain
Preference > Pluginから入れておきましょう

Extract Vue Component を使う

あまり知られていないですが、IntelliJ の機能で単一ファイルコンポーネントの切り出しができます。

Vue.js—IntelliJ IDEA | Vue working with VueComponents extract component

  1. template 内を範囲選択
  2. Alt + Enter or 右クリック →Refactoring
  3. Extract Vue Component

とすることで選択した範囲の html、関連する要素を別ファイルに切り出してくれます。

f:id:s_tanaka:20201204094821g:plain
ListをVueComponentに切り出している

なんという神機能!

優秀なポイントとして、関連する script 内の data や computed を良い感じに props で受け取れるようなコンポーネントにしてくれます。

更に、style タグの中身が pure な css の場合、要素の class や id の style ごと移植してくれます(これだけのために SCSS での BEM 記法を辞めても良いと思えるほど…)

ロジックの実装に熱中していたり、リファクタリングの時間が無いと、ついついモノリシックなコンポーネントが出来上がりがちです。

コンポーネント指向のフレームワークにおいては

  • 責務
  • 再利用性
  • テスト容易性

の観点からコンポーネントは小さくしてなんぼです。ガンガンコンポーネントを切り出しましょう!

vcomputed 等の LiveTemplate を使う

LiveTemplate や Postfix completion をよく使う方はご存知かもしれません。

computed: { 
  someComputedValue() { }
}

や、

data() {
   return { }
}

等は、Vue において頻出イディオムと言えます。

IntelliJ ではこれらの入力を補助する LiveTemplate があります。

  1. vcomputed or vdata と入力する
  2. Tab or Enter

で上記のイディオムを挿入することができます。

f:id:s_tanaka:20201204095025g:plain
vdataでdata optionを挿入

他にもテンプレートや Vuex 用の LiveTemplate があるので眺めてみると良いと思います。

余談ですが、LiveTemplate は頻出イディオム集なので、初めて触る言語の LiveTemplate を眺めると言語のコツがつかみやすいです。

f:id:s_tanaka:20201204095123p:plain
LiveTemplateの編集画面

IntelliJ 上でフロントエンドのデバッグをする

通常フロントエンドのデバッグ、簡単なものであれば console.log 、入り組んだものになると ChromeDevToolVueDevTool で行うことが多いです。

ですが、可能であればエディタ上でブレークポイントを貼ることができれば直感的だと思います。

IntelliJ では npm script を Debug 実行することでフロントエンドのデバッグIntelliJ 上で行うことができます。

  1. 開発モードが有効になっている Vue の devserver 起動コマンド or それを登録した npm script を IntelliJ 上でデバッグ実行
  2. デバッグ実行でターミナル上に表示される localhost の URL を Shift + Cmd を押しながらクリック
  3. IntelliJ のデバッガーと接続している Chrome が立ち上がるので、IntelliJ 上でお好みの位置にブレークポイントを貼る

Vue.js—IntelliJ IDEA | Vue running and debugging

これで IntelliJ 上から出ることなくデバッグができますね。

ただし、Nuxt 固有の SSR でのみ実行されるライフサイクル(asyncData や fetch)ではブレークポイントに引っかかりませんでした。

SSR では設定にひと手間いるようです。

Nuxt.js Debugging with WebStorm. Get Nuxt.js debugging up and running… | by Fernando Alvarez | Medium

まとめ

これらの機能を使いこなせるようになると実装スピードがグンと上がると思います。

特に、 Extract Vue Component は億劫になりがちなリファクタリングを助けてくれる神機能だと思います。ガンガン使って効率化していきましょう!!

スクラムチームに振り返り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