Eclipse Collections 10.0の新機能 - Part 1 #EclipseCollections

本記事はDonald Raab氏による New Features of Eclipse Collections 10.0 — Part 1 - Donald Raab - Medium を元にした日本語版の記事です。日本語でわかりやすい情報となるように適宜編集しており、日本語訳とは違って必ずしも元の文章を訳したものではありません(本人にrewriteの許可をもらっております)。原文を参考にしたい場合は上記リンクからどうぞ。

Eclipse Collections 10.0の新機能を用いたコード例を紹介していきます。

f:id:itohiro73:20190804213414p:plain
Eclipse Collections 10.0の新機能10個

サマリー

本記事では、Eclipse Collections 10.0がリリースされました #EclipseCollections - itohiro73’s blog で挙げた26個の新機能のうち最初の10個を紹介します。

1. MultiReaderList/Bag/Set に特化したInterface

Eclipse Collectionsには、長らくmulti-reader対応のコレクション実装が存在していましたが、それらに対応するインターフェースは存在していませんでした。10.0以降ではこれらのインターフェースが利用できるようになっています。

f:id:itohiro73:20190804214204p:plain
MultiReaderList/Set/Bagインターフェース

@Test
public void multiReaderList()
{
    MultiReaderList<String> list = 
        Lists.multiReader.with("1", "2", "3");

    list.withWriteLockAndDelegate(backingList -> {
        Iterator<String> iterator = backingList.iterator();
        iterator.next();
        iterator.remove();
    });
    Assert.assertEquals(Lists.mutable.with("2", "3"), list);
}

2. プリミティブListからプリミティブStreamを生成する実装

普通のListからStreamを生成するのは簡単ですが、Eclipse Collections 9.xまではプリミティブListからStreamを生成するのは簡単ではありませんでした。10.0からは下記のようにプリミティブListからプリミティブStreamを簡単に生成できます。

@Test
public void primitiveListToPrimitiveStream()
{
    IntStream intStream1 =
            IntLists.mutable.with(1, 2, 3, 4, 5)
                    .primitiveStream();
    IntStream intStream2 =
            IntLists.immutable.with(1, 2, 3, 4, 5)
                    .primitiveStream();

    LongStream longStream1 =
            LongLists.mutable.with(1L, 2L, 3L, 4L, 5L)
                    .primitiveStream();
    LongStream longStream2 =
            LongLists.immutable.with(1L, 2L, 3L, 4L, 5L)
                    .primitiveStream();

    DoubleStream doubleStream1 =
            DoubleLists.mutable.with(1.0, 2.0, 3.0, 4.0, 5.0)
                    .primitiveStream();
    DoubleStream doubleStream2 =
            DoubleLists.immutable.with(1.0, 2.0, 3.0, 4.0, 5.0)
                    .primitiveStream();
}

3. toMapにおけるターゲットMapのサポート

toMapオーバーロードとして、ターゲットのMapを引数としてとるメソッドが追加されました。

@Test
public void toMapWithTarget()
{
    MutableList<Integer> list =
            Lists.mutable.with(1, 2, 3, 4, 5);

    Map<String, Integer> map =
            list.toMap(String::valueOf,
                       each -> each,
                       new LinkedHashMap<>());

    Map<String, Integer> expected = new LinkedHashMap<>();
    expected.put("1", 1);
    expected.put("2", 2);
    expected.put("3", 3);
    expected.put("4", 4);
    expected.put("5", 5);

    Assert.assertEquals(expected, map);
}

4. MutableMapIterable.removeAllKeysの実装

Mapに対して、あるSetに含まれる要素に対応するキーを削除する removeAllKeys メソッドが利用できます。

@Test
public void removeAllKeys()
{
    MutableMap<Integer, String> map = 
        Maps.mutable.with(1, "1", 2, "2", 3, "3");

    map.removeAllKeys(Sets.mutable.with(1, 3));

    Assert.assertEquals(Maps.mutable.with(2, "2"), map);
}

5 RichIterable.toBiMapの実装

Eclipse Collections 10.0では、RichIterableBiMapに変換することができます。

@Test
public void toBiMap()
{
    MutableBiMap<String, Integer> expected = 
      BiMaps.mutable.with("1", 1, "2", 2, "3", 3);

    MutableBiMap<String, Integer> biMap = 
      Lists.mutable.with(1, 2, 3).toBiMap(String::valueOf, i -> i);

    Assert.assertEquals(expected, biMap);
}

6 Multimap.collectKeyMultiValues の実装

Multimapに対して、keyとvalueの両方に対してfunctionを適用して変換できるcollectKeyMultiValuesメソッドが追加になりました。

@Test
public void collecKeyMultiValues()
{
    MutableListMultimap<String, String> multimap =
            Multimaps.mutable.list.with(
                    "nj", "Monmouth",
                    "nj", "Bergen",
                    "nj", "Union");

    MutableBagMultimap<String, String> transformed =
            multimap.collectKeyMultiValues(
                    String::toUpperCase, 
                    String::toUpperCase);

    Assert.assertEquals(Multimaps.mutable.bag.with(
            "NJ", "MONMOUTH",
            "NJ", "BERGEN",
            "NJ", "UNION"), transformed);
}

7 コレクションファクトリにおけるfromStream(Stream)の実装

ListSetBagStackのそれぞれのファクトリクラスにおいて、Streamからコレクションを生成するfromStreamメソッドが利用できるようになりました。

@Test
public void fromStreamOnCollectionFactories()
{
    MutableList<Integer> 
        list = Lists.mutable.fromStream(Stream.of(1, 2, 3, 4, 5));
    Assert.assertEquals(
        Lists.mutable.with(1, 2, 3, 4, 5), list);

    MutableSet<Integer> set = 
        Sets.mutable.fromStream(Stream.of(1, 2, 3, 4, 5));
    Assert.assertEquals(
        Sets.mutable.with(1, 2, 3, 4, 5), set);

    MutableBag<Integer> bag = 
        Bags.mutable.fromStream(Stream.of(1, 2, 3, 4, 5));
    Assert.assertEquals(
        Bags.mutable.with(1, 2, 3, 4, 5), bag);

    MutableStack<Integer> stack = 
        Stacks.mutable.fromStream(Stream.of(1, 2, 3, 4, 5));
    Assert.assertEquals(
        Stacks.mutable.with(1, 2, 3, 4, 5), stack);
}

8 LazyIterate.cartesianProductの実装

直積集合を求めるcartesianProductメソッドはもともとユーティリティメソッドとして存在していましたが、Setのみをパラメーターとしてとっていたため、他のIterableに対してはは明示的にSetに変換してから呼び出す必要がありました。今回導入された新しい LazyIterate.cartesianProductではどのようなIterableからも直積集合を求めることができます。

@Test
public void cartesianProduct()
{
    MutableList<Integer> numbers = Lists.mutable.with(1, 2, 3);
    MutableList<String> letters = Lists.mutable.with("A", "B", "C");

    MutableList<Pair<String, Integer>> pairs =
            LazyIterate.cartesianProduct(letters, numbers).toList();

    MutableList<Pair<String, Integer>> expected =
            Lists.mutable.with(
                    Tuples.pair("A", 1),
                    Tuples.pair("A", 2),
                    Tuples.pair("A", 3),
                    Tuples.pair("B", 1),
                    Tuples.pair("B", 2),
                    Tuples.pair("B", 3),
                    Tuples.pair("C", 1),
                    Tuples.pair("C", 2),
                    Tuples.pair("C", 3));

    Assert.assertEquals(expected, pairs);
}

9 プリミティブMapへのupdateValuesメソッドの追加

プリミティブMapに対して全てのvalueを更新したい場合、 updateValuesを使うことができます。

@Test
public void updateValues()
{
    MutableIntIntMap map = IntIntMaps.mutable.empty()
            .withKeyValue(1, 5)
            .withKeyValue(2, 3)
            .withKeyValue(3, 10);

    map.updateValues((k, v) -> v + 1);

    MutableIntIntMap expected = IntIntMaps.mutable.empty()
            .withKeyValue(1, 6)
            .withKeyValue(2, 4)
            .withKeyValue(3, 11);

    Assert.assertEquals(expected, map);
}

10 MutableMultimap.getIfAbsentPutAllの実装

MutableMultimap.getIfAbsentPutAllは、MutableMap.getIfAbsentPutと同様の機能を持ちます。違いとしては、MutableMultimap.getIfAbsentPutAllでは複数のvalueをコレクションとしてputすることができる点にあります。

@Test
public void getIfAbsentPutAll()
{
    MutableListMultimap<Integer, Integer> multimap =
            Multimaps.mutable.list.with(2, 1);

    ImmutableList<Integer> defaultValue = 
            Lists.immutable.with(1, 2, 3);
    MutableList<Integer> oneValue = 
            multimap.getIfAbsentPutAll(1, defaultValue);
    MutableList<Integer> twoValue = 
            multimap.getIfAbsentPutAll(2, defaultValue);

    Assert.assertEquals(defaultValue, oneValue);
    Assert.assertEquals(Lists.mutable.with(1), twoValue);
}

まだまだあります

Eclipse Collections 10.0にはまだ16個の機能が残っています。これらも記事として公開していきますのでお楽しみに。

Eclipse Collections 10.0の新機能、楽しんでいただけると幸いです!!

Eclipse Collectionsコントリビューションを受け付けています!どこから始めていいかよくわからないという方はitohiro73までDMいただければ喜んでガイドさせていただきます。

もしライブラリを気に入っていただけて、われわれに知らせたいという方は、Twitterハッシュタグ#EclipseCollectionsをつけてつぶやいていただいたり、もしくはGitHubレポにスターをつけていただけるとめっちゃよろこびますのでどうぞよろしくお願いします!!

Eclipse Collections 10.0がリリースされました #EclipseCollections

本記事はDonald Raab氏による Eclipse Collections 10.0 Released - Oracle Developers - Medium を元にした日本語版の記事です。日本語でわかりやすい情報となるように適宜編集しており、日本語訳とは違って必ずしも元の文章を訳したものではありません(本人にrewriteの許可をもらっております)。原文を参考にしたい場合は上記リンクからどうぞ。

f:id:itohiro73:20190804203215p:plain
The features you want, with the collections you need.

コントリビューターの皆さんありがとうございました

Eclipse Collections の10.0は2019年の7月にリリースされ、前回の9.2リリースから1年4ヶ月となるメジャーバージョンのリリースとなります。9.xバージョンのリリースは非常に機能が豊富で、コミュニティからたくさんのコントリビューションがあったんですが、10.0ではそれを超えるものになりました!なんと、10.0のリリースでは実に18人ものコントリビューターが機能追加をしてくれており、着実にOSSとしての成長を歩んでおります。貴重な時間を費やしてEclipse Collectionsの機能追加や品質改善にご協力いただいたコントリビューター皆さんに感謝の気持ちでいっぱいです!ありがとうございました!!

ひとつの記事にまとめるには機能が多すぎるw

Eclipse Collectionsの10.0は非常に多くの新機能が含まれており、ひとつの記事に収めるには多すぎるため、今後3つの記事に分けて機能の詳細を紹介していきます。本記事では、新機能のサマリーを紹介するにとどめます。

新機能のサマリー

  1. MultiReaderList/Bag/Set に特化したInterface
  2. プリミティブListからプリミティブStreamを生成する実装
  3. toMapにおけるターゲットMapのサポート
  4. MutableMapIterable.removeAllKeysの実装
  5. RichIterable.toBiMapの実装
  6. Multimap.collectKeyMultiValues の実装
  7. コレクションファクトリにおけるfromStream(Stream)の実装
  8. LazyIterate.cartesianProductの実装
  9. プリミティブMapへのupdateValuesメソッドの追加
  10. MutableMultimap.getIfAbsentPutAllの実装
  11. Bag.collectWithOccurrencesの実装
  12. プリミティブのIterableへのreducereduceIfEmptyの追加
  13. プリミティブ用の<type1><type2>To<type1>Functionの追加
  14. プリミティブMapへのof/withInitialCapacityメソッドの追加
  15. RichIterable.countByEachの実装
  16. UnifiedSetWithHashingStrategy.addOrReplaceの実装
  17. UnmodifiableMutableOrderedMapの実装
  18. ミュータブルなプリミティブMap上でのwithAllKeyValuesの実装
  19. IterableからPrimitivePrimitive/PrimitiveObject/ObjectPrimitiveMapを生成する機能の実装
  20. HashingStrategySets.of/withInitialCapacityの実装
  21. RichIterable.getAnyの実装
  22. プリミティブのハッシュデータ構造におけるresize/rehashメソッドの刷新と標準化
  23. Iterable<BoxedPrimitive>からPrimitiveStack/Bag/List/Setへ変換するファクトリメソッドの実装
  24. Multimapsファクトリクラス内の'ImmutableSortedBagMultimapFactory`の実装
  25. MapをパラメータにとるMapファクトリメソッドの実装
  26. MultableMultimap.putAllPairs/addメソッドパラメーターへのワイルドカード型の導入

新機能については 最新のJavaDocもご覧ください。

機能外での改善点

ライブラリ本体の機能追加以外にも、さまざまな改善がなされています。 1. テストカバレッジの改善 2. ビルドの改善 3. コードの重複の削減 4. 非推奨なクラスの削除 5. ジェネリクスの改善 6. 新しいベンチマークテストの追加 7. その他いろいろ!

ありがとうございます

すべてのコントリビューターとコミッターを代表して... Eclipse Collectionsを使っていただきありがとうございます!! 10.0リリースでの新しい機能と改善を楽しんでいただけると幸いです。

これら新機能の詳細は例をつけて追って記事化していきますのでお楽しみに!

Eclipse Collectionsコントリビューションを受け付けています!どこから始めていいかよくわからないという方はitohiro73までDMいただければ喜んでガイドさせていただきます。

もしライブラリを気に入っていただけて、われわれに知らせたいという方は、Twitterハッシュタグ#EclipseCollectionsをつけてつぶやいていただいたり、もしくはGitHubレポにスターをつけていただけるとめっちゃよろこびますのでどうぞよろしくお願いします!!

FOLIOからfreeeに転職します

2019年1月20日付で株式会社FOLIOを退職し、2019年1月21日付でfreee株式会社に入社します。

f:id:itohiro73:20190121080854j:plain

FOLIOで何をしてきたか

FOLIOでは2017年4月にテックリードとしてジョインし、その後Head of Engineeringへと名称が変わり、その後病気による休職を経て復職し、シニアソリューションアーキテクトとして働いてきました。 たった1年8ヶ月ほどの在籍でしたが、非常に密度の濃い日々を過ごしてきた気がします。

今回はせっかくなので生々しい話も含めて在籍中の思い出を語ってみたいと思います。

FOLIOとの出会い

FOLIOを知ったのは2017年の2月頃、この記事を見つけたのがきっかけでした。

newspicks.com

前職の先輩であるbitFlyerのCEO加納さんのこのようなコメントをみて、プロダクトリリース前でのこの期待値はすごいな、面白そうだな、と思った記憶があります。

f:id:itohiro73:20190120165104p:plain

さっそくコーポレートサイトのトップページにあった「採用に興味がある方はこちら」のフォームから応募しました(意外に思われる方も多いかもしれませんが、立ち上げ期のスタートアップに入りたいのであれば、このやり方が一番手っ取り早いです。)。

面談ではCEOとCDO、そして全てのエンジニアたちと話をしました。とにかく皆優秀なエンジニアたちで、話をしていてほんとうに面白かった。話をしている中で、βリリースに向けての課題は山積みで、とくに証券会社の複雑なシステムを作り上げる上で、エンジニアリング上の課題を理解しつつビジネス側の要求を整理し、期待値を調整しつつ開発プロジェクトを推進していけるようなシニアな人材が当時おらず、自分がそこにフィットしそうだと感じました。

初日でほぼ気持ちはかたまり、2日目の面談で条件面をすり合わせ、オファーをいただきその場で承諾しました。

βリリースに向けた戦い

当時は2017年の5月8日がβリリース日として設定されていました。私の正式入社日が5月17日の予定だったのですが、正直そんな悠長なことを言っていられないくらい開発が切羽詰まっており、2017年の4月6日から有給消化期間を利用して開発のお手伝い(という名の怒涛のプロジェクトマネジメント)を始めます。

思い出深いのが4日6日の初出社日。とにかく現状把握のため、βリリースに関わるエンジニア全員と1 on 1をしました。そこでわかったことは惨憺たるものでした。

  • そもそもの機能要件からして未確定なものがある
  • インフラ人員がアプリケーション開発を手伝っており、肝心のインフラ整備が追いついていない
  • 開発スケジュールのつくりかたも積み上げ式の見積もりではなく、5月7日ありきの逆算型でつくられている
  • その他細々した問題が山積み

Webサービスとはいえ、お客様のお金を預かる証券会社のサービスです。たった1日のヒアリングでしたが、正直1ヶ月後にβリリースを控えた開発状況とはとうてい言えない肌感を得ました。もうこの時点で既にデスマーチの様相を呈しており、エンジニアたちも疲弊している状況でした。早速CEOをつかまえて1ヶ月後のβリリースは非常に危険な状態であることを伝えます。が、このとき自分のコミュニケーションの浅はかさに気づきます。

自分としては当然危険な状況であることが肌感としてわかったのでそれをそのままCEOに伝えたのですが、CEOにしてみればそのころはバーンレートも急激に上がってきた段階で、ビジネスプランのフィージビリティも投資家からガツガツつめられたりするような情勢の中、安易なスケジュール遅延は到底受け入れられません。初日に来たばかりの新参者のたかが肌感で、「1ヶ月後のリリースは無理だ、遅らせる必要がある」と言われても、はいそうですかと言えるわけがないのです。

私としてもこの時点では開発の責任者を請け負っているので、安易にデスマーチを受け入れるわけにはいきません。それこそ無理な開発スケジュールで開発を強行することによってビジネスが破綻してしまう可能性さえあるのです。初日からガチバトルです(健全なバトルだったと思います)。

とにもかくにも、CEOからは現状の精査をもう少し解像度の高い形でしてくれとの要請を得て、その日はおさまりました。

ここから怒涛の日々が始まります(*注:入社前)。

3日後には開発の現状をまとめたプレゼンを経営陣と開発PM陣にし、今の開発状況で技術的負債がどのような形で蓄積されているのか、このまま1ヶ月後のリリースを見据えて開発を続けた場合にどのようなネガティブインパクトが起こりうるのかを切々と語りました。

経営陣にはある程度納得してもらったところで、今度は10日後の取締役会(実態は投資家全員が参加する株主総会)で投資家たちにスケジュール変更の提案をするように要請されます。

さらに怒涛の日々を過ごし、見積もりを精緻化させ、取締役会に備えます(*注:入社前)。

取締役会では、開発現状のポジティブ面ネガティブ面の両面をうまく見せながら、積立方式の見積もりでゼロバッファーで見積もった場合に6月19日が最短のスケジュール(ただし達成確立は50%以下)、そこをマイルストーンとして置いて開発を進めると結果として2週間後の7月3日のリリースを目指すのがおおよそ70%ラインというお話をしました。

いろいろ突っ込まれるかとヒヤヒヤしていましたが、投資家達はおしなべて理解を示してくれて、このスケジュールで進めていこうという合意がなされました。

しかしながら、この時点では「絶対無理なデスマーチ」が、「普通のデスマーチ」に変化しただけなので、全く油断ができません。ここからもエンジニア達とともに開発にいそしむ怒涛の日々が始まります(*注:入社前)。

その後5月17日の入社日も無事迎えることができ、エンジニアたちとβリリースに向けて走り続けます。

結果としては、達成確立70%の見積もりラインを4日だけ超えて無事に7月7日にβリリースを迎えることができました。(せっかくのリリースなので、7月7日の七夕かつゾロ目がよいのではという判断なので実際はもうすくし早く開発は終えてました)。これに関しては、見積もりとほぼ違わずにリリースできたというサクセスストーリーでもあるのですが、無理な見積もりを強行せざるを得なかったというバッドストーリーでもあるので両面から語り継いでいきたい話ではあります。

https://story.folio-sec.com/n/n6576f573e5b6

非常に産みの苦しみの大きい最初のリリースでしたが、ただでは転ばないのかFOLIOのエンジニア達のすごいところ。チームとしてβリリース後も大小さまざまなプロジェクトを経験してきましたが、まっちゃらはそれらから学んだ見積もりの手法の重要性を理解し、下のような素晴らしい資料をつくって社内での勉強会を開いてくれたりしました。

matsu-chara.hatenablog.com

とにかくFOLIOのエンジニアたちはさまざまな困難を踏み抜いても、それを自分たちの血肉にしていく強さを持ち合わせていると感じます。とても貪欲でかつ知的好奇心に溢れる技術集団で、自分としても今後も見習っていきたいなと思います。

証券会社としての業務システム「PORT」という概念の導入

もともと証券会社でエンジニアとして12年間働いていた経験から、証券会社におけるバックオフィスのシステムの重要性は身にしみて理解していました。証券サービスを提供するにあたっては、口座開設のワークフロー、顧客や自己ポジションの管理、現金残高の管理、コンプライアンスによる売買審査等々、さまざまな証券業務を支えるバックオフィスシステムを開発・運用する必要があります。さらに、バックオフィスシステムのユーザーは社内の同僚であり、これらの業務システムを協力して大切に育てていきたい性質のものだという考えを持っていました。

バイモーダル戦略で言うところのSoE(System of Engagement)がFOLIOWebサービスやモバイルアプリに相当するシステムであり、SoR(System of Record)がこれらバックオフィスシステムに相当します。

FOLIOにジョインした当時は、リリース前でそもそも社内の証券業務部の人員も存在しない時期だったため、経営陣もクリエイター陣もとくにこの辺の肌感や重要性が共有されておらず、バックオフィスの業務システム自体が(意図せず)軽視されていました。開発見積もりにおいても単に「管理画面」と呼ばれ、非常にモチベーションの低い対応がなされていました。

このような状況を改善するためバックオフィスの業務システムの名付けからスタートすべき、と言う話をして(*注:入社前です)、「PORT」という名前が付いたと言う話を下のブログに書いています。 itohiro73.hatenablog.com

ここからさらに、シンプルにSoEとSoRの2分割では足りないよーといろいろ掘り下げてくれたのが現バックエンドエンジニアリードのむらみんです。詳しくは下のブログをご覧ください。

t-and-p.hatenablog.com

クリエイターブランディングチームの導入とScala人材採用へのインパク

私がFOLIOにジョインした2017年4月当時は、全社員が20人そこそこ、半分の10人ほどがクリエイター(デザイナー+エンジニア)と少人数で、証券会社をつくりあげていくにはとにかく人手が足りない状況でした。今後組織を拡大していくにあたって、目先の開発のみに注力するだけではなく採用を強化するためにも、クリエイターのブランディングを確立していくことは非常に重要だという認識を持っていました。デザイナーに関してはCDOである広野氏が当時でも十分すぎるくらいのブランディング力を発揮していましたが、当時はまだ確立していなかったFOLIOのエンジニアのブランディングも強化していきたい想いがありました。

フロントエンドエンジニア、バックエンドエンジニア、インフラエンジニア(現在はSREと呼んでいます)、アプリエンジニア等々さまざまな職能で組織を拡大していかなければいけない中で、一番採用に関してチャレンジングであった領域が、バックエンドを支えるScalaエンジニアでした。Scalaエンジニアはとにかく人材市場規模が小さく、まだ知名度のないFOLIOにとっては採用をしかけていくのが非常に大変だったのです。

当時モヤモヤと思い描いていたブランディング戦略としては、バックエンドエンジニアの何名かがJavaのコミュニティ内である程度の訴求力をもっていたため、Scalaユーザーも一定数いるJavaコミュニティでの認知を増やしていければいずれScalaコミュニティにも波及し、Scalaエンジニアの採用へとつながるのではないかという漠然とした考えを持っていました。

漠然とはしていましたが、持てる力はすべて出し尽くそうという考えのもと、入社直後からアクセル全開で攻めていきました。

とれる戦術の順序としては、『Javaコミュニティでの認知の拡大』→『技術的な実績づくり』→『Scalaコミュニティでのさらなる認知の拡大』です。

当時ほぼ同時期に入社したJava女子部部長であるよこなさんとともに、まずはバズりそうな入社エントリーをダブルで出稿します。

ihcomega.hatenadiary.com

itohiro73.hatenablog.com

この時点でJavaコミュニティに対しては、「おっ?FOLIOってなんだ?」という小さな認知の拡大をもたらせた気がしています。

この後のJJUG CCCで話したのが、技術的にも非常に面白いテーマであるテンポラルデータモデルとReladomoのお話。これが見事にバズります(407はてブ)。これは狙ってもなかなかできるものではないので、自分でもビックリしました。 b.hatena.ne.jp

そして、ここで奇跡が起きます。

なんとScalikeJDBCの開発者である瀬良さんが興味を持ってくださいました!

FOLIOでは証券会社として履歴にまつわる機能要件が非常に多く、もともとReladomoをScalaから使えるようなライブラリーをつくっていきたいと考えていました。しかし、上記のようにβリリースに向けての多忙に見舞われる中で、自分で開発に携わるのは相当厳しい情勢でした。そんな中、瀬良さんに手伝っていただければScalaのラッパーを書けるのでは!と盛り上がります。

速攻DMで連絡し、いろいろとお話をさせていただいたのちに業務委託として「ReladomoのScalaラッパーをOSSとして開発」していただくことになりました🎉

このころ、Java/Scalaだけにとどまらず、フロントやインフラ、アプリなどの様々なエンジニアリング領域、そしてクリエーター全体でのブランディングを底上げするために、「クリエイターブランディングチーム」を立ち上げます。メインのリードは前述のよこなさんです。ブランディングチームの戦略について、CEOを巻き込んで重要性を理解してもらうことにも注力しました。

人事部とも協力して、社内のエンジニアが働いている様子のWantedlyの記事化にも取り組みました。

そうこうしているうちに、秋口のScala関西Summitが近づいてきます。

ここで、Scala関西Summitの2日前にScalaエンジニアの紹介記事をぶちこみます。 www.wantedly.com

これが功を奏し、Scala関西Summitに参加するエンジニアたちにFOLIOの名前をほのかに認知させることができました。そして、Scala関西Summit当日、瀬良さんと共同でreladomo-scalaのセッションで登壇し、OSSのリアルタイム公開に踏み切ります。

www.wantedly.com

Reladomoの持つ技術的な面白さに相まって、OSSをリアルタイムで公開すると言う試みでこのセッションがいい感じにバズりました。

そして、なんと、ここでの懇親会で採用につながったのが、先述の、現在FOLIOでバックエンドのリードをつとめるむらみんです。

2017年のScala関西Summitで採用された彼が2018年の同イベントで登壇するという流れもできたので、非常にポジティブなブランディングサイクルになっています。

www.wantedly.com

さて、Scala関西Summitでの直接採用は一人だけですが、ブランディング施策のインパクトとしてはこれにとどまりません。

時系列でみてみると、私がジョインした2017年5月からScala関西Summitが開催された2017年9月までのバックエンドエンジニアの採用は0人なのですが、2017年9月から2018年8月までのバックエンドエンジニア採用実績はなんと9人リファラル等も含まれるので単純なブランディングだけのインパクトは計れないですが、間違いなく効いてきています。

最初は漠然とした戦略として始めたものが、様々な偶然も重なった上で実際にブランディングの結果として目に見える形で成果が現れたのは非常に感慨深いものがあります。

ここで紹介した施策は主に私が関わっていたScalaにまつわるものですが、それ以外の領域でもクリエイターブランディングチームはエンジニア達の対外活躍を支援するとりくみをしてくれています。例えばアドベントカレンダーFOLIO Advent Calendar 2017 - QiitaFOLIO Advent Calendar 2018 - Adventar)、証券会社が2年連続で完走するのは非常に珍しいと思います。今後もFOLIOの技術領域の動向をウォッチしていきたい次第です。

エンジニアの組織のブランディングを確立していく上で重要なのは、対外的な訴求とともに対内的な訴求も強めていくということだと思っています。つまり、外部の人への認知を拡大するための施策をとるだけでなく、社内のエンジニアたちも自分たちの持つ技術力や開発力に自信を持って、もっとこの環境で働きつづけたい、自分の知り合いにも勧めたいと思えるような、正のフィードバックループが働くブランディングをしていくことが重要だと思います。そのためにも、DX(Developer Experience)の改善や社内勉強会の充実、OSSや外部コミュニティへの貢献の促進等、環境を整えていくことががとても大切になってくると考えます。この辺は今後関わっていくであろう様々なエンジニアリング組織でも突き詰めていきたいところです。

なぜ辞めるのか

FOLIOのビジネスはものすごく面白く、意義のあるものだと思っています。日本の個人の金融資産が約1800兆円あるといわれており、そのうちの半分以上である約960兆円が現金・預金が占めています(参考)。これまで様々な有識者や金融機関が「貯蓄から投資へ」を声高に叫んできましたが、この個人金融資産の半分以上を占める現金預金比率はここ20年以上変わってきていません。

フォリオのようなワクワクする、楽しく投資をするサービスが広まることで、今まで投資を選択肢に入れてこなかったような大勢の人たちが投資をしはじめる。それによって今まで循環してこなかった何兆円もの資金が投資に流れ、結果として経済を活性化させるという世界観は、ほんとうにエキサイティングなものであり、証券会社にエンジニアとして長年勤めてきた身としては今後はこのビジネスにコミットし続けていくものだと考えていました。最初の会社では12年働いたので、漠然と次の12年はFOLIOへコミットし続けていきたいなと考えていました。しかし、世の中そううまくはいきませんでした。

ここから急に重い話題になります。

2017年の11月、様々な複合要因から「抑うつ状態」と診断され、会社を休職しました。ほんとうに様々なストレス要因がからみあって引き起こされた病気だったため、ひとつずつストレス要因を解決していく必要がありました。

そこからは地獄のような日々が始まります。

最初は12月には復帰できそうな情勢でいたのが、復帰直前に再発、その後最悪な産業医面談による病状の悪化(詳細は省く)、リハビリによる回復等の紆余曲折を経てなんとか回復し、2018年の4月にようやく復職します。

この頃には当初の要因となった問題はほとんど解決されていたのですが、ひとつだけ大きなストレス要因が解決できていませんでした。これに関しては、もはや自分で解決できる問題ではなかったため、カウンセリング等を通じてその問題に対する見方を変えるしか方法はありませんでした。

休職中はベッドから起き上がれないくらい体が動かないような日々もあったのですが、復職後は身体への影響はもはやほとんどなく、仕事や日常生活は普通にこなせる状況まで回復していました。はたから見たら普通となんら変わらないように見えたと思います。

しかし、上記のひとつだけ残っていたストレス要因への対処がまったくうまくできず、精神的にはまだヤバい状態が続いていました。

どれだけヤバかったかというと、2018年のおおよそ5月〜9月くらいのTwitterの下書きがこんな感じで残っています。

f:id:itohiro73:20190120170732j:plain
復職後にも精神的に苦しんでいる様子がTwitterの下書きから伺える

下書きにとどまっていて、実際にはツイートしていないのがポイントですね。これはつぶやいたらアカンという葛藤がみえる。

このような精神状態は、もはやカウンセリングでは全く改善されず、問題に対する見方も変わる気配がありませんでした。このころから環境を変えざるを得ないのではないかという考えが自分の中で大きく占め始めます。

とはいえ、とにかくFOLIOの目指す世界観、FOLIOの同僚たちも仕事も大好きだったので、どうにかしてこのままFOLIOでの仕事を続けらないか模索していました。自分の精神状態を悪化させている明確な要因はFOLIOの環境下に存在しており、これは私にとって非常にセンシティブな問題なので公表できないのですが、取締役を含む数人にのみ共有していました。会社は私の抱えている問題に対して真摯に対応してくれており、相当な時間をかけて解決に向けて協力してくれていました。

しかしながら、このように周りの支えもあったのですが、残念ながら復職から半年経ってもどうにも改善できない状況を鑑みて、退職を決意せざるをえませんでした。

自分としてはあまりにもつらい決断で、最初に退職の申し出をした際も、社長の部屋でわんわん泣いてしまいました。実はこの際に一度強く引きとめられており、一旦保留としていたのですが、数週間ほどぐるぐると終わりのない考えを繰り返し、それでもにっちもさっちもいかない状態になり、やはり最終的に退職を決断した、という流れになります。

【2019年1月30日追記】 ここに書かれている事は、私個人に特有の事情によるところが大きく、FOLIO自体には何ら問題はありません。むしろ問題は私の内面の方にあると言えます。FOLIOはとても良い会社で、前述のようにとても面白いビジネスモデルを持ってます。同僚も優秀で働いていて楽しいですし、特にエンジニアにとっては、複雑なドメインと技術領域にチャレンジできるとても面白い職場だと思います。私も自分の抱えている問題が解決できたらいつか戻りたいとも思える素敵な場所なので、そこのところは勘案しつつ読んでいただけると幸いです。

大丈夫なの?

現在はFOLIOでの最終出社日から約1ヶ月経っており、ストレス要因からは離れてゆっくり休むことができました。

今となってはそれまでの精神的な苦悩がほんとうに嘘かのように消え去っており、現在は毎日を晴れ晴れしい気分で過ごしています。新しい職場での新しい仕事に対しても、ものすごくポジティブなワクワク感を持っていて、環境を変えるということのインパクトの大きさを身にしみて感じております。

freeeとの出会いとこれから何をするのか

実はfreeeとの出会いは約4年半前にさかのぼります。当時ちょっと外の世界をのぞきにいっていた時期があって、Wantedlyを通じて当時まだ2,30人くらいの規模感だったfreeeのオフィス(当時は麻布十番と赤羽橋の間あたりにありました)に遊びに行かせてもらいました。CEOの佐々木さんとは中小企業のバックオフィスを強化したいというビジネスの話で盛り上がり、その後お話を聞かせていただいたCTOの横路さんとは技術的な話でもりあがったものの、「伊藤さんがフィットしそうなのはもうちょっとfreeeが成長して組織的な課題が出てきた頃な気がしますね」というお言葉をいただき、自分もそんな気がするなーという感覚をもってその場はお開きになった覚えがあります。

その4年後である2018年、Wantedly経由で再度お声がかかり、久しぶりにキャッチアップをしませんかというメッセージがとどきました。五反田のオフィスに遊びに行かせていただいて、その際は基盤エンジニアの求人でお話しさせてもらったんですが、面白そうなポジションではあったものの特にお互いピンと来る感じではなく、またkeep in touchしましょうねという流れになりました。しかし、その1ヶ月後くらいに「freeeの新規部署・組織開発チームの担当者を募集!」という別のポジションで求人が出されているのをネットでたまたま見つけました。これは非常に面白そうと思い、再度何人かの経営陣やエンジニアとお話しさせていただいて、エンジニア組織が拡大していく中でのさまざまな課題感を聞かせてもらいました。なんたる偶然か、まさに組織が成長したことによる課題が4年越しでいろいろと顕在化してきたところで自分が色々と貢献できそうな仕事です。こちらは順調に選考が進み、無事内定をいただくことができました。

freeeで取り組むのは「開発組織企画」という新しいチームで、エンジニアの組織的な課題を解決していくポジションになります。全く新しいポジションなので裁量をもっていろいろおもしろい取り組みができそうで、今からワクワクしております。

自分はかねてから、自分自身が普通の人の1.5倍のパフォーマンスを出すよりも、チームの皆がそれぞれ1.2倍のパフォーマンスを出せるような環境を整備してあげることで、10人の組織であれば12人分、100人の組織であれば120人分、といったスケーラブルな形で大きなインパクトをもたらす仕事に興味をもっているので、このポジションは非常に面白いことができるのではないかと思っています。

FOLIOでも取り組んでいたようなエンジニアのブランディング活動も、組織を拡大して強くするために必要な施策だと思うので、freeeでも同様な取り組みを牽引していければと思っています。

今後のもろもろに、ご期待いただければと思います。

Wish List

ほしい物リスト、置いておきますね🙏

www.amazon.co.jp

Scala関西Summitでブロックチェーンについて話してきました #scala_ks

昨年に引き続き、Scala関西Summitに参加してきました。

今年もFOLIOがスポンサーしており、自分含め二人で登壇!

自分のセッションではScalaで簡易ブロックチェーンを実装してみる話をしました。ブロックチェーンの基本的な部分の解説と、その実装例をライブコーディングを交えて紹介しました。

高嶋さんによるうれしいフィードバックもいただき、Day2の打ち上げの際もご本人から「Scala以外の技術的領域に興味を広げられてよかったです」といううれしいコメントをいただきました。感謝。

Day1の終盤では去年のScala関西Summitで引き抜いた(?) id:mura-_-miFOLIOスポンサーセッションとして登壇していたので、巣を立つヒナをみる親鳥のような感覚で見ておりました。おかげさまで本セッションは多方面に好評をいただいており、自分ごとのように嬉しい限りでした。いやー、むらみん入ってなかったらほんとにFOLIOのバックエンドチームは今ほど体系だった組織に成長してなかったであろうと思うので、本当にScala関西Summitが与えてくれた出会いには感謝しております。

togetter.com

Day2のアンカンファレンスではとーますさんのAirframeのハンズオンに参加したり、IntelliJハンズオンのつまみ食いをしたり、おえさんのsbtタスクグラフのお話を聞いたり、Akkaに入門したりしました!とってもためになったし楽しかった!

大阪最後の夜は何食べようかなと思ってたら、Day2の打ち上げをするということで、参加させていただきました。 2次会では噂には聞いていた、待望のイタリアバルSCALAに初めて行ってきました。美味しかったし楽しかった!

とにかく最初から最後まで熱量高く、楽しい2日間でした。

いろいろとモチベーションが上がったし、来年もまたぜひ参加したいと思える素敵なイベントでした!!

ReladomoにEclipse Collectionsをサポートする変更をコントリビュートしました

Reladomoとはゴールドマン・サックス社がOSSとして公開しているJavaのORMで、型安全なクエリ言語やバイテンポラルデータモデルのサポートなど、エンタープライズグレードの機能を持つORMフレームワークです。

詳しくはこれらのスライドをご覧ください。

www.slideshare.net

www.slideshare.net

さて、こちらのReladomoですが、データベースから取得したコレクションを、同じくゴールドマンサック社が同社初のOSSとして公開したGS Collectionsに変換する機能をもともとサポートしていました。

ReladomoでGS Collectionsを使用するコード例
// select * from PERSON
PersonList people = PersonFinder.findMany(PersonFinder.all())

//苗字を取得
List<String> lastNames = 
        people.asGscList().collect(Person::getLastName);

//猫を飼っている飼い主を取得
List<Person> catOwner = 
        people.asGscList().select(person -> person.hasPet(PetType.CAT));

//PetTypeごとに飼い主をグルーピング
MutableListMultimap<PetType, Person> peopleByPetType = 
        people.asGscList().groupByEach(person -> person.getPets().asGscList().collect(Pet::getPetType));

しかし、ご存知の方もいるかもしれませんが、GS Collections自体は現在開発が終了しており、Eclipse Foundationへ移管されたEclipse Collectionsという新たなプロダクトとして生まれ変わっております。

Eclipse Collections - Features you want with the collections you need. (日本語ページ)

こちらのEclipse Collections、GS Collectionsと上位互換の機能を持つのですが、パッケージの構成が異なり、実態としては異なるライブラリ依存になります。ですので、これまでReladomoがサポートしているGS CollectionsをそのままEclipse Collectionsとして使用することはできませんでした。

今回の変更では、新たにEclipse CollectionsをサポートするAPI asEcList() を追加し、もともとのasGscList()がdeprecated(非推奨)となりました。本機能はReladomoの17.0.0より使用することが可能です。

これにより、Reladomoそのものが持つ強力なDBクエリー言語に加えて、DBから取得したコレクションをインメモリーで操作することもEclipse Collectionsの機能で扱うことができ、データ操作の選択肢の幅が広がります!

ReladomoでEclipse Collectionsを使用するコード例
// select * from PERSON
PersonList people = PersonFinder.findMany(PersonFinder.all())

//苗字を取得
List<String> lastNames = 
        people.asEcList().collect(Person::getLastName);

//猫を飼っている飼い主を取得
List<Person> catOwner = 
        people.asEcList().select(person -> person.hasPet(PetType.CAT));

//PetTypeごとに飼い主をグルーピング
MutableListMultimap<PetType, Person> peopleByPetType = 
        people.asEcList().groupByEach(person -> person.getPets().asGscList().collect(Pet::getPetType));

その他変更の詳細はこちらのPRをご覧ください。

github.com

Reladomoでは、1年後の2019年3月にはGS Collectionsのサポートは終了する予定です。ですのでReladomoとGS Collectionsを併せて使用している方は早めの移行をおすすめします。

移行ガイドはこちら。

reladomo/GSC_TO_EC_MIGRATION_GUIDE.md at master · goldmansachs/reladomo · GitHub

ReladomoとEclipse Collections、最強の組み合わせでぜひ楽しい開発をしてみてください。Enjoy Happy Development!!

【第三回】経費申請のフローで学ぶCamundaの基本(BPMN実行編)

前回は、Camunda Modelerを用いて「経費申請」フローの簡単なBPMNモデリングをしてみました。第一回と第二回までの作業が終了したプロジェクトがGitHubにあがっているので、今回はここからスタートします。

今回は、前回作成したBPMNモデルをCamunda上で実行可能な状態にし、申請、承認、確認に必要なフォーム(html/JavaScript)とService Taskで実行するJavaコードを実装し、実際に使える業務アプリケーションとして動作させてみましょう。

まず先に業務要件をモデリングしてからアプリケーションの実装を行うということで、モデル駆動開発とも呼べる流れになっています。この方法論も楽しんでもらえればと思います。

前回作成した経費申請のBPMNモデル

f:id:itohiro73:20180115112911p:plain

BPMNを実行可能にする

まずは前回作成したBPMNモデルをCamunda上で実行可能にする必要があります。「経費申請」プールを選択して、右側に現れるメニューからExecutableという項目にチェックを入れれば実行可能になります。

また、Process Idにはわかりやすい名前をつけておきましょう。今回はExpenseApplication としました。

f:id:itohiro73:20180115185002p:plain

ちなみに、Camunda ModelerでつくるBPMNモデルは普通に考えてCamunda上で実行するんだから、デフォルトで実行可能にしてくれよ、っていうissueがあがっていて、どうやら次のバージョン(v1.12.0)にマージされているようです。リリースはよ。

フォームの作成

BPMNのワークフローを実行するにあたって、人間による作業や確認をするためのフォームが必要になってきます。

今回の経費申請フローでは、下記のようにそれぞれのイベント・ユーザータスクのBPMNコンポーネントに対応する、3つのフォームを作成します。

BPMNコンポーネント フォーム名 ファイル名
f:id:itohiro73:20180115165914p:plain 経費申請フォーム apply-form.html
f:id:itohiro73:20180115165959p:plain 経費承認フォーム approve-form.html
f:id:itohiro73:20180115174804p:plain 経費確認フォーム confirm-form.html
src/main/resources/static/forms/apply-form.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>経費申請フォーム</title>
</head>
<body>
<h2>経費申請フォーム</h2>
<form role="form" name="expenseForm"
      class="form-horizontal">

    <div class="form-group">
        <label class="control-label col-md-4"
               for="expenseReceiptUpload">領収書のアップロード</label>
        <div class="col-md-8">
            <input type="file"
                   id="expenseReceiptUpload"
                   cam-variable-name="expenseReceipt"
                   cam-variable-type="File"
                   cam-max-filesize="10000000"
                   class="form-control"/>
            <div class="help-block">経費の領収書の画像を選択してください</div>
        </div>
    </div>

    <script cam-script type="text/form-script">
            var fileUpload = $('#expenseReceiptUpload');
            var fileUploadHelpBlock = $('.help-block', fileUpload.parent());

            function flagFileUpload() {
              var hasFile = fileUpload.get(0).files.length > 0;
              fileUpload[hasFile ? 'removeClass' : 'addClass']('ng-invalid');
              fileUploadHelpBlock[hasFile ? 'removeClass' : 'addClass']('error');
              return hasFile;
            }

            fileUpload.on('change', function () {
              flagFileUpload();
            });

            camForm.on('submit', function(evt) {
              var hasFile = flagFileUpload();

              // prevent submit if user has not provided a file
              evt.submitPrevented = !hasFile;
            });

    </script>

    <div class="form-group">
        <label class="control-label col-md-4"
               for="detail">内容</label>
        <div class="col-md-8">
            <input cam-variable-name="detail"
                   cam-variable-type="String"
                   id="detail"
                   class="form-control"
                   type="text"
                   required />
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-md-4"
               for="amount">金額(円)</label>
        <div class="col-md-8">
            <input cam-variable-name="amount"
                   cam-variable-type="Double"
                   id="amount"
                   name="amount"
                   class="form-control"
                   type="text"
                   required />
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-md-4"
               for="expenseCategory">経費種別</label>
        <div class="col-md-8">
            <select cam-variable-name="expenseCategory"
                    cam-variable-type="String"
                    id="expenseCategory"
                    class="form-control">
                <option value="TRAVEL" label="旅費交通費"/>
                <option value="MEAL" label="接待交際費"/>
                <option value="EXPENDABLES" label="消耗品"/>
                <option value="OTHER" label="その他"/>
            </select>
        </div>
    </div>

</form>

</body>
src/main/resources/static/forms/approve-form.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>経費申請フォーム</title>
</head>
<body>
<h2>経費申請フォーム</h2>
<form role="form" name="expenseForm"
      class="form-horizontal">

    <p>経費情報を確認の上、承認してください</p>

    <div class="form-group">
        <label class="control-label col-md-2">経費領収書</label>
        <div class="form-control-static col-md-10">
            <input type="hidden" cam-variable-name="expenseReceipt"/>
            <img src="{{ camForm.variableManager.variable('expenseReceipt').contentUrl }}" class="img-responsive">
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-md-2">内容</label>
        <div class="col-md-10">
            <input cam-variable-name="detail"
                   type="text"
                   name="detail"
                   readonly="true"
                   class="form-control" />
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-md-2">金額(円)</label>
        <div class="col-md-10">
            <input cam-variable-name="amount"
                   type="text"
                   name="amount"
                   readonly="true"
                   class="form-control" />
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-md-2">経費種別</label>
        <div class="col-md-10">
            <select cam-variable-name="expenseCategory"
                    cam-variable-type="String"
                    name="expenseCategory"
                    disabled="true"
                    class="form-control">
                <option value="TRAVEL" label="旅費交通費"/>
                <option value="MEAL" label="接待交際費"/>
                <option value="EXPENDABLES" label="消耗品"/>
                <option value="OTHER" label="その他"/>
            </select>
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-10 col-md-offset-2">
            <label>
                承認しますか?
                <select cam-variable-name="approved"
                        cam-variable-type="Boolean"
                        id="approved"
                        name="approved"
                        class="form-control">
                    <option value="true" label="承認"/>
                    <option value="false" label="却下"/>
                </select>
            </label>
        </div>
    </div>

</form>

</body>
src/main/resources/static/forms/confirm-form.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>経費申請フォーム</title>
</head>
<body>
<h2>経費申請フォーム</h2>
<form role="form" name="expenseForm"
      class="form-horizontal">

    <p>経費情報を確認の上、タスクをCompleteにしてください</p>

    <div class="form-group">
        <label class="control-label col-md-2">経費領収書</label>
        <div class="form-control-static col-md-10">
            <input type="hidden" cam-variable-name="expenseReceipt"/>
            <img src="{{ camForm.variableManager.variable('expenseReceipt').contentUrl }}" class="img-responsive">
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-md-2">内容</label>
        <div class="col-md-10">
            <input cam-variable-name="detail"
                   type="text"
                   name="detail"
                   readonly="true"
                   class="form-control" />
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-md-2">金額(円)</label>
        <div class="col-md-10">
            <input cam-variable-name="amount"
                   type="text"
                   name="amount"
                   readonly="true"
                   class="form-control" />
        </div>
    </div>

    <div class="form-group">
        <label class="control-label col-md-2">経費種別</label>
        <div class="col-md-10">
            <select cam-variable-name="expenseCategory"
                    cam-variable-type="String"
                    name="expenseCategory"
                    disabled="true"
                    class="form-control">
                <option value="TRAVEL" label="旅費交通費"/>
                <option value="MEAL" label="接待交際費"/>
                <option value="EXPENDABLES" label="消耗品"/>
                <option value="OTHER" label="その他"/>
            </select>
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-10 col-md-offset-2">
            <label>
                承認情報:
                <select cam-variable-name="approved"
                        cam-variable-type="Boolean"
                        id="approved"
                        name="approved"
                        disabled="true"
                        class="form-control">
                    <option value="true" label="承認"/>
                    <option value="false" label="却下"/>
                </select>
            </label>
        </div>
    </div>

</form>

</body>
BPMNコンポーネントとフォームの紐付け

これらのフォームをそれぞれのイベント・タスクと結びつけるために、BPMNコンポーネントを選択肢、右メニューのFormsタブにあるForm Keyという項目に、対応するフォームのファイルパスをembedded:app:に続けて入力します。

f:id:itohiro73:20180115174130p:plain

embedded:app:forms/apply-form.html
embedded:app:forms/approve-form.html
embedded:app:forms/confirm-form.html

以上でフォームに関する設定は完了です。

Exclusive Gatewayの分岐条件の設定

次に、「承認」タスクで承認者がタスクをCompleteした後に、ゲートウェイにおいて承認済みか否かの分岐判定が必要になります。

「承認済み?」のExclusive Gatewayから伸びている「いいえ」と「はい」のフローコンポーネントそれぞれに、「承認」タスクでフォームで入力されたapproved変数を用いた条件式を設定します。

「いいえ」の分岐

f:id:itohiro73:20180115182800p:plain

f:id:itohiro73:20180115183128p:plain

「はい」の分岐

f:id:itohiro73:20180115183236p:plain

f:id:itohiro73:20180115183322p:plain

Service Taskの実装

「経費記録」のサービスタスクでは、今回は単純に経費情報をログに出力してみましょう。

f:id:itohiro73:20180115165141p:plain

Service Taskは実装方法を選択する必要があるので、「経費記録」のService Taskの詳細に、今回はJava Classとして下記のように入力しておきます。

f:id:itohiro73:20180115164728p:plain

Javaの実装としては、経費種別のラベリングのためのenum Category.javaと、サービスタスク ExpenseRecorder.javaを以下のように作成します。

src/main/java/sample/Category.java
package sample;

public enum Category {
    TRAVEL {
        @Override
        public String toString() {
            return "旅費交通費";
        }
    },
    MEAL {
        @Override
        public String toString() {
            return "接待交際費";
        }
    },
    EXPENDABLES {
        @Override
        public String toString() {
            return "消耗品";
        }
    },
    OTHER {
        @Override
        public String toString() {
            return "その他";
        }
    }
}
src/main/java/sample/service/ExpenseRecorder.java
package sample.service;

import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import sample.Category;

import java.util.logging.Logger;

public class ExpenseRecorder implements JavaDelegate {
    private final Logger LOGGER = Logger.getLogger(ExpenseRecorder.class.getName());

    @Override
    public void execute(DelegateExecution execution) throws Exception {

        LOGGER.info("内容: " + execution.getVariable("detail"));
        LOGGER.info("金額(円): " + execution.getVariable("amount"));
        LOGGER.info("経費種別: " + Category.valueOf((String)execution.getVariable("expenseCategory")));
        LOGGER.info("承認済み?: " + (((Boolean)execution.getVariable("approved")).booleanValue() ? "はい" : "いいえ"));
    }
}

BPMNの配備とプロセス設定

作成したBPMNはsrc/main/resources直下に配備しておきましょう。

src/main/resources/expense_application.bpmn

次に、src/main/resources/META-INF/processes.xmlを作成し、経費申請BPMN(expense_application.bpmn)をデフォルトで読み込むように設定します。

<process-application
  xmlns="http://www.camunda.org/schema/1.0/ProcessApplication"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

  <process-archive>
    <process-engine>default</process-engine>
    <resource>expense_application.bpmn</resource>
    <properties>
      <property name="isDeleteUponUndeploy">false</property>
      <property name="isScanForProcessDefinitions">false</property>
    </properties>
  </process-archive>

</process-application>

ここまででBPMNはCamundaで自動的にロードされ実行可能な状態になりました。試しにSpring BootからCamundaを立ち上げてみましょう。

$ gradle bootRun

立ち上がったら http://localhost:8080/ にアクセスし、demo/demoでログインします。

f:id:itohiro73:20180116142223p:plain

Applications項目にあるTasklistをクリックします。

f:id:itohiro73:20180116142346p:plain

Tasklistが開き、現状ではリストは空の状態です。

f:id:itohiro73:20180116142650p:plain

さぁ、ここから経費申請フローを動かしてみましょう。

経費申請フローのデモ

今回は、承認者が承認する条件分岐で経費申請フローを試してみます。また、簡単のために申請者承認者経理の役割ははすべてdemoユーザーで実行します。

CamundaにデプロイされたBPMNのプロセスを起動するために、Tasklistの右上のメニューにあるStart Processをクリックします。

f:id:itohiro73:20180116142619p:plain

ポップアップで現れるメニューからExpenseApplicationをクリックすると、経費申請のプロセスが開始します。

f:id:itohiro73:20180116142859p:plain

この時点で「経費申請フォーム送信」イベントがトリガーされ、イベントに紐づけられたフォームが出現します。

f:id:itohiro73:20180116143715p:plain

適当な領収書の画像ファイルを選択し、必要な項目を入力し、Startボタンを押します。

f:id:itohiro73:20180116144114p:plain

ここで一旦Tasklistのページをリロードすると、承認タスクが現れます。

f:id:itohiro73:20180116145113p:plain

この承認タスクをクリックしてみると、今度は承認タスクに紐づけられたフォームが出現します。

f:id:itohiro73:20180116145031p:plain

demoユーザーにアサインするために、右上のClaimボタンを押すと、

f:id:itohiro73:20180116145307p:plain

承認タスクがdemoユーザー担当になります。

f:id:itohiro73:20180116145337p:plain

これで承認できるようになったので、ドロップボックスを「承認」のままCompleteボタンを押してみましょう。

f:id:itohiro73:20180116145624p:plain

すると、今度はTasklist経理担当の確認タスクが出現します。

f:id:itohiro73:20180116150004p:plain

以上で経費申請のワークフローが経理の確認待ちという状態まで遷移しました。

閑話休題)Cockpitでの実行中のプロセスの確認

デモの最中ですが、ここでいったんCockpitという機能を紹介したいと思います。

CamundaにはCockpitというダッシュボード機能がデフォルトで備わっており、デプロイされたBPMNや、現在実行中のプロセス、現在タスク完了待ちのユーザータスクなどが確認できます。

試しに上記のデモの途中の、経費を承認後(つまり、経理の確認待ちの状態)に、実行中のプロセスの状態をCockpitで確認してみましょう。

Cockpitにアクセスし、Running Process Instancesをクリックします。

http://localhost:8080/app/cockpit/default/#/dashboard

f:id:itohiro73:20180116150227p:plain

実行中のプロセスが表示されるので、ExpenseApplicationを選択します。

f:id:itohiro73:20180116151046p:plain

すると、下のように現在①プロセスが「確認」タスクのところにとどまっていることが可視化されます。

f:id:itohiro73:20180116152236p:plain

このように、Cockpitは実行中のプロセスをモニターするのに便利な機能を提供します。

確認タスクの実行により経費記録のサービスタスクをトリガー

さて、デモに戻ります。

上のCockpitでも確認できるように、最後に残るユーザータスクは経理の確認のみで、そのあとはサービスタスクである経費記録(ロギング)がトリガーされるはずです。

さっそく「確認」タスクを完了してみましょう。

Tasklistに戻り、「承認」タスクの場合と同様に「確認」タスクをClaimし、demoユーザーにアサインします。

f:id:itohiro73:20180116153128p:plain

f:id:itohiro73:20180116153156p:plain

今回のタスクは確認するのみなので、特に何も編集する必要なしにCompleteボタンを押します。

f:id:itohiro73:20180116153336p:plain

「確認」タスクをCompleteするとすぐに「経費記録」のサービスタスクが実行されるので、Spring Bootのログを確認してみましょう。

2018-01-16 15:41:35.131  INFO 16948 --- [io-8080-exec-10] sample.service.ExpenseRecorder           : 内容: Scala関西の交通費
2018-01-16 15:41:35.131  INFO 16948 --- [io-8080-exec-10] sample.service.ExpenseRecorder           : 金額(円): 28000.0
2018-01-16 15:41:35.140  INFO 16948 --- [io-8080-exec-10] sample.service.ExpenseRecorder           : 経費種別: 旅費交通費
2018-01-16 15:41:35.140  INFO 16948 --- [io-8080-exec-10] sample.service.ExpenseRecorder           : 承認済み?: はい

見事ログが出力されましたね!

今回は簡単なデモのためにログ出力としましたが、サービスタスクの実装はJavaのクラスで行われているので、外部のAPIを叩くもよし、データベースにストアするもよし、メッセージキューに送信するもよし、なんでもできます。

以上で今回の経費申請デモは完了です。今回の記事で実装したプロジェクトはこちらになります。

github.com

皆さんの手元でも、承認が「却下」になる分岐を実行してみたり、「却下」の場合の情報も出力するサービスタスクを追加して実行してみたり、遊んでみてください。

業務ワークフローをBPMNエンジンで実装するメリット

CamundaのようなBPMNワークフローエンジンを利用するメリットとして、私個人として一番大きいと思っているのは、ワークフローの「状態遷移」をバックエンドの実装と分離することができるところです。BPMNでモデル化できるような業務フローをバックエンドサービスとしてフロムスクラッチで実装しようとすると、どうしてもステートマシンのようなロジックを実装する必要がでてきます。手動で実装するステートマシンは結構楽しいんですが、業務要件に変更が加わった際に牙を向きます。ある状態から他の状態へ遷移する途中に新しい状態や分岐が必要となった場合、影響範囲の調査もなかなか厳しいですし、実装の変更工数はもとよりテストの工数もかさみます。さらにいうと本番環境でステートマシンから外れた例外的な状態遷移を実行する必要が出てきたときにニッチもサッチもいかなくなるという悲しみも抱えることになります(経験談)。

BPMNワークフローエンジンを用いて状態遷移をBPMNのモデルに集約するようにすると、状態遷移のロジックは分離され、バックエンドの実装は情報のストア(CRUD)やメッセージングに専念することができます。BPMNモデルそのものが業務要件を表現しているので、変更の影響範囲は可視化されます。新しい状態「遷移」を追加するにはBPMNのモデル側を変更すればよく、状態「情報」そのものはサービスタスクを用いてバックエンドにストアするような形になるでしょう。バックエンド側に遷移ロジックが入り込まないことで、変更に伴う状態の矛盾のようなバグが入り込む余地がなくなり、アジャイルで柔軟かつロバストシステム開発を実現できるのではないかと考えています。

さらに、バックエンドの情報ストアに関しては、Reladomoのように履歴管理が容易なデータアクセスレイヤーを利用できると業務アプリケーションとしてはさらにいろいろ面白いことができるでしょう。この辺はいつか別のブログで書けると良いなと思います。

BPMNに変更を加える際のデプロイの方法論に関してはいろいろ検証が必要だと思いますが、デプロイ前にワークフローの中間状態のプロセスを排除(全て終了まで持っていくか、全てを始点に戻す等)するような施策が必要になってくるかなと思います。

第三回のまとめ

今回はBPMNでモデル化した経費申請フローを用いて、Camunda上で実行させるのに必要なフォーム実装とサービスタスク実装、BPMNの設定を行いました。第一回〜第三回までの準備・モデリング・実装プロセスだけで実際に動作する業務アプリケーションが完成しました。

いかがだったでしょうか。思いのほか簡単にモデリングできるし、アプリケーションの実装も非常にシンプルです(実装のほとんどはフォーム側で、バックエンドに相当するJavaのコードは今回はほとんど書いてませんw)。

ここまででCamundaの持つモデリング駆動の開発の可能性と、ワークフローエンジンとバックエンドストアの責務分離の強力さをなんとなく感じてもらえたら幸いです。

次回は、BPMN単独ではなかなかモデリングが難しい、意思決定のためのモデリング記法であるDMNを学んでみましょう。今回までに作成した経費申請ワークフローを拡張し、意思決定プロセスを導入してみます。お楽しみに!

【第二回】経費申請のフローで学ぶCamundaの基本(BPMNモデリング編)

前回Camunda ModelerのインストールとCamunda Spring Boot StarterによるCamunda環境構築を行いました。

今回は、前回インストールしたCamunda Modelerを使って、「経費申請」の基本的なワークフローをBPMNでモデリングしてみましょう。

BPMNとは

BPMNとはBusiness Process Modeling Notationの略で、ビジネスプロセスをモデリングするための記法です。

ビジネスプロセスとは業務の一連の流れのことで、BPMNは、タスクの担当者や担当範囲、フローの分岐条件、システムが担当するタスクやメッセージのやりとりなどが明確かつロジカルに定義できる、定型作業としてのワークフローをモデルとして表記することを得意とします。

百聞は一見にしかずなので、経費申請のワークフローを下記のような簡単な業務要件としてモデリングしてみましょう。

  • ユーザーが経費申請のフォームを入力する
  • 承認者が申請を承認する
    • 申請が却下された場合、ワークフローは終了する
    • 申請が承認された場合、経費の記録のためのタスクに経理アサインされる
      • 経理が確認を終了すると、自動的にシステム上で経費が記録され、ワークフローは終了する

上記のような要件をBPMNで表現してみると下のようなモデルが出来上がります。

f:id:itohiro73:20180115112911p:plain

GitHubにもあげてあるのでCamunda Modelerから開いて見ることも可能です。

ぱっと見ただけでも直感的に何を表現しているかわかりやすいかと思いますが、今回はこのBPMNのモデルを作成するのに必要ないくつかのコンポーネントの説明と、Camunda Modeler上でのコンポーネントの作成方法を解説していきます。

プール

ひとかたまりのビジネスプロセスや、あるプロセスを担当する一番高次な概念(たとえば部署等)を表現するためのコンポーネントを「プール」と呼びます。プールは、Camunda Modeler上の左下にあるこちらのボタンを押すと作成することができます。

f:id:itohiro73:20180115112950p:plain

プールの詳細についてはこちらのドキュメントに詳しく記載があります。

今回は「経費申請」のためのプロセスということで、こちらのボタンから経費申請のプールを作成すると下のようになります。

f:id:itohiro73:20180115113026p:plain

プールの名前(今回は「経費申請」)は、プールを選択した際に右側に表示されるProperties PanelName欄に入力すればOKです。

f:id:itohiro73:20180115113055p:plain

プール以外の他のBPMNコンポーネントについても、名前に関してはすべて同じProperties PanelName欄から記入することができます。

スイムレーン

スイムレーンとは、プール内で特定のタスクの担当範囲を分割するためのコンポーネントです。詳細についてはこちらのドキュメントを参照してください。

今回の経費申請ワークフローでは、申請者承認者経理という担当範囲を定義しているので、経費申請プールにこのスイムレーンを作成してみましょう。

まずはプールを選択した際に右側に現れるDivide into two Lanesというボタンを押してみましょう。

f:id:itohiro73:20180115113135p:plain

下のように、プールが2つのレーンに分割されます。

f:id:itohiro73:20180116082730p:plain

次に、どちらかのレーンを選択して右のメニューのAdd Lane aboveもしくはAdd Lane belowどちらでも良いので押して見ましょう。

f:id:itohiro73:20180115113159p:plain

これでスイムレーンが3つ作成できたので、それぞれ申請者承認者経理としましょう。

f:id:itohiro73:20180115113234p:plain

イベント

BPMNには「イベント」という概念があり、ワークフローのスタート起点となるイベントStart Eventであったり、プロセスの途中に発生するエラーハンドリングのようなイベントIntermidiate Event、プロセスの終点となるイベントEnd Eventを表記することができます。

Camunda Modeler上では左のメニューにあるこれらの丸いコンポーネントがそれぞれStart EventIntermidiate EventEnd Eventとなります。

f:id:itohiro73:20180115113558p:plain

さらに、それぞれのイベント内にもメッセージを起点とするイベントMessage Eventや時間発火するイベントTimer Event、特定の条件で発動するイベントConditional EventSignal Event等さまざまな種類のイベントが存在します。今回はこれらの紹介は省きますが、イベントの詳細はこちらのドキュメントを参照してみてください。

f:id:itohiro73:20180115114027p:plain

今回は起点イベントとして「経費申請フォーム送信」というStart Eventを作成してみましょう。左のメニューからStart Eventを選択して申請者レーンに配置をし、「経費申請フォーム送信」という名前をつけるだけなので直感的にいけるかとおもいます。

f:id:itohiro73:20180116082758p:plain

タスク

さて、一旦起点イベントから「経費申請フォーム」が送信されたら、今度は承認者が申請フォームを承認する必要があります。BPMNでは、このような、人やシステムが何かしらのアクションを起こす概念を「タスク」と呼びます。

タスクにもさまざまな種類があり、こちらのドキュメントで詳細を確認することができます。

「経費申請」フローで使用するタスクは、人がアクションを起こすUser Taskと、システムがアクションを起こすService Taskの2種類で、今回の例ではUser Taskは「承認」と「確認」、Service Taskは「経費記録」のタスクをこなします。それぞれ下のようなBPMNコンポーネントとなります。

f:id:itohiro73:20180115114159p:plain

f:id:itohiro73:20180115114213p:plain

ここでは承認者が承認するユーザータスクUser Taskを作成してみましょう。

先ほど作成した「経費申請フォーム送信」イベントを選択すると、タスクを追加するためのAppend Taskというメニューが表示されるので、こちらをクリックします。

f:id:itohiro73:20180115114650p:plain

タスクを配置する場所をマウスで選択できるので、承認者レーンでクリックします。

f:id:itohiro73:20180115114851p:plain

すると、下記のようなメニューが出現するので、レンチマークのChange Typeを選択し、

f:id:itohiro73:20180115114936p:plain

タスクのタイプとしてUser Taskを選択しましょう。

f:id:itohiro73:20180115115001p:plain

名前を承認とすることで、下のようなモデルが出来上がります。

f:id:itohiro73:20180115115031p:plain

これでUser Taskを作成することができました。

ゲートウェイ

ビジネスプロセス上では、さまざまな状況に応じてプロセスが分岐したり、分岐したフローが合流したりといった事象を表現をしたいことがあります。BPMNでこの分岐や合流を表現するのがゲートウェイです。

ゲートウェイの詳細はこちら

今回は、経費申請が「承認されたかどうか」という判断と分岐するためにExclusive Gatewayというコンポーネントを使います。

f:id:itohiro73:20180115115117p:plain

Exclusive Gatewayは、ある条件(例えば今回は「承認済みかどうか」)に対して一つの分岐フローだけが選択されるゲートウェイになります。

承認ユーザータスクから「承認済み」かどうかを判定し分岐するためのExlusive Gatewayを足します。

f:id:itohiro73:20180115115135p:plain

作成されたExclusive Gatewayに「承認済み?」という名前をつけ、「いいえ」の場合の分岐に「経費却下」というEnd Eventを作成しましょう。下のようにEnd EventAppend EndEventから作成することができます。

f:id:itohiro73:20180115115207p:plain

f:id:itohiro73:20180115115235p:plain

「承認済み?」の条件が「はい」だった場合の分岐には、経理のレーンに「確認」のUser Taskを作成します。

f:id:itohiro73:20180115115304p:plain

仕上げ

ここまででプールスイムレーンイベントタスクゲートウェイというBPMNの基本的なコンポーネントを使用して「経費申請」のワークフローのほぼ最終段階までモデリングしてきました。最後に、これまでと同様の流れで「経費記録」というService Taskと「経費受理」というEnd Eventを作成すれば今回のBPMN図は完成になります。

f:id:itohiro73:20180115112911p:plain

最初にも書きましたが今回作成したBPMNはこちらのGitHubにあげてあるので、Camunda Modelerから開くことができます。最終成果物を確かめてみたい方はこちらからどうぞ。

https://github.com/itohiro73/camunda-expense-example/blob/2-bpmn-model/src/main/resources/expense_application.bpmn

第二回まとめ

いかがでしたでしょうか。業務要件の定義さえしっかりされていれば、Camunda Modelerを用いたBPMNモデリングは非常に直感的かつ簡単にできることがおわかりいただけたのではないでしょうか?

今回は「経費申請」という単純なワークフローを題材に、下記のBPMNコンポーネント用いてビジネスプロセスをモデリングしてみました。

次回は、今回作成したBPMNモデルをCamundaの実行環境にデプロイし、いくつかのフォームとJava実装と連携することで、実際に動く経費申請アプリケーションを作成してみます。お楽しみに。