ClojureのアプリをKotlinで書き直してみた (途中経過)

※この記事は学習の途中経過を書いた記事のため、今後更新があるものとしてお読みください

なぜ

  • FPな言語からベースがOOPな言語に書き直すことで、FPとOOPの違いを噛みしめることができるのではないかという仮定
    • 設計の仕方の違いを学ぶことで、今後(ミクロでもマクロでも)設計の選択肢の幅が広がるのではないか
  • Kotlin in Actionを読んでKotlinを体系的に学んだので、その知識を元に設計・実装することでKotlinの実践力を身につけたかった
  • Clojureのアプリは業務でたまに活用する機会があるものだが私しか使っていなかったので、Kotlinであれば他の人も環境構築・機能追加がしやすいのではないかという仮定

行なっていること

  • グループの組み合わせを作成するClojureアプリgrouperをKotlinアプリkt-grouperで書き直している (現在進行形)
  • 同じ機能を持ち、同じドメインを表現するものとした (機能追加は書き直しが終わってからの予定)
  • Clojureアプリと同様、Clean Architecture, DDDを意識して設計した
  • まずはClojureのnamespaceをKotlinのパッケージorクラス、Clojureの関数をKotlinのトップレベル関数またはクラスメソッドにしてみた
    • 違和感を感じたものからOOP or Kotlinならではの表現に変更していった

アプリの簡単な説明

  • 複数のメンバーをいいかんじにグルーピングするアプリ
  • 具体的なインプットやアウトプットはgrouperのREADMEを参照
  • インプットの内訳は以下
    • Requirement: 必ず従うべき要求
      • 対象のメンバー
      • 作成するグループの数
    • Request: できれば実現したい要望
      • 各メンバーが誰と一緒にグループを組みたいか
      • 各メンバーが誰と一緒にグループを組みたくないか
      • 過去の組み合わせの結果 (過去組み合わせたメンバー同士はグルーピングを避ける形にする)
  • アウトプットはグループ分けされたメンバーと各グループがどれくらいいい感じなのかを示すスコア

FP with Clojure -> OOP with Kotlinにしてみて気づいたこと

FPでドメインサービスだと思っていたものがOOPではドメインサービスではなかった

  • Clojureアプリではドメインサービスとして表現している処理があった
    • Clojureアプリではドメインロジックを以下の抽象的な処理に分けていた
      • grouper: インプットをもとにグループの組み合わせを生成する
      • evaluator: グループがRequestをどれくらい反映しているかをスコアとして付与する
      • picker: 複数のグループの集合から何かしらの基準で一番良いとされるものを選択する
    • これらの関数のインプットは引数として渡したり、(何度も呼ばれる関数に関しては)クロージャすることで渡していたのだが、インプットもアウトプットも基本は値オブジェクトの役割を持つmapを使用していた
    • つまり私の頭の中では上記の3つの抽象的な処理は値オブジェクトを消費・生産するが自身はデータを持たない = ドメインサービスだと認識していた
  • さて、以上と同じロジックをKotlinで実装しようとすると、何が起こったか
    • 上記の3つの処理の引数や返り値に使うものとして値オブジェクトのクラスが大量にできる
    • たとえばInputといったインプット情報を全て持つ値オブジェクトができ、それをGrouperクラスの引数にするとする
    • GrouperクラスはInputクラスしか使わないので、データと振る舞いが分かれている理由はなんだっけ?むしろGrouperクラスいるんだっけ?となる
  • OOP→FPの移行であれば当たり前に気づくものではあるが、(ドメインロジックがすべてドメインサービスになる)FP→(ドメインサービス・値オブジェクト・エンティティという選択肢がある)OOPに移行するとFPで通用したドメインモデルを再考する必要がある
    • →同じドメインを表現するものでも、FPとOOPではドメインモデルの実現方法は大きく異なる
    • よって関数が主役だったFPからクラスが主役のOOPドメインモデルの組み立て方を合わせていく必要がある

振る舞いの持たせ方を注意深く考える必要がある

  • Kotlinだと振る舞いを与える場合に、クラスのメソッド・Companion Objectのメソッド・拡張関数・トップレベル関数と選択肢が多いので、注意深く表現方法を考える必要がある
  • 正解が考えられるほどまだ実践が足りないのだが、自分なりに良いと思っている分け方は以下の通り
    • 値オブジェクト・エンティティの振る舞い→クラスのメソッド
    • ドメインサービスの振る舞い→Companion Objectのメソッド > トップレベル関数 > データを持たないクラスのメソッド
      • トップレベル関数は(クラス名がない分)比較的ドキュメント性が低いor良い関数名がつけにくいという意味でドメインを表現するという意味で難易度が高い(トップレベル関数を多用するならパッケージの命名から工夫が必要だと考える)。ドメインレイヤーの外で使われる関数でありコンテキストが限られているのであれば比較的使いやすい。
      • データを持たないクラスのメソッドは、ドメインを記述するという文脈で正しくないという判断。また、ユースケース内でそのクラスを生成することになるので、単体テストでモックするのが難しくなる
      • 上記二つほどデメリットがないという意味でCompanion Objectのメソッドを一番良しとしているが、今回使っているモックライブラリでいい感じにモックできないのでモヤモヤしている
    • ドメインレイヤーの外で値オブジェクトなどに関連づけたい場合→拡張関数