ContractS開発者ブログ

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

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

Nuxt.js上でFroala Editorを動作させる方法

Holmesでエンジニアをしている三澤です。

弊社のサービスであるホームズクラウドには、契約書のテンプレートを自由に作成することができる機能があります。この機能を実現する上で、軽量なJavaScript リッチテキストエディターのFroala Editor(公式サイト)を利用しています。Froala Editorは、vue.js版も提供されており、vue.jsで動作するサンプルはあるのですが、Nuxt.js上で動作するサンプルが見当たらなかったので忘備録として残したいと思います。

本投稿では、まず①Froala Editorを表示させます。その後、②独自カスタマイズボタンを追加させたいと思います。 ポイントは、FroalaEditorをプラグイン化して呼び出すこと、独自カスタマイズボタンを追加する場合、DOM操作が行われる処理を実行するタイミングに注意することです。

環境

  • macOS Catalina
  • node v12.16.1
  • npm 6.13.4

前提

create-nuxt-appでNuxt.jsの環境構築済み(SSRモード(※サーバーサイドレンダリングモード)、すべてデフォルト設定)

インストール

Froala Editor

npm install vue-froala-wysiwyg

※本記事作成時にインストールされたバージョンは3.1.0でした。

実装

まずFroala Editorを表示させることを実現したいと思います。

Froala Editorをプラグイン

続いてpluginsディレクトリ内に下記のファイルを作成します。 このファイルと、次項のnuxt.config.jsの編集によってどのコンポーネントからもFroala Editorを呼び出すことができるようになります。

  • ファイル名:froala.js

  • 場所:plugins/froala.js

//froala本体インポート
import 'froala-editor/js/plugins.pkgd.min.js';
//サードパーティプラグインインポート
import 'froala-editor/js/third_party/embedly.min';
import 'froala-editor/js/third_party/font_awesome.min';
import 'froala-editor/js/third_party/spell_checker.min';
import 'froala-editor/js/third_party/image_tui.min';
//cssインポート
import 'froala-editor/css/froala_editor.pkgd.min.css';
 
import Vue from 'vue'
import VueFroala from 'vue-froala-wysiwyg'
import 'froala-editor/js/froala_editor.pkgd.min.js'
Vue.use(VueFroala)
Vue.config.productionTip = false

nuxt.config.jsの編集

前項でプラグイン化したFroala Editorを呼び出せるようにします。 併せてjqueryを使用できるようにします。

//省略
/*
** Plugins to load before mounting the App
*/
plugins: [
  { src: '~/plugins/froala.js', ssr: false } //追加
],
//省略

これで準備は完了です。 これからFroala Editorを呼び出すコンポーネントを作成していきます。

Froala Editorを呼び出すコンポーネントを作成

Froala Editorはコンポーネント化しなくても、ページ内に直接タグを記述してもいいのですが、エディターのヘッダー定義や独自カスタマイズボタンをする場合、複数の箇所で記述するのはよろしくないのでコンポーネント化し、それをページから呼び出したいと思います。

ここで行っている処理の概要は下記の通りです。

  1. templateタグ内で、froalaと内部変数をバインディング

  2. propsで初期表示する文字列を親コンポーネントから受け取る

  3. data定義でFroala Editorのヘッダーボタンの定義やプレイスホルダー、イベントハンドラの定義

  4. created処理でFroala Editorで入力した文字列を内部で保持するモデルにpropsで受け取った初期表示の文字列を設定

<template>
  <div>
      <froala id="edit" :tag="'textarea'" :config="config" v-model="content"></froala>
  </div>
</template>

<script>

export default {
  props: {
    initContent: String
  },
  data () {
    return {
      config: {
        toolbarButtons: {
          'moreText': {
            'buttons': 
                 [
                     'bold', 'italic', 'fontSize','underline', 
                     'strikeThrough', 'textColor', 'backgroundColor',
                     'inlineClass','inlineStyle','clearFormatting'
                 ],
            'buttonsVisible': 3
          },
          'moreParagraph': {
            'buttons': 
                 [
                     'alignLeft', 'alignCenter','alignRight', 
                     'alignJustify', 'formatOLSimple', 'formatOL', 
                     'formatUL', 'lineHeight', 'outdent', 
                     'indent', 'quote'
                 ],
            'buttonsVisible': 3
          },
          'moreRich': {
            'buttons': ['insertImage', 'insertTable',
                              'specialCharacters', 'insertHR'],
            'buttonsVisible': 3
          },
          'moreMisc': {
            'buttons': ['undo', 'redo','insert'],
            'buttonsVisible': 3
          }
        },
        placeholderText: '入力してください',
        events: {
          initialized: function () {
            console.log('initialized')
          },
          input: function () {
            console.log('inputed')
          }
        }
      },
      content: null
    }
  },
  created : function () {
      this.content = this.initContent
  },
}
</script>

上記で作成したコンポーネントを呼び出すページを作成

先ほど作成したFroalaView.vueを呼び出すページを作成します。

  • ファイル名:index.vue

  • 場所:pages/index.vue

ここで行っている処理の概要は下記の通りです。

  1. templateタグ内で、FroalaViewタグにpropsとして初期表示文字列を渡す

  2. import構文で先ほど作成したコンポーネントを読み込む

  3. componentsプロパティでFroalaViewコンポーネント使用を宣言

  4. dataでエディターに渡す初期表示文字列を定義

<template>
  <div class="container">
    <div class="editor">
      <FroalaView :initContent="editorContent"></FroalaView>
    </div>
  </div>
</template>

<script>
import Vue from 'vue'
import FroalaView from '~/components/FroalaView'

export default {
  components: {
    FroalaView
  },
  data: () => ({
      editorContent: 
            'こんにちは、契約マネジメントシステム「ホームズクラウド」の開発者ブログです。'
  })
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
.editor {
  width: 800px;
}
</style>

動作確認(Froala Editorが表示されること)

アプリケーションを実行し、localhost:3000にアクセスして確認します。

f:id:a-misawa:20200927205405p:plain

Froala Editorが表示されました。続いて、エディターのヘッダーに独自カスタマイズボタンを追加したいと思います。今回はFroala Editorのヘッダーにマークボタンを追加し、クリックすると、spanタグを挿入する、ということを実現したいと思います。

独自カスタマイズボタンを追加

カスタマイズボタンを追加する場合、Froala Editorを呼び出したコンポーネントに処置を追加します。

// 省略 created()の直下に、下記コードを追加
mounted : function () {
  const FroalaEditor = require ('froala-editor')
  FroalaEditor.DefineIcon('insert', {NAME: 'plus', SVG_KEY: 'add'});
  FroalaEditor.RegisterCommand('insert', {
    title: '要素を追加',
    focus: true,
    undo: true,
    refreshAfterCallback: true,
    callback: function () {
      this.html.insert('<span class="contractor-seal">挿入された要素</span>');
    }
  });
}
// 省略 最下行に下記コードを追加
<style>
  .contractor-seal {
    color : red;
    width: 65px;
    height: 65px;
    border: 1px solid red;
    border-radius: 4px;
    text-align: center;
  }
</style>

※注意点として、Nuxt.jsプロジェクト作成時にSSRモードで作成した場合、サーバー内でデータを取得してレンダリングしてくれます。しかしFroala Editorに独自のカスタムボタンを追加する場合、Frola Editor内でDOM操作が行われており、サーバーサイドで実行されると内部エラーが発生します。それを回避するためにFroala Editorの処理を実行するのはmounted(DOM作成後=クライアントサイド)にて行っています。

動作確認(独自カスタマイズボタンが追加され、ボタンが機能すること)

カーソル位置にタグを挿入されました

独自カスタマイズボタンが追加され、クリックすると正しくカーソル位置にタグが挿入されることが確認できました。

所感

このように簡単にNuxt.jsプロジェクトでFroala Editorを起動することができることがわかりました。注意点として、Vue.js(Nuxt.jsでも)でDOM操作を行うことは本来あまり勧められていませんがどうしても必要な時もあります(どうしても以前開発したjQueryで記述された処理を移行させたい、jQueryで動作するプラグインを動かしたいなど)。その場合、SSR環境の場合はDOM操作が行われるタイミングに注意する必要があります。

以上、Nuxt.js上でFroala Editorを動作させる忘備録でした。

最後に

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

lab.holmescloud.com

lab.holmescloud.com

SpringBootのアノテーションを使う際につまずいたポイント

Holmesの倉島です。サーバーサイドの実装をしている最中に度々存在する、@(アノテーション)とそれに関連するエラー に頭を悩ませています。今回はそんな一例を紹介しようと思います。

@RestController

とあるControllerクラスに@RestControllerをつけたところ、実行時にエラーになってしまった!

新たにパッケージ内にControllerクラスを作成し、@RestControllerをつけたところテスト実行時に「ApplicationContextが読めない!」という旨のエラーが出てしまった。

java.lang.IllegalStateException: Failed to load ApplicationContext


結論を述べると、どうも同じ名前のControllerクラスが別パッケージ内に存在しているのでエラーになっていたらしい。成程それならばどちらかのControllerクラスの名前を変更すれば解決できそうです!余談として、どうしてもControllerクラスの名前を変更したくない場合は@RestControllerと同時に@Bean(name="hoge”)を付けても解決できるようです。

// before
@RestController

// after
@Bean(name="hoge")
@RestController

("hoge"の部分がSpring内での管理名になる。ちなみに"hoge"の部分は他のControllerクラス名と被っては同じ轍を踏むことになるので注意)

@SpringBootApplication

Entityクラスを、今まで保存している場所とは別のパッケージに作成したが、何やらエラーになってしまった...。

こちらも実行時にエラーになってしまった。

Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat

私はどうしても別パッケージにEntityクラスを作成したいのです!
解決法を求め調べを進めていくと、@SpringBootApplicationが付いているクラスにEntityクラスを保存すべきパッケージが設定されているらしい。
私はJPAを使用しているので、@SpringBootApplicationが付いているクラスにはEntityManagerFactoryが作成されるクラス名が指定してありました。そのクラスに念願のパッケージ指定箇所がありました!あとは、setPackagesToScanでパッケージのパスを指定して、晴れてEntityクラスを別パッケージに保存することができました。(ちなみに、repositoryクラスの保存すべきパッケージも@SpringBootApplicationが付いているクラスで指定できる)

@NamedQuery

Entityクラスの先頭部に記述されていた。エラーは出ていないが一体どういう意図で使用されているのだろう?

Entityクラスに、@Entityや@Data(lombok)と同じように@NamedQueryが@NamedQuery(name = "hogeTable.findAll", query = "SELECT h FROM hogeTable h") このように指定されていた。

@Entity
@Data
@NamedQuery(name = "hogeTable.findAll", query = "SELECT h FROM hogeTable h")
public class hogeTable { 

どうも、nameに設定されている名前を指定することによりqueryに設定されているsql文を実行することができるらしい。 別途Repository(jpa)を作成していた場合は、上記の例だと hogeTbaleRepository.findAll を指定することにより、SELECT h FROM hogeTable h の結果を取得することができる。(Repositoryを作成しない場合の指定方法は、EntityManagerクラスのcreateNamedQueryを使用する。EntityManager.createNamedQuery("hogeTable.findAll")のように指定する)

おわりに(感想)

結論(感想)としては、@(アノテーション)は理解した上で使用するのであれば面倒なことを考えなくていいので便利!と感じる反面、一見すると意味がわかりづらく保守性が下がる可能性があるのでは?という面もありました。過ぎた力は身を滅ぼしますね。
なんとなく表面だけ理解した状態で、言葉足らずの説明でしたが、参考になれば幸いです。

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

lab.holmescloud.com

lab.holmescloud.com

gilotによるGitログ解析を、複数Gitリポジトリに対して試してみた

Holmesでエンジニアをしている山本です。

gilot (ジロー)の紹介記事を見て、複数チーム・複数Gitリポジトリで開発している弊社ではどのような結果が得られるかと思い、試してみました。

qiita.com

実行環境

名前 バージョン
OS Windows 10 Pro 64bit バージョン2004
Python 3.8.5
Git 2.28.0.windows.1
gilot 0.2.4

参考

Qiitaの記事gilotのGitHubリポジトリ、また以下の記事を参考にしました。

解析対象

解析対象とするリポジトリについて

現在、弊社では、複数のGitリポジトリに対して複数チームが開発を行っています。

今回は、Javaによるバックエンド、Vue.jsによるフロントエンドを対象とし、それぞれ名前を backend, frontend とします。対象とするブランチは、いずれもmasterです。

以下のディレクトリ構造にて、 root ディレクトリをカレントディレクトリとしてコマンドを実行していきます。

root/
  ├ backend/
  └ frontend/

hotspotやhotgraphで対象とするファイルパスについて

hotspotやhotgraphでは、ユニットテストなどのファイルを含めると結果が正しく取れないため、対象とするファイルパスを指定します。

backendは、プロジェクト管理ツールとしてGradleを使用したSpring Bootアプリケーションのため、解析時には --allow-files 'src/main/*' を指定します。

frontendは、複数パッケージを packages/* 配下で管理しており、その中にStorybookやJestのテストファイルが含まれるため、解析時には --allow-files 'packages/*' --ignore-files '*/__tests__/*' '*.spec.ts' '*.stories.js' を指定します。

gilotによる解析

インストール

GitとPython3はインストール済みのため、 pip install gilot でインストールできました。

gilotのインストールに成功すると、バージョンが表示されます。実行環境にも記載しましたが、記事作成時点でのバージョンは0.2.4です。

Installing collected packages: gilot
Successfully installed gilot-0.2.4

また、 author コマンドによるグラフ描画に使用されるということで、IPAゴシックフォントをインストールしておきます。

IPAフォントのダウンロード

log

まずはログの取得を行います。 hotspothotgraph では --full オプションをつけて出力しておく必要があります。

後の解析コマンドにファイルを渡しやすいよう、出力ファイルにはプレフィックスとして log- を付与しておきます。また、frontendの開発が始まったのが2020年6月のため、 --since 2020-06-01 で開始日を指定します。

gilot log --full -b master -o log-backend.csv --since 2020-06-01 --full backend/
gilot log --full -b master -o log-frontend.csv --since 2020-06-01 --full frontend/

backendは約1,300行の出力に1分ほど、frontendは約2,100行の出力に2分ほどかかりました。

出力にはマージコミットも含まれますが、除外せずそのまま解析を行います。

plot

plot コマンドにて、グラフ生成を行います。

開発手法としてスクラムを採用しており、スプリント期間は各チーム1週間となっているため、解析時のタイムスロットを -t 1w で1週間とします。

backendとfrontend、両方合わせたものと個別で3種類出力してみます。

gilot plot -i log-*.csv -t 1w -o plot-all.png
gilot plot -i log-backend.csv -t 1w -o plot-backend.png
gilot plot -i log-frontend.csv -t 1w -o plot-frontend.png

それぞれの結果は、以下のようになりました。

f:id:h-yamamoto_holmescloud:20200904162547p:plain
plot-all

f:id:h-yamamoto_holmescloud:20200904162603p:plain
plot-backend

f:id:h-yamamoto_holmescloud:20200904162620p:plain
plot-frontend

frontendは6月から開発を開始したため、環境構築時のコミット行数が多く、かなり上振れしたグラフになっておりました。出力期間の変更が必要そうです。

アウトプットの落ち込みがある部分は、お盆休みの週となります。

hotspot

hotspot コマンドにて、変更頻度の高いファイルのランキングを出力します。ユニットテスト関連ファイルなどを除外するため、--allow-files および --ignore-files を指定しております。

gilot hotspot -i log-backend.csv \
  --allow-files 'src/main/*'
gilot hotspot -i log-frontend.csv \
  --allow-files 'packages/*' \
  --ignore-files '*/__tests__/*' '*.spec.ts' '*.stories.js'

# 出力結果
------------------------------------------------------------
    gilot hotspot ( https://github.com/hirokidaichi/gilot )
------------------------------------------------------------

 hotspot  commits  authors file_name
   19.57       57        5 src/main/java/<path_to_file>
   17.71       67        5 src/main/java/<path_to_file>

また、 --csv -o 出力先ファイルパスCSV出力が可能です。この場合、除外していないすべてのファイルについて、解析結果が出力されます。

gilot hotspot -i log-backend.csv --csv -o hotspot-backend.csv \
  --allow-files 'src/main/*'
gilot hotspot -i log-frontend.csv --csv -o hotspot-frontend.csv \
  --allow-files 'packages/*' \
  --ignore-files '*/__tests__/*' '*.spec.ts' '*.stories.js'

以下のようなCSVが出力されました。--csv を付与せず実行した場合より、項目が増えています。

file_name,hotspot,commits,authors,edit_rate,lines
src/main/java/<path_to_file>,19.56934308077001,57,5,0.7472647702407003,1828
src/main/java/<path_to_file>,17.703904086980828,67,5,0.6457142857142857,1050

hotgraph

hotgraph コマンドにて、同時に変更されやすいファイルのネットワーク図を出力します。こちらも hotspot と同様、ユニットテスト関連ファイルなどを除外するため、--allow-files および --ignore-files を指定しております。

また、Windows環境では --stop-retry を付与しないとエラーが発生するため、オプションを付与しております。

gilot hotgraph -i log-backend.csv --stop-retry -o hotgraph-backend.png \
  --allow-files 'src/main/*'
gilot hotgraph -i log-frontend.csv --stop-retry -o hotgraph-frontend.png \
  --allow-files 'packages/*' \
  --ignore-files '*/__tests__/*' '*.spec.ts' '*.stories.js'

それぞれの結果は、以下のようになりました。(ファイル名が含まれるため、ぼかしをかけています)

f:id:h-yamamoto_holmescloud:20200904172917p:plain
hotgraph-backend

f:id:h-yamamoto_holmescloud:20200904172938p:plain
hotgraph-frontend

同時に変更されやすいファイルが、同じ色の円で表示されます。

author

author コマンドにて、コミットした人ごとのコミット数や、割合を出力します。

デフォルトではトップ10人が出力され、それ以外の人は「Others」としてまとめられます。人数は -t 人数 で指定可能です。

-n 名前 で、出力画像右上にタイトルを追加できます。

plot と同様、backendとfrontend、両方合わせたものの3種類を出力してみます。

gilot author -i log-*.csv -o author-all.png -n all
gilot author -i log-backend.csv -o author-backend.png -n backend
gilot author -i log-frontend.csv -o author-frontend.png -n frontend

それぞれの結果は、以下のようになりました。(個人名が含まれるため、私とOthers以外はぼかしをかけています)

f:id:h-yamamoto_holmescloud:20200904175731p:plain
author-all

f:id:h-yamamoto_holmescloud:20200904175757p:plain
author-backend

f:id:h-yamamoto_holmescloud:20200904175831p:plain
author-frontend

info

info コマンドにて、各コマンドの解析元となる情報を、JSONで出力できます。

plot と同様、 -t オプションを指定できますが、gilot v0.2.4 では無視されてデフォルトの2週間で出力されるようです。

-o オプションは指定できないため、ファイル出力する場合はリダイレクトします。

gilot info -i log-*.csv -t 1w > info-all.json
gilot info -i log-backend.csv -t 1w > info-backend.json
gilot info -i log-frontend.csv -t 1w > info-frontend.json

以下のJSONが出力されます。タイムスロットが2週間になっています。

{
    "gini": 0.23784183100679956,
    "output": {
        "lines": 236045,
        "added": 62121,
        "refactor": 0.7368256052871274
    },
    "since": "2020-06-02T09:01:06.000000000",
    "until": "2020-09-03T17:01:01.000000000",
    "timeslot": "2 Weeks",
    // 以下省略

振り返り

それぞれの解析処理を実行してみました。いずれの解析も、 gilot log でログ出力しておけば後から実行できるため、CIと連携させて出力するのは難しくないと思います。

以下所感です。

plot

スポットで出力するだけでは、あまりメリットを感じませんでした。

1か月などの期間で定期的に出力し、大きな変化があれば原因を特定、必要があれば改善するなど、実際のプロダクト開発に利用することで意味が出てくるのではないかと思います。

(根本的なところを言うと、それぞれのグラフの意味をきちんと把握していないので、まずはそこから学習しないとです...)

hotspot, hotgraph

これらはファイル間の関連性、依存性を確認することができるので、スポットでも有用だと思います。

静的解析ツールなどによる複雑性の確認と組み合わせることで、リファクタリングの優先度が高いファイルの発見に使えそうだと思いました。

author

これはそのままコミット数になります。

見ていると面白いですが、同じチーム内でも各人のコミット粒度が同じとなることはなかなかないと思うので、単体では何かの指標として使いにくいかと思います。

info

それぞれの解析結果の元となる情報のため、より高度な利用方法になるかと思います。

ツールの習熟が進み、いずれかの指標を画像ではなくデータとして扱いたいときに、こうしたコマンドがあるのはありがたいと思います。

期間指定変更後の再plot

frontendから抽出する期間を2020/6/15以降に変更して plot してみたところ、コミット行数の上振れが解消されました。

backendも合わせて期間を変更してみます。

gilot log --full -b master -o log-backend.csv --since 2020-06-15 --full backend/
gilot log --full -b master -o log-frontend.csv --since 2020-06-15 --full frontend/

gilot plot -i log-*.csv -t 1w -o plot-all.png
gilot plot -i log-backend.csv -t 1w -o plot-backend.png
gilot plot -i log-frontend.csv -t 1w -o plot-frontend.png

結果は以下のようになりました。コミット行数の上振れがなくなり、グラフの上限値が適切な値になっています。

f:id:h-yamamoto_holmescloud:20200904215634p:plain
plot-all(期間変更後)

f:id:h-yamamoto_holmescloud:20200904215711p:plain
plot-backend(期間変更後)

f:id:h-yamamoto_holmescloud:20200904204505p:plain
plot-frontend(期間変更後)

最後に

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

lab.holmescloud.com

lab.holmescloud.com

課題解決のためにスキルマップ(星取表)を作成

はじめまして、Holmesでスクラムマスターの役割を担っているid:tomoya_misudaです。

今回は、スキルマップ(星取表)を実施したことをお伝えしたいと考えます。

組織が抱えていた課題

企業、組織としての成長過程で、組織を構成するメンバーには一定の流動性が伴います。
こうした中で、現時点で組織として足りないスキルが何か、について考えたことがきっかけでした。

  • チームで足りていない知識ってあるのか?
  • 自分たちに必要なスキルってなんだろう?
  • 他のメンバーのスキル、やりたいことはなんだろう?

問題を深く捉えるため、上記の問を自分自信に投げかけたことで、「メンバーのこと知らなかったな」という感想を持ちました。
特に、普段一緒に仕事をしているチーム以外のメンバーについては、わからないことだらけでした。

解決への模索

市谷さんカイゼンジャーニー、チームジャーニーを読むことで解決方法を探すことにしました。

www.amazon.co.jp

www.amazon.co.jp

チームジャーニーを読んでいる際、「チーム・ジャーニー 著者による本読みの会」に出席しました。市谷さんがスキルマップ(星取表)の良さを共有してくれました。私は「これだ」と感じました。 そこからは、本当に「スキルマップが自身のチームに必要か」を考察しました。

やると決めて、また自問自答

やると決めて、また自問自答で意味、実施方法、確認しました。

  • 「スキルマップをやる意味は?」
    → 課題のスキルの足りない箇所確認よりは、メンバーことを知ることで、コミュニケーションの向上とモチベーション維持が大切と設定しました。また、目標決めに役に立てばと考察しました。

  • 「スキルマップをやることの注意点はあるのか?」
    → メンバーには、素直に記載してもらうことを伝える必要がある。評価利用することもできるが、今回はメンバーの興味や自身が得意なことを見える化しようと決めました。

  • 「どんなスキルがあるだろうか?」
    → Holmesで利用しているスキルをメインで抽出して、Development、Operations、マネイジメント、プロダクトオーナー色々な観点からスキルを一覧化しました。

  • 「メンバー評価のスキル段階は?」
    → 各メンバーの評価軸を合わせることは、やらないことを決めました。結果、各自の自己評価で「◎:好き、◯:一人でできる、△:助けがあればできる、空欄:できない」回答してもらいました。ここは、メンバーに意見を求めて一番工夫しました。

スキルマップを作成

スキルマップを作成して、開発部のメンバーを招待して30分のタイムボックスで実施しました。アジェンダは下記の通りです。

  • やる目的
  • 記載方法
  • 全員で記載
  • まとめ

忙しい中、全員参加してくれました!

結果

メンバー間のコミュニケーションを手助けできました。「ここを得意としているんだ!」「憧れているメンバーのスキルはこんな感じなんだ!」色々な観点を生み出すことができました。また、採用へのスキル確認ができました。

まとめ

問題に対して解決方法は色々ある中で、今自身のチームに必要か?を自問自答することはとても大切で、私たちに合う形を考え実行しました。
スキルは、それぞれのチームで違うので合うかわかりませんが、スキルの項目を下記に記載します。
少しでもチームのコミュニケーションや目標共有の手助けができればと思います。

デザイン     
        UX設計
        UI設計
        User Story Map
        Mindmap
        ビジネスプロセスモデリング
        ワイヤフレーム
        画面設計
        アイコン作成
フロント        
        Storybook
        HTML
        CSS
        SASS
        Vue.js
        Nuxt.js
        Jest
        JavaScript
        JQuery
        Webpack
サーバー        
        Gradle
        Spring Boot
        Java
        Kotlin
        Python
        Groovy
        SQL
考え方       
        Atomic Design
        テスト駆動開発
        ドメイン駆動開発
        オブジェクト指向
        DB設計
        Git-flow
        スクラム
リリース        
    リリース手順検討作成  
        ECS リリース
        Elasticbeanstalk リリース
        Elasticseach リリース
        API Gateway
        RDS リリース
        コードリリース
テスト       
    テスト設計 
        サーバーサイドテスト設計
        フロントテスト設計
        負荷テスト
    テスト自動化  
        テストシナリオ作成
        python selenium
        selenium IDE
インフラ        
    AWS 構築、保守、設計    
        EC2 / ELB
        RDS for mysql
        ECS / ECR
        MQ
        Elasticbeanstalk
        Lambda
        Amazon Connect
        S3
        API Gateway
        Cloudfront
        Elasticsearch
        SES
        Route53
        Lightsail
    gitlab 運用保守 
        パイプライン作成
        unittest runner サーバーチューニング
        unittest Docker Image メンテナンス
    その他   
        apache / tomcat の middleware 構築、保守、設計
サポート・トラブルシュート     
    監視  
        Cloudwatch alarm / Lambda / SNS / Slack による監視構築
        ログ確認
        RDS SQLチューニング
プロダクト     
        仕様理解
        タスク出し
        デザインチェック
        コードレビュー
        バグ(障害)調査対応
その他       
        ファシリテーション
        1on1
        KPI
        PDCA
        セキュリティ
        ISMS

スキルマップを定期更新することで、コミュニケーション、メンバーの成長を実感できるチームを目指していきたいです。

おわりに

この記事で、チームのコミュニケーション、モチベーション維持に貢献できたらと考えます。 Holmesはエンジニア・デザイナーを募集しています 興味がある方はぜひこちらからご連絡ください!

lab.holmescloud.com

lab.holmescloud.com

REST APIのレスポンスステータスについて改めて考える

こんにちは。7月にHolmesにジョインした友野です。サーバーサイドエンジニアをしています。

今回はREST API実装時のHTTPレスポンスステータスについてです。
基本的なことですが、重要なことなので備忘録もかねて記事にしておきます。

余談ですが、Holmesでは秋口のUI/UXリニューアルに向けて開発を進めています。ゴールに向けて、複数のスクラムチームが並行して開発をしているため、各々のチームで経験に基づいた暗黙知による実装差分が稀に発生します。
都度共有もしていますが、エンジニアが増えてきた今だからこそ、「ちゃんと決めなきゃね」という話から今回の内容も改めてまとめておこうと思い立ちました。

利用するHTTPメソッド

GETPOSTPUTDELETE の4つです。リソースの更新はPUTで行い、PATCH は使いません。 べき等性を意識するという側面もありますが、なによりシンプルに実装を進めるためです。

レスポンスステータス

チームで話をするきっかけになったのが、このレスポンスステータスです。
正常時、GET200 OKPOST201 Createdを返すという認識は合っていたのですが、 PUTAPIを実装する際に、

  • 200 OKを返す
  • 204 No Contentを返す

の2つに意見が分かれました。 宗教戦争、とは言いませんが、様々なバックグラウンドを持つメンバーから成るチームだからこそあり得ることです。

定義がどうなっていたか、改めてRFCを確認してみます。

RFC7231 PUT *1

If the target resource does have a current representation and that representation is successfully modified in accordance with the state of the enclosed representation, then the origin server MUST send either a 200 (OK) or a 204 (No Content) response to indicate successful completion of the request.

リクエストが正常に完了したことを200 OK204 No Contentのいずれかで示さねばならないとあるのみでした。 これを踏まえて、開発チームではPUTで既存のリソースを更新する場合は、204 No Contentを返すと取り決めました。

200 OKではなく、204 No Contentを選択した理由は、特に返却する情報はないと考えたためです。 リソースが必要な場合は改めて取得し直すべき、と判断しました。
なお、リソースを作成する場合は明示的にPOSTを利用するので、PUTによるリソース新規作成はしません。

レスポンスヘッダー/レスポンスボディ

さて、そうするとレスポンスヘッダーとレスポンスボディについても明らかにしておいた方が良さそうです。 こちらもRFCを念のため確認した上で、特に意見も分かれなかったので以下としました。

RFC7231 POST
RFC7231 DELETE

  • 201 Createdを返却する場合
    • Locationヘッダーに作成したリソースのURIを合わせて返却する
    • レスポンスボディは不要
  • 204 No Contentを返却する場合
    • レスポンスボディは不要

まとめると、以下の通りです。

HTTPメソッド レスポンスステータス 備考
GET 200 OK 単一/複数リソース取得に利用する
POST 201 Created Location ヘッダーに作成したリソースのURIをセットする
レスポンスボディは不要
200 OK 複雑/組合せの条件などクエリパラメータではリクエスURIの文字数に懸念がある場合*2にリソース取得(検索)時にも POST を利用する
PUT 204 No Content 既存のリソースを更新する
レスポンスボディは不要
DELETE 204 No Content 既存のリソースを削除する
レスポンスボディは不要

Springによる実装イメージ

レスポンスボディを持たない実装イメージは以下の通りです。

  • 201 Created
@PostMapping("/api/tasks")
public ResponseEntity<Void> createTask(@RequestBody CreateTaskCommand command) {
    String taskId = createTaskService.handle(command);
    URI location = UriComponentsBuilder.fromUriString("http://example.com/api/tasks/" + taskId).build().toUri();
    return ResponseEntity.created(location).build();
}
  • 204 No Content
@DeleteMapping("/api/tasks/{taskId}")
public ResponseEntity<Void> deleteTask(@PathVariable("taskId") String taskId) {
    deleteTaskService.deleteWith(taskId);
    return ResponseEntity.noContent().build();
}

最後に

当たり前だと思っていたことが、わずかな思い違いが事故につながることも少なくありません。このような事故を未然に防ぐために、少しずつ明文化していきたいと思います。

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

lab.holmescloud.com

*1:当該RFCは執筆日現在で"PROPOSED STANDARD"ステータスです

*2:RFCでは最大文字数は定義されていないようですが、サーバーやブラウザの実装に依存するのを回避するためです