2022近況

前回の記事から1年以上開いてしまった・・・ 暗いニュースが多い昨今ですが元気にやっております。

転職したぞい

去年7月、ずっと副業でお手伝いしていたアルプ株式会社に転職しました。 特に不満があって前職を辞めたというよりはチャレンジするぞ〜というノリでの転職です👍

次は起業したいかも〜と思ってたのですが、創業時からサーバーサイドの設計に関われていて、インフラも創業時から複雑性を見据えて設計されており、事業が解決する課題もめちゃくちゃ大きく社会的意義もでかいという状況がすでにあるならそっちに入るのが合理的じゃね・・?というか1からこんな環境作れてるのを再現するの無理では・・・?となったのも大きな要因でした。

前職のチームは最高なので名残惜しい気持ちでいっぱいではありましたが、チャレンジして良い環境がつくれたらまた一緒に働く機会もできる・・・!と思って頑張っております。

thealp.co.jp

これまでのキャリア的にはサーバーサイドとインフラ兼務のロールが多かったのですが、アルプにはハイパー優秀SRE人材が揃っているので自分がインフラを見る必要がまったくなく(というかワイでは太刀打ちできんw)、 サーバーサイドに専念させていただいております。

ドメインがとても広く深いため、設計周りのトレードオフを複雑なアプリケーションに立ち向かうための方向に全力で倒しつつ、FPやら自動生成やらで楽する方向も捨てない方針で進めています。

エンジニアメンバーはラストマンシップの強い設計大好きマンが多く、設計議論やみんなのこれまでの経験を聞いたりするのが楽しかったりします。

CustomerSuccess,Salesチームも、プロダクト理解がエンジニアのそれに近いデータ設計周りまで及んでいる人がいたり、かなり深い仕様問答をしていたり、顧客の事業状況に伴走するソフトスキルなどなど凄すぎてかなりビビります。

総じてアルプがValueの芯として掲げている真摯さをもったいい奴ばかりでとても気持ちよく仕事が捗っています。

ScalaMatsuriで話します

光栄なことにCFP通ったので、Alp-original ’Eff’ pearls というタイトルで話します・・・!

これといったベターな実装方法がない認可周りを、Stateモナドを使ってスコープをエフェクトに積み、 そのStateを利用して認可判定をするエフェクトを作ることでシンプルかつ統一的な権限管理をする仕組みの話とかとか、 面白い内容に仕上がったのでご興味のある方ぜひ〜

今年はオンライン開催(youtube live)で、なんと無料で視聴できる・・・!

scalamatsuri.org

Lookerのグラフ埋め込み機能が素敵だったので紹介する

この記事は『Looker Advent Calendar 2020』11本目の記事です。

Looker Advent Calendar 2020 - Qiita

f:id:ma2k8:20201210204758p:plain

今年の5月頃、副業で参画しているALP社で「分析機能を作るぞ」というプロジェクトに技術選定の段階でJOINし、Lookerを利用したところ超絶便利だったので、 今回はその便利機能の中からグラフの埋め込み機能について紹介したいと思います。

ちなみに分析機能は10/5にプレスリリースを出すことができました。🎉🎉

prtimes.jp

実際に埋め込み機能を実装してみると、Lookerとの連携に必要なサーバーサイド&フロントエンドの実装はとてもシンプルに済み、 権限によるデータの出し分け周りなども柔軟かつ表現力高く実現できたので、めちゃくちゃオススメです。

また、SaaSとしてかなりイケている設計をなされている部分が多かったので、とても勉強になりつつ、楽しく実装できました。 イケている部分の一部ですが、連携方法がとてもエレガントである旨の記事を以前書いたので、よければこちらもどうぞ。

SaaSのエレガントな連携とは - まっちゅーのチラ裏

選定理由

ALP社が提供しているScalebase というサブスクリプション管理SaaSの1機能である管理画面に分析機能を追加するという要件で、 グラフの埋め込み機能があるものの中から選定し、以下の点でLookerの採用を決めました。

  • LookMLでプログラマブルにデータを管理できる
  • プロジェクトをGithubで管理でき、GitOpsに載せやすい
  • Lookerがパフォーマンスのボトルネックになりにくく、エンジニア側で自由度高くチューニングができる
  • 埋め込み機能がある(顧客へ提供する自社サービス(Scalebase)の画面に埋め込みたかったので必須)

埋め込み方法

以下3つの方法から選択できます

パブリック共有、インポート、埋め込み

プライベート埋め込み

シングルサインオン(SSO)組み込み

今回の要件では、顧客に画面を提供するので、最も柔軟にハンドリング可能な "シングルサインオン(SSO)組み込み" で実装しました。

シングルサインオン(SSO)組み込みのフロー

基本的には、サーバーサイドからLookerの管理画面で生成したシークレットを含めた埋め込みURL生成リクエストを送り、返ってきたURLを フロントに埋め込むだけというシンプルなフローです。

f:id:ma2k8:20201210214112p:plain
embed_sequence

実装方法

サーバーサイド

公式でJava,C#,Node.js,Python,Ruby,PHPのサンプル実装を用意してくれています。 基本的にはURL生成リクエストをLooker側に投げるだけなのでシンプルに済みます。

github.com

Scalebaseでは、以下の機能も実装したのでそれらに必要なモデルもコード化しました。

  • ユーザー属性によるデータの出し分け
  • Scalebase側の権限に応じたLooker側の権限ハンドリング(viewer or explorer)

埋め込みURLで指定できるパラメータをモデル化したので、Scalaのコードですが貼っておきます。(雑に持ってきたのであくまで参考程度で🙏)

github.com

ユーザー属性によるデータの出し分け

ここがかなり便利だったので紹介します。

まず、LookMLのexploreで以下のようにaccess_filterを設定します。

f:id:ma2k8:20201210222035p:plain

こうしておくと、URL生成リクエストにユーザー属性として provider_id="ID_A" を渡すことで、 exploreの情報にアクセスする際にWHERE fuga.provider_id = "ID_A" が自動で追加されデータの出し分けが可能になります。

とても柔軟かつ表現力が高く、少ない工数で安全なデータの出し分けが可能になるのでとても助かりました。

フロントエンド

表示のみであればiframeに埋め込むだけで済みました。 Scalebaseでは、独自グラフの生成や、Lookerからのeventハンドリングしてフィルタのコンポーネントを iframeの外に出したりといったことも行っています。

画面からシュッと埋め込みURLを生成するツールがあるので、フロントの動作確認は気楽に実施することができます。

SSO Embed Constructor

完成画面

完成した画面はこんな感じ

f:id:ma2k8:20201211002037p:plain
分析画面

ドリルダウンもいい感じにできるので細かい値を見るのも便利です。

f:id:ma2k8:20201211002114p:plain

最後に

BIツールの枠に収まらない、プログラマブルに管理可能な分析基盤としてとても便利なLookerですが、 埋め込み機能も少ない工数で安全かつ表現力高く実装することができました。

backendもfrontendもかなり少ない工数で実装でき、Looker上でのデータの定義や見せ方、GitOpsの構築や開発フローの洗練に注力することができたので、 Lookerを選んで本当に良かったと思っています。

気楽にデータの可視化をしたいだけであれば色々な選択肢があるかもしれませんが、 プログラマブルかつGitOpsを交え、かっちりデータの管理がしたかったり、データ分析機能や埋め込みグラフの実装が必要だったり、 グラフを作る人がエンジニアだったりするのであれば、持続可能な基盤をしっかり作ることができるのでめちゃくちゃオススメです。

DDDにおいて、外部連携先のコールバック処理は鬼門となるので注意したい話

DDDを実践するにあたり、外部SaaS等のデータ構造がドメイン層まで食い込んでいるのをよく見かける。

これは複雑な連携であればあるほど避けるべきなのに、複雑な連携こそ、そうなっている事例をよく見る。

どうしてか考えてみたところ、それらには共通して「コールバック」による状態通知のような処理が存在していることに気がついた。(それ以外の場合は普通にアーキテクチャモデリングに問題がありそう)

これを「自組織のフロントエンドやアプリとのやりとり」と「コールバック通知のやりとり」で分けて考えると問題点に気づきやすいのではと思ったので書いてみる。

自組織のフロントエンドやアプリとのやりとり

自組織だったり、公開APIとしてのやりとりで使うrequestやresponseは、割とドメインモデルに近い形をしている。 ドメインモデルに近い形では厳しい場合にCQRS+ESにしたり、一部そのままでは都合の悪い場所をプレゼンテーション層で加工したりする程度である。

PrimaryAdapter -> requestToDomainModel -> UseCaseArgs(has a domainModel) -> UseCase という流れで、ここに問題はない。(domainModel変換なしでUseCaseArgsに詰めてUseCaseでRepositoryを引くパターンももちろんある)

コールバック通知のやりとり

一方、この方法で外部連携先のコールバック通知を実装するとどうだろう

PrimaryAdapterが受け取ったものをそのままrequestToDomainModelとしてしまうが、コールバックのリクエストの形は外部連携先が管理しているただのデータ構造だ。

これが無遠慮にドメイン層に流れ込んでくることになる。これを許容して良いのだろうか。

これでは外部連携先のデータ構造がドメインに漏れ出てしまい、変更容易性も凝集性も結合度もすべておじゃんである。

ではどうすればいいのだろうか

この外部のデータ構造そのものはUseCase層に置き、ただのDTOとして扱うべきだと思う。 これはUseCaseの引数以上の意味はなく、我々のドメインには全く関係がない。

流れとしては👇 PrimaryAdapter -> requestToDto -> UseCaseArgs(has a DTO) -> UseCase

※もちろん、内部のパラメーターの一部がドメイン足りうることはある。外部連携用IDとか。だが、構造そのものがドメインになることはほぼない。

こうすればドメインは真にやりたいことに注力でき、構造も外部連携先に左右されず、処理も変に外部連携先のデータ構造と密結合になることもない。(変換層があるとはいえ、同じ形に変換していたら疎結合とは言えないのではないだろうか)

本来、requestToDomainModel とするのも手間を嫌うのと実用上問題ないからであって、理想論はすべてDTOを噛ませるべきなのだろう。(手間がでかすぎるしメリットがないのでやらないけど)

保存はどうするのか

おそらくここが意見の分かれるところだと思われる。 外部連携先からの情報はもれなく保存していたほうが無難である。だが、ドメインとしては扱いたくない。

自分は👇で書いたDelegateProcess相当の処理でDTOをデータモデルに変換して保存している。 再考 - ドメインサービス - まっちゅーのチラ裏

こうすれば、ドメインはいつでも情報の過不足なくコールバック通知の情報を元にモデルを組み立てられるし、構造はドメイン層に漏れ出さない。

他には、外部連携用のコンテキストを分けてしまい、そこではガッツリドメインとして登場させる等の手法も考えられるがそこまでやるのはオーバーキルかなと思っている。

考え方

UseCaseに何もやらない作業者を想定しない という考え方が良いのかなと思う。

ECサイト作る際、商品配送を外部の配送会社に委譲することを考える。

配送会社ごとに伝票のフォーマットが違うと仮定したとき、ドメイン層に外部連携先のデータ構造を置くということは

  • ①作業者 -> ユーザーの配送情報をください!
  • ドメイン層 -> ホイ。配送情報です。
  • ③作業者 -> azs! 伝票書きたいんだけど書き方がわからん!配送会社Aの伝票の左上の欄には何を書くべきですかね?
  • ドメイン層 -> 住所やな。というかハイ。書いといたわ。伝票。
  • ⑤作業者 -> わーい。配送会社に出しておきますね!

という流れである。めちゃくちゃドメイン層に頼りまくってる。

データ構造を置かない場合の流れは👇

  • ①作業者 -> ユーザーの配送情報をください!
  • ドメイン層 -> ホイ。配送情報です。
  • ③作業者 -> azs!伝票書いちゃお〜〜
  • ④作業者 -> 伝票できた!配送会社に出したろ!

むちゃくちゃスムーズで、いちいちドメインが伝票の構造を把握する必要もない。

もちろん、ドメインが伝票相当の構造に興味があるケースもあるかもしれないが、ほとんどのケースでは興味がないはずで、伝票のどこに何を書くかくらいの情報は、知識というよりは作業者がアドホックに判断するものとした扱ったほうがスマートでないだろうか。

表示したいという1点だけで興味があるケースもあるが、それは伝票IDとTypeをpresentation層に持っていって直接データ引いちゃったり、CQRS+ESにしたり、UseCaseで表示用のDTOを持っちゃってPresenterでResponse用のモデルに変換したりで回避できる。

まとめ

コールバック通知等の情報を同期する系の連携は密結合になりやすいという話でした。

外部連携先オブジェクトのライフサイクルそのものにめちゃくちゃ関心があるのであればドメインに顔を出さざるを得ないと思いますが、構造そのものを出す必要があるのかはめちゃくちゃ考えてできるだけ避けていきたいですね。

自分が大規模システムで組むアーキテクチャは、下から上の依存は構造として参照すらできないように制限してるのですが、下へ下への圧力が強くなりがちなのかもと思うところがあったので、上の層における部分は置くようにし、下から上への圧力も意識してドメインをシンプルにし、本来やりたいことに注力できるようにしていきたいと思います。

モジュラモノリスの実現方法

ほぼ創業時から副業で手伝っているAlp社の同僚がモジュラモノリスの実現方法をScalaMatsuri2020で発表したので気になる方はどうぞ。

横の境界だけきった状態から、途中でモジュラモノリスに舵を切ったのでDB分割はこれからですが、それ以外の部分はキレイに分離できています。

自分がモジュラモノリスに重要だと考えているのは以下の2点です。

  • いかに少ない手数でマイクロサービス化できるか
  • マイクロサービス化した場合と同じ制約をコードに持たせられるか
    • パッケージで制限するのは少し制約として弱いと思っている。

この方法は、アプリ側は内部通信用adapterを叩くclientのDIを差し替えるだけでマイクロサービスとして切り出せるし、境界外のコードは参照すらできないようになってるのでマイクロサービスと全く同じ制約をもたせられている。とても良い感じに実現できた👍

ScalaMatsuri 2020 | アジア最大級の Scala のカンファレンス

speakerdeck.com

マイクロサービスでチームを分離したくないマン

コンウェイの法則とかで、マイクロサービス=組織 という話になることが多いなと感じる。

正解の場合もあるし、不正解の場合もあると思っていて、個人的には小さいチームでもマイクロサービスをやるメリットは技術的にも組織的にもあると思う。 そのメリットを無視してすぐ組織の話に持っていきたくないので、基本分離したくないマンとしての主張を書いておく

技術観点でのメリット

いまさら語るまでもないけど、

  • ドメイン境界の分離
  • デプロイ独立性
  • リソースの最適配分
  • 障害の局所化(サーキットブレーカー等)

このうち、ドメイン境界の分離だけはモジュラモノリスで対応可能だが、あとの3つにはマイクロサービスが必須。(もっとあるかも)

この3つが必要なのにモノリス or モジュラモノリス で進める判断をするということはシステムの表現力を落とすことに直結する。

もちろん、複雑度は増すし難易度も増す。熟練のサーバーサイドエンジニアとインフラエンジニアは必須。

※別の記事でも言及してますが分散モノリスにならないようアーキテクチャ側で制約をもっている。チームが分かれてなくても分散モノリスの回避は可能。

※モジュラモノリスはどこまでも分離を前提として設計しています(アプリ側はDI差し替えたら分離完了する)。そうしないとあまり意味がなさそう。

組織、チーム観点でのメリット

開発を進めていき、コードベースが大きくなっていくと「全てを把握してコードを書く」ことがどんどん難しくなる。

ここで安易に人数を投入して把握すべき単位を小さくするアプローチは、個人的にはかなりコスパの悪いアプローチだと思っている。

  • 安直なコンウェイ(というかSRP=組織構造?)否定派のワイ。なので、リポジトリもチームも別でやるMicroserviceには基本否定派である。
    • 完全個人的見解ですがAWSは各サービスの独立性が強くて(1サービス1プロダクトと捉えられる)上手くいってるし、Netflixはテクニカルな単位でサービスを切ってる&ハイパー高給と強い人事施策で組織的なコンテキスト跨ぎを促進して上手くいってると捉えている。
    • それ以外のケースではリソース効率だったりアジリティだったりの面できびしいことも多いんじゃないだろうか(異論は認める)

具体的には以下のようなことが考えられるからである。

  • エンジニアは把握すべき単位を超えた部分に対する理解や責任が浅くなり、開発の速度や機能開発の質が悪くなる
  • 案件をすすめるにあたり、コミュニケーションを取る対象が増え、アジリティが低くなる
  • 開発チーム内でサイロ化が始まり、やるべき破壊的な変更に対し実行する判断を下せなくなる(めんどくさくてやらなくなる)

この辺はラストマンシップをもった人で構成されているチームであればある程度解消できるが、そもそもラストマンシップをもっている人にとってはこの垣根は邪魔なものでしかない。

個人的には適切に関心事が分離されているコードベースであれば、1プロダクトであればかなりの規模のコードでも少人数で回せると思っている。(少人数の定義はむずいが、1つのプロダクトに100~1000人のエンジニアは絶対要らないはず。もちろん場合にもよると思うけど)

理由は 

  • 適切に関心事が分離されていれば、全てを把握してなくても「今触りたい機能に関係する部分」だけ知識のアップデートをして開発に取りかかれる
  • 誰がどの機能に詳しいなどの偏りは出るが、むしろそれによってアサインが明確になりレビューが捗る(その機能に詳しい人+そうでもない人をレビュワーとしてアサインする等)し、チームメンバーのコミュニケーションも円滑になる(経験則)
  • 複数の機能に詳しい人が育ちやすく、それによって案件を一人で遂行できる能力がつき、ラストマンシップが身につく といった流れができる。

上記のようなメリットを享受しつつ開発時のコミュニケーションも最小で済ませることができる。

単機能を担当するような組織だと担当機能の変更を好まない性質が出やすい。

  • 変更しないほうが障害リスクもないし対外的に見て得なことが多い。スケジュールの握りも過剰に安全性に倒しまくったほうが得になる。
  • 常にシステム全体の利害関係者であるべきだが、その状態が維持しにくい。維持できないと適切な機能開発は難しく、歪になっていく。
  • ○○チームがあまり要望聞いてくれないから勝手に○○機能実装しました。みたいな例も聞いたことがある・・・。馬力のあるやつの無駄遣いすぎる。

以上のようなことを考えないで済むのはかなりのメリットだと思っている。

まとめ

現職では、19のマイクロサービス(Scalaで76万clocほど)は15人ほどのサーバーチームと、5人のSREチームで管理している。(マイクロサービス化した頃は、7つのサービスにサーバー3人、インフラ兼サーバー1人だった) それらは案件ベースで協力して動くが、組織的観点では同じチームだし、案件に必要ならどのマイクロサービスも触る。(クレイグ・ラーマンの法則に近い動き方なのかな?あんまり詳しくないのでわからないけど)

普通に事業的にも、開発的にも上手く回ってるし、分割したら上手く回らない未来しか見えない。(少なくとも自分のチームでは)

これからチームがどんどん大きくなっていっても、少なくとも1サービス-1チームの分割はしないんじゃないかなーと思っている。

ただ、上手く回ってるのは、以下の要素も大きいのかなーと思うので、どこでも適用できる話ではないのかもしれない。

  • Scala+Effによるシステムの表現力
  • CleanArchitectureによる分散モノリスを防ぐ制約(例えば👇)
    • 内部通信用アダプタ(gRPC)と外部通信用アダプタ(http)を分けている
    • ドメインに他サービスの関心事が入ってこないようRepositoryとは別で出力OnlyのDIPをしている再考 - ドメインサービス - まっちゅーのチラ裏
    • sbtプロジェクトによる、コードを直接参照できない強固な縦横の境界付と、依存方向を単方向に強制する制約
  • 開発が普通にできるレベルのエンジニアリング力がありつつ、PM力もすごい強いPMが複数いる
  • インフラもk8sで運用が楽。
  • CIも工夫して手間なく複数サービスのデプロイができる

とはいえ、ECSやk8sだったり、CIの仕組みだったり、マネージドサービスだったりでマイクロサービスもどんどん運用が楽になってきているし、これからもなっていくだろう。

その時流の中、チームが小さくて組織とマッピングできないからといって検討もせずにコンテキスト分離のメリットを捨ててしまうのはもったいないと思う。

小さいチームでもマイクロサービスは十分有効で、組織分割は必須ではないという意見でした。

※マイクロサービスの前にモジュラモノリスを考えるべきではある。デプロイ独立性等は不要で、縦のコンテキスト分離だけ必要ならそれで十分なので。

※組織を分割する必要のない小さいチームでもコンテキスト分離を試みるメリットについて書いたのは👇 認証と認可と課金とコアドメインを分離したシステムは勝てるという話 - まっちゅーのチラ裏

※追記 コンウェイってより逆コンウェイじゃね?と教えてもらって、たしかにそうっぽい。と思ったので逆コンウェイの引用も貼っておく。 逆コーンウェイ戦略とDevOps, Microservices, Agile | an Agile Way

ざっくり👇のように理解した。

  • コンウェイ => 組織 -> システム設計となりがち。という知見の話
  • コンウェイ => システム設計にあわせて、組織を変えよう。という戦略の話

※追記 マイクロサービス化 -> 組織分割の間にはグラデーションがあると思っているだけで、組織分割自体を否定したいわけではありません。すぐそっちに話を持っていくのが違うなーという。特に「コアドメインのコンテキスト分離の境界をどこにするのか」は、めちゃくちゃ難しいので組織分割を先にしてコンテキスト分離すると意味不明な単位になるし、組織を専任で当てるほどでもない小さい単位でも、分離すると有用なケースも往々にしてあると思う。

再考 - ドメインサービス 

自分が大規模システムで組むアーキテクチャは基本的にはCleanArchitectureを踏襲しているが、その中の構成要素であるドメインサービスだけは少し独自(?)の解釈をしていて、書籍などでよく見る

という責務の他に、外部システムへの委譲処理だったり、共通UseCaseのような責務も持たせている。

これは、自分が「xxService」という命名にトラウマがあり(何でも置き場になりがち)、単なるServiceだとコントローラやらプレゼンターやら、どこから呼ばれても違和感がない様に見えてしまうから、とりあえずDomeinServiceへ寄せている経緯がある。

※ここで語るのは、あくまで大規模想定で、小さいシステムならこんな事を意識する必要はないはず。

※あくまで自分の考えで、一般的ではない可能性があることをご了承ください。

なぜDomainServiceに寄せているのか

domainServiceと呼ぶのも👇のような考えがあったからで、DIPして技術関心事へある程度処理を委譲しつつドメインロジックを実現する場所として色々定義したいからだった(インターフェースはドメイン層だし、ドメインロジックと呼べるよな!?というノリで寄せている。

  • 銀行の順番待ちみたいな実装をすると、待ち番号が書いてある紙を渡すUseCaseがあったとして、待ち番号を発行するロジックをドメインで持つのは、現実世界では待ち番号を案内担当者がメモって採番する行為に等しいように思う。
  • 担当者が2人いると採番するのにコミュニケーションが必要だし面倒。採番はなんだかよくわからんシャシャシャーと紙を吐き出す機械に任せたい=技術的関心事に渡そうぜ みたいな話だと思っている。
  • 技術的関心事によって実現するドメインに必要な要素を都合よく隠蔽できる場所(Repositoryに近しいドメインとインフラの境界の処理で、Repositoryは入出力、DomainServiceにもたせているのは出力のみ)だと思っている。
  • それが正しいかはしらんけど、そうすると絶対的に便利だなとおもって書いている。(めっちゃ適当に書いてるのでツッコミ大歓迎w)
  • iDDDのサンプルソースなどでは、repositoryのDIだけしてdomainServiceはドメイン層にベタ書き実装があったり、書き方が揺れている。悪いとは思わないが、repoがある時点でpureなdomainServiceではないし、委譲処理を書くところがない(または曖昧)のは分散モノリスになりやすそうという個人的な印象。

ドメイン層で表現しようと思えばできるという余談

コストに見合うとは全く思えないけど、表現しようと思えばできる。

  • Effを使えば受付番号採番機みたいなドメインモデルを定義して、採番的なドメインロジックに技術津的関心事を注入することも可能だが、そこまで細かいモデリングが必要なケースとそうでないケースがありそうだなと。(隠蔽したほうがシンプルになるはず)
  • ActorSystemのアプローチであれば、担当者同士の状態が常に問い合わせられる状態で存在しており、コミュニケーションがとれる(オンメモリに状態を持って常駐している)ので、合意形成のアルゴリズムなどをガチガチに実装すれば採番も可能にできるかも。
    • とはいえ基本的には、ActorSystem以外のアプローチだとDB等に副作用を保存しているので都度都度スナップショットを取り出してこないといけない。
    • 取り出すオブジェクトが一意じゃなければ並列性等考慮すると採番のロジックも普通にバグってしまう。
    • こういうのを、redisなどのシングルプロセスのkvsに一旦保存してincrみたいなコマンドで採番していけば、「一旦保存」の部分さえ気をつければincrは全く同時に叩いてもかぶった番号を返すことはないのでサクッと採番が可能になる。
    • これは「採番機」みたいなオブジェクトを実装側に隠蔽して、ドメインでは意識しない良いテクニックだと思う
    • 「採番はなんだかよくわからんシャシャシャーと紙を吐き出す機械に任せる」ということをコードで定義するとこうなる
    • 採番機をドメインにおくのは、めっちゃ採番機に詳しい担当者を前提にしているようなことな気がしている(そんなやつおらん)

細分化してみよう

とはいえ、domainServiceの実装がsecondary-adapterにあるのは違和感はあるっちゃーあるし、共通UseCaseをdomainServiceと呼ぶのも違和感がある。

細分化してみると現状のドメインサービスは以下の3つ存在していそう

  • pureなdomainService
  • 汎用業務プロセス(UseCaseからUseCase呼びたい時に逃がすやつ
  • 技術関心事への委譲手続きプロセス(現状はここが多そう)
    • 委譲には現状のドメインサービスでやっている👇のような処理をドメインとAdapterの境界に持ってくることで表現力を上げている
    • 外部SaaS連携する際の、外部連携APIへのリクエストとドメインオブジェクトのマッピングドメイン要件でもありビジネス要件でもある(ドメインのどのパラメーターがリクエストに対応するかはビジネス要件だが、リクエストの型自体や通信の方法はシステム要件)という曖昧な処理の置き場所(そうすることでドメイン層の汚染を防げている)
    • ある要件を実現するために技術関心事の力を借りる場合(上の例の採番ロジックでいうRedis)

これを、

  • pureなdomainService => DomainService
  • 汎用業務プロセス => JointProcess
  • 技術関心事への委譲手続きプロセス => DelegateProcess

として分けるとかなり腹落ちがした。 JointProcessの中でDelegateProcessやDomainServiceを呼んでもOKだし、UseCaseから呼んでもOK。

JointProcessはUseCaseに実装ベタ書き、DomainServiceはDomain層に実装ベタ書き、DelegateProcessはUseCase層にインターフェースを置いてSecondaryAdapterで実装注入(DIP)する。

自分の中で一番表現したかったのはDelegateProcessの部分。これがなしでRepositoryのみだと SaaSのエレガントな連携とは - まっちゅーのチラ裏 でも書いたとおり、双方向の依存を作り、分散モノリス化しやすいと思う。(多分みんなServiceとかに切り出して書いてるのかな。)

DelegateProcess相当のものを用意することで、たとえ双方向の依存が必要な連携方法だったとしても隠蔽でき、UseCase層からは単方向の依存に見せかけることができる。

クソ雑な図で示すと、Repositoryと委譲処理の処理の流れのイメージは👇のような感じ。(

f:id:ma2k8:20201019172420p:plain
ca

また、サードパーティapiのコールバック等、コントローラーからusecase通って〜という処理を書かないといけないからサードパーティapiのデータ構造を丸コピしたようなドメインを定義しないといけないみたいなケースも、コントローラーからusecase -> deligate processと呼んで、ドメイン層を通さずにデータ構造をそのままDBに保存してしまうしまうという手が使えそう(UseCaseにDTOは要るが、ドメインは汚染されない。ドメインに侵食しないのが大事。外部のデータ構造なんてドメインは興味ない。) サードパーティapiのデータ構造は基本全部保存はしといた方がいいが、ドメインに顔出した方がいいかは全く別問題なのでこの手は有効だと思っている。(保存さえしておけば、後で情報落ちで困ることもないし、こういう情報をドメイン層介さず保存することで、自分たちのドメインモデルとして柔軟にReposiotryで組み立てることができる)

委譲プロセスがないと、別コンテキストの関心毎が各所に散らばって分散モノリスっぽくなってしまうと思っていて、みんなどこに書くんだ!と思っていたが名前をつけて思考が整理され、スッキリした。(最近、巷で分散モノリスの話が出てきてるが、これも委譲プロセスをアーキテクチャの中に見いだせていなからおきてると思ってる。UseCase層に登場するからドメインや!的なノリ。委譲プロセスとして切り出せば関心事は最小にできる

JointProcess,DelegateProcessの命名は適当なのでチームの人とかと話していい名前を見つけてみる(個人的には割としっくり来ている)

なぜApplicationServiceじゃダメなのか

目的自体は委譲なので、DIPで注入さえできればApplicationServiceという名前でもいいんだけど、UseCaseと混同する気もするし、「どこかにお願いする処理」を表す言葉として表現に乏しい気がしている。

なぜDomainServiceのままじゃダメなのか

別にダメじゃないと思う。

個人的には なぜDomainServiceに寄せているのか に書いたとおり、DelegateProcessはDomainServiceと呼べると思っているし、アーキテクチャの表現力の観点では全く問題がないので。 ただ、JointProcessはDomainServiceとは呼べないなという違和感と、DelegateProcessは名前をつけて意識しやすくしてあげることで分散モノリスのような悲劇(もちろん途中から分離した場合はある程度仕方ない)を回避できるんじゃなかろうか。と考えている。

あと、DomainServiceだとドメイン層に置かないといけないが、DelegateProcessならUseCase層における。ドメインロジックだよな!から、あくまで業務手順の一つで詳細はしらん!にできる。ドメインモデルじゃなくてDTOで事足りる場面も多いと思うのでこれは良い戦略何じゃないかなと思っている(過度にやりすぎるとドメイン知識が漏れ出すので注意は必要)

※あくまで僕の実装しているプロジェクトの命名規則どうしようかな〜の話です🙏 認知負荷下げていきたい!

まとめ

これを徹底して、UseCaseに委譲の関心事を押し出すことで、誰もが一度は憧れた(かもしれない) :point_down:のようなドメインモデリングが可能になる。(流石にやりすぎではあるけど、あくまでイメージですw)

case class Payment( id: PaymentId, type: PaymentType )

コレを不可能にしていた要素は大部分、UseCase層に押し出せる。(変に抽象化すると歪さをうむのでUseCase層ではある程度愚直に書かないといけない。ドメイン層のものと混ざらないことが大事。また、業務知識として出てきたものをUseCaseへ押し出してはいけないという前提のもとです)

課金履歴みたいなデータ構造は課金手段によって構造が変わるが、PaymentTypeだけ意識してドメイン層で取り扱うようにし、UseCaseで履歴の構造を組み立てるとかもできるかもしれない。(ここはある程度ドメイン層で表現した良さそうな直感はある。Repositoryによる入力が発生しそうなので。なるべく上に押し出す意識が持てればOK。外部連携用のデータをそのまま表示したいなどの要件があった際は、CQRS+ESで表示用のコンテキストに持っていけば、リードモデルが連携先のデータ構造に似てるとかは考えうる。課金レベルの複雑なドメインで表示の要件が出たときの歪さは自分の中でもきっちりとは整理できていないがCQRS+ESにのっておけば適切に分離できるだろうと考えている。近々決済機能を実装する機会があるのでそこで試す。他のドメインでは上手くいっている)

こうすることで、複数の課金要素(たとえばLinePayUseCaseやそれに必要なDTO)はそのままの形ではドメイン層に現れない。 コレでビジネスはシンプルになるはずだ。

※共通プロセス(JointProcess)、委譲プロセス(DelegateProcess)という名前は、結構解釈が割れそうな気もするのでいい名前思いついたらコメントいただけるとうれしい

※JointProcessは使用する側される側どっちなのかわかりにくい気がする・・・

※DelegateProcessは、Delegateという言葉に色々意味が含まれすぎてる気がする・・・

※追記: DelegateProcessは、xxxSolutionとかのほうが、注入するのも違和感ないし、UseCaseからもどっかに依頼してる感が出るし、業務的にもSolutionのことは手順内でやりとりだけ知っとけばいい感あるかもしれない。とはいえ、Serviceと同じくらいフワっとしてる気もする /(^o^)\

我々はまだEffの表現力の恩恵を、わずかな一部分しか得られていないのかもしれない

頭の中言語化チャレンジ企画

01.png (46.3 kB)

飲みながらだと、「Effいいんだよぉ!!」と脳死説明しかできないので事前に言語化しておくチャレンジをするマン。

Effのざっくりとした解説は社のブログに書いたので気になる方はどうぞ

tech.recruit-mp.co.jp

Eff導入まで

長いことモナドトランスフォーマー+typed-finalでプロダクションコードを書いていて、型パズルどうにかならんかと思うことがよくあり、当時のチームメンバーだったイッペーと「Typed-finalは1効果-1型パラの抽象化しかできないし、モナトラは効果の組み合わせが増えていくとどんどんつらくなる。良い解決策になりそうなのはFreeモナドあたりで、そこでなんとかゴニョゴニョすれば1対多の抽象化が楽にできるんじゃね?」と漠然と話をしていた。

論文を読んだり勉強会の資料を漁ったりし、2年前の1月にEffを使ったアーキテクチャの構想に至り、メンバーとなんやかんや議論しつつ、2018年6月くらいに担当しているサービスの1サービスをすべてEffに書き換えてリリースした。

リリースはしたが、Effの表現力によって得られる恩恵の大きさを測りかねていた。

シンタックスがフラットになって可読性があがるとか、律速問題の解決とか、計算の順番が変わっても結果が変わらないとかは、more extensible effect誕生のモチベーションそのものなので自明だが、 ソフトウェアの表現力の部分で、もっとどデカいメリットがあると思っていたので、どういうところに一番効くかをぽーっと思案していた。

自分は、研究と実践でいえば実践好きで、研究で得られた理論は実践によって昇華されるはずだと強く思っている。(もちろん研究で得られた理論がないと体系だった実践はできないので双方ともに重要ではあるし、現時点で世界がおいついていないから実践できない理論とかもあるが、細かいことはおいておく)

新しい技術にただ飛びつくのではなく、1実践者として理論の旨味を理解し、どう昇華させるか、枯れさせていくかを考え、すでにある枯れた技術とうまく掛け合わせて行くべきだと思っているので、かなり自明なメリットがないとprodの環境に何かを導入はしないマンである。(dev,stgまではバシバシ入れて検証しまくるではある。要はEffは結構慎重に入れたよという話です🙏 論文や資料を読み込まずに導入するのはおすすめしません🙏)

担当しているサービスで強行軍でアーキテクチャを整えたのも、「マイクロサービスまでは表現力獲得のためには当たり前ラインで、その先の運用やその効率化、インフラストラクチャや、その上に乗るアプリケーションを考えるチームとして充分なメンバーがいるし、サービスの領域もそれくらいやる意義と必要がある」と考えてのものだった。

※手を入れる前の既存の実装は力技の実装が多かったので、問題のレベルが高くないと解のレベルも上がらないとも思っていた。その時はサーバーを4人、サーバー兼インフラ1人で回していたが、新しい人も入り始めていたので、より良い制約と解が必要だった。(このときmodular monolithに考えが至っていたら、最初で7つに分けたサービスは4つですんだが、4つはデプロイ独立性とリソースの最適配分も必要だったので、どうしてもマイクロサービスとして分けないといけなかった)

※当たり前ラインとかいうと戦争が起きそうですが、ある一定以上の複雑性に立ち向かう上では標準装備になっていくものだと思っています😋

そんなこんなあって、みんなの助けがあって(まじで一生感謝)苦労してもってこれた当たり前ラインに、Effという表現力爆増の武器を搭載したので、ソフトウェアアーキテクチャ+Eff, マイクロサービス+Effで素晴らしい次のステップの実践ができる状態になった。

ソフトウェアアーキ + Effで考えたこと

Effの使いみちとして最初に浮かんでいたものは「トランザクションモナド」で、DBの操作をフラットに記述できる事によりslickのDBIO型やFujiTaskのような仕組みの旨味だけを享受できると考えた。これらは便利だが、取り扱う型が増えるためにシンタックスが煩雑になり、受ける恩恵と同等かそれ以上のデメリットがあったのをEffは解消できる。(型が増えるとまじで型合わせが辛い・・・)

あと、説明で使ったのは「メール」だった。 メールは一度発火するともう取り返しがつかない。トランザクションの最後の方に記述するようにはしていたが、トランザクションがコケない確証はどこにもない。 Effであれば処理のどの位置に記述しても、トランザクションの確定後にメールを送信するような表現ができる。

次にうかんだのは、認証、認可、課金で、UseCaseやドメインにしれっと顔を出しやすいこれらの分離と柔軟な表現に使えると考えた。

マイクロサービス + Effで考えたこと

マイクロサービス上で実装する上で、まだ愚直に書く選択しかない「補償トランザクション」、「分散トレース用のrequestId引き回し」をEffのeDSLで表現することによってシンプルに扱いやすく記述できると考えた。

特に補償トランザクションはTryを送る -> Eitherを取り出してRightならConfirm,LeftならCancelという処理が書け、Eitherの効果をまたぎ、継続の恩恵も受けることができ(すべての処理が終わってEitherが返った段階で判定する)、_compensationTranTry_compensationTranConfirmOrCancel のような効果に分けることでインターフェースに補償トランザクションのどのフェーズの処理かも明示できるようになるので、愚直に書くよりは認知負荷や処理漏れなどのバグの心配も格段に減らすことができると思っている。

分散トレース用のrequestId引き回しも、普通に書くとUseCaseまでrequestIdが顔を出すので色んな所を汚染してしまうが、内部通信用の効果のインタプリタに渡せばよいだけになるのでとてもクリーン。

担当しているサービスでは、これ以上の用途は思いついていなかったがマイクロサービス上で楽に正確にわかりやすく記述できるのでこれだけでもテンションが上っていた。

SaaS + Effで考えたこと

SaaSは汎用性と専門性のせめぎあいが必要になると思う。

大型案件などで、どうしても局所的に専用の処理を組んだりする必要がでてくることもあるだろう。

そういう、各社の持つ独自仕様や、マルチテナントをせざるを得ない状況に陥ったときを考えると「アレ、これどうすんだ、人海戦術しか残っていないのか・・?」という気持ちになって憂鬱になる。

だが、「ドメインロジックやら各種Repositoryへのstore処理も効果として定義すれば良いんじゃね?」という考えに至ったときに、Effの懐の深さでなんとかなるかもな?と思った。

そうすれば、どうしても対応しなければならない各社の持つ独自仕様のようなものが出てきた時でも、かなりの部分インタプリタで吸収できる。UseCase同じでデータ構造違うみたいなこともスッキリ表現できる。それで吸収できない部分はUseCaseごと分ければ良い。

マルチテナント化に舵をきるときも「仕様の違いでforkします!!」という地獄のような理由で判断を下すリスクを極限まで減らすことができるな?と気づいた時に「いけるかもしれん」と思った。(やったことないのであくまで思っただけ)

👇コード書きながらそれを思いついて、テンションがあがり酒飲んで訳わからんことをつぶやいていた。(自分自体はアーキテクトってよりはSWE寄りだと思っております)

Screen Shot 2020-09-27 at 4.20.54.png (81.3 kB)

最後のドメイン系IOのEff化はどう書くのかまだ決めきれていない(というか、現時点では必要がない)が、SaaSが死ぬときは、本質の機能提供がミクロな解の提供によって阻害され、ドメインが汚染されて表現力が落ちていくのが要因であることも多そうだなと思っていたので、 それに備えられるのは良いし、この柔軟性こそが競合優位性になりうる。

インタプリタを作りまくって差し替えまくってより良い挙動を検証していくことができるかもしれない。

どうしても対応しないといけない巨額案件の微妙な要件も、専用のインタプリタとControllerでproviderId判定するだけですむかもしれない。

まとめ?

現状、EffはドメインやUseCaseの記述が自然言語に近くなるだったり、シンタックスがフラットになる恩恵や、トランザクションなどの独自定義モナドの恩恵受けることができているが、他にも色々な使いみちがあるかもしれぬ。主に効果のカプセル化とも呼べそうな部分に魅力を感じていて、適用可能な範囲めちゃくちゃ広そうだなー

とか考えると、まだまだ表現力の表層しか使えてないかも? と思うのであった。みんなで使いながら探っていきたい。