このところOCamlでアプリケーションをほそぼそと作っているのですが、その過程でClean Architectureっぽいものを採用してみました。

作っているアプリケーション自体は、完全に趣味の領域のものなのでまだ公開していません。ただ、OCamlであってもなんであっても、ある程度の規模になったらなんらかの方法論は必要かな、と思い始めました。

ある程度Clean Architectureっぽいことも出来るようになってきたので、自分の知識を整理する上でも書いてみます。

Clean Architectureとは?

ググってもらうのが一番早いのですが、それは流石に不親切すぎるので・・・。

Clean Architectureは、 http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html が原典とされ、 Robert C. Martin によって提唱されたアーキテクチャです。 Onion Architecture/Hexagonal Architectureなど、過去のアーキテクチャも参考にしながら作成されたものになり、それらの特徴も一部受け継いでいます。

Clean Architectureは、以下のような制約を導入します。

これがClean Architectureにおける唯一の制約です。層としては、基本形として以下を提唱しています。

ただ、これ以外にも *層を* 追加することは可能です。唯一の制約である、層の間にある依存方向を守る限り、層の増減は自由です。

OCamlでの課題(自分的に)

OCamlである程度の規模のアプリケーションを作成する場合、恐らくある程度の単位でパッケージを作成する場合が多いと思います。最近は dune を利用すると思いますが、使わない場合であったり、一パッケージに全ファイルを置くのは色々問題があります。

特に、頻繁に使うmodule名が長いと色々だれますし、毎回aliasを書くのもしんどいです。以前よりもmoduleを明示しなければならないケースは少なくなりましたが、一級moduleを利用しているとやっぱりきついです。

パッケージを分ける上での基準も厄介です。大抵は機能とか役割別だと思いますが、結構分けづらい感じになっていったりで・・・。

そこで、パッケージを分ける基準として Clean Architecture を使ってみました。

OCaml on Clean Architecture

とはいっても、基本的には各層ごとになるようにパッケージを分割するだけです。そうすると、dependenciesの方向制御も簡単になります。私は、 domain, usecase, gateway, binary と分けています。 こうすると、OCamlの制約とduneの機能で、以下のような利点を自動的に得られます。

これは現状のサーバー側のアーキテクチャです。クライアント側は同じような別のアーキテクチャを採用しています。

現状、presenter/adapter/gatewayはサブパッケージとして作成したものを統合している形です。さて、こういうふうに書くのはいいんですが、Clean Architectureは非常に抽象層が多く、Javaとかで実装する場合でもかなり紛糾する部分です。それをOCamlでどう実装していきましょう?

抽象化のやり方(記述時点版)

この記事を書いているタイミングでの抽象化のやり方を簡単に書きます。

(* Use Caseを例に取ります *)

(* 基底になるUsecaseのsignatureです。Lwtは全体を通して利用しているので、利用している事自体はあまり気にしないでください *)
module type Usecase = sig
  type input

  type output

  type error

  val execute : input -> (output, error) result Lwt.t
end

(* なにかするUse Case。実際には動詞を使うと思います *)
module Some_use_case = struct
  (* use caseのinput/output/errorの型をまとめて宣言します *)
  module Type = struct
    type input = unit

    type output = string

    type error = unit
  end

  (* このUse Caseのsignatureです。Use caseを利用し、Typeで指定した型を共有します *)
  module type S =
    Usecase
    with type input = Type.input
     and type output = Type.output
     and type error = Type.error

  (* Use Caseの実装です。Sをそのまま利用して、依存するmoduleをFunctorの引数として受け取ります *)
  module Make (C : Repository) : S = struct
    include Type

    let execute () =
      let%lwt condition = C.resolve () in
      let%lwt keymap = R.resolve () in
      let keymap = Key_map.subset keymap ~condition in
      Lwt.return_ok keymap
  end
end

(* 利用するときはこんな感じになる *)

let () =
  let module U = Some_use_case.Make (Repo_impl) in
  U.execute () |> ignore

こんな感じに書くと、ユニットテスト時には適当なdummy moduleを渡せばよく、実装自体は気にしない、という形に出来ます。 Type として独立したmoduleにしているのは、単にsignatureとFunctorで二回同じのを書きたくなかったからなので・・・。

また、各use case自体は同じインターフェースを強制して、型だけを切り替えればよい、という形にしています。結構なんとかなるし、型だけ見えればいいのであればTypeだけ利用する、ということも出来ます。 Domain層のrepositoryや、gatewayなどで依存を導入することも難しくはないです。

ただ、いろいろ欠点もあって・・・。

改善したい点

冗長

Clean Architecture自体がわりかしファイル数が増えたりしていろいろ冗長なんですが、各UseCase毎に上のような書き方は面倒くさいです。ただ、UseCase自体を差し替えることを可能とするためには、このようにしないとならないので・・・。

Interactor/Input/Outputがうまく設計できていない

原典では、UseCaseの Interactor というものがあり、request/responseを切り離すことを可能としています。これはデータフローの向きを強制する効果も有ります。上記のような実装だと、request/response/errorを Type で宣言しているので、そういったことが出来ない状態です。

ただ、input/outputを分ける事自体が結構面倒、かつinputなどの型をレコード型にしてやったりすればいいだけなので、ここはあまり困っていない感じがあります。

依存性の注入がひたすら面倒

OCamlには私が知る限り、JavaとかであるようなDependency Injectionを行うようなライブラリは存在しません。なので、上のような形で作ると、基本的に依存するmoduleをその場で組み立てていく必要があります。

実際に書いてみないと中々実感できませんが、これは 非常に 面倒です。ぶっちゃけやりたくない。また、内側の層の依存は外側の層から渡す必要があるため、Functorの引数がかなり多くなっていく傾向があります。

(* こんな感じ *)
let () =
  let module A = A_impl in
  let module B = B_impl.Make(A) in
  let module C = C_impl.Make(D_impl)(B) in
  ...

SpringとかのDIライブラリがあれば、この辺をうまくやってくれるケースが多いので、そこまで関係ないケースが多いですが・・・。やるとしたら、組みたてたmoduleを返すような関数群を定義したsignatureを作り、その実装で各々のmoduleを組み立てる、という感じでしょうか。 ただ、実装でまだ分離がうまくやれていない部分があるのも事実なので、そこらへんがうまく行き始めると、もう少しマシになるかもしれません。

classベースの方が楽かも?

Functorと一級moduleを組み合わせて色々やっていますが、objectベースでやったほうが楽なんでは?と思ってもいます。ただ、OCamlのobjectをゴリゴリに利用したようなアプリケーションは聞いたことがないので、なんとも言い難いですが・・・。

OCamlでもClean Architecture/DDDは可能

関数型言語であろうと何であろうと、Clean Architecture/DDDはあくまで考え方や構成法なので、適用できないということはありません。ただ、大抵はAndroid/Swift/Java/C#といったクラスベースの言語で書かれたものが大半であるため、OCamlに適用していくのは結構骨が折れます。

しかし、優れた方法論は、範囲が一緒なのであれば、実装が変わろうとも関係ないはずです。実際、Clean Architectureにしたことで、OCamlでもユニットテストがかなり書きやすくなりました。 物凄い型の構成を考えたりして、型を駆使すると、色々とテストしなくていい場面というのが増えるかもしれませんが、結局テストしないとわからないものはあります。テスト容易な実装にしていきやすいClean Architectureは、OCamlでも有用だと思います。

もっとOCamlに習熟したら、こういう手段に訴えなくても、より楽・堅牢な実装をしていけるかもしれないので、より精進していきたいです。