ここ最近5年くらい、クリーンアーキテクチャという言葉をよく目にするようになりました。特に有名なのが、同心円上に描かれたレイヤー構造です。この図はいわゆるイメージ図ですが、初見ではこれに驚いた方は多いのではないでしょうか? 少なくとも私には驚きでした。
というのも、私のこれまでのソフトウェア開発のイメージでは、むしろ中心の方にハードウェア寄りの処理があり、外側がアプリケーション寄りという風に捉えることが多かったからです。例えばLinuxなどは、まさにLinux Kernelという核(Kernel = 核)が中心にあり、その上にミドルウェアなどが乗り、アプリケーションは一番外側という風に理解していたからです。
しかし、それでもいろいろな記事や解説を読み進めていくうちに、1つのアプリケーションを開発する立場からすると、この図に示されるように、中心にアプリケーションのコアとなるビジネスロジックを配置し、画面処理やデータベースなどの外界とのやり取りを外側に配置した発想というのは、非常に理にかなっていると思う様になりました。
(図は https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html より)
さて、発想の雰囲気はつかめたとして、では実際の実装について試そうと思うと、これはC# で実装されている例のものが多く、私自身、クリーンアーキテクチャはオブジェクト指向プログラミング(OOP)とセットのように扱われているように思っていました。
(一方で少数ながら、Scalaにおいて、関数型プログラミングとしてのクリーンアーキテクチャについて考察された記事 https://qiita.com/KtheS/items/3df2f2a717b34d761550 もあります)
そこで、今回はHaskellライクな純粋関数型言語であるPureScriptで、このクリーンアーキテクチャの実装を試してみよう、と思い立ち、いろいろとまだ不完全なところはあるものの、とりあえず出来たものについて話したいと思います。(PureScriptについては、https://qiita.com/hiruberuto/items/2316b58162cfec150460 に非常に詳しく解説されています。素晴らしい記事です! また、PureScriptと同様にHaskellライクなAltJS言語としてElmがあります。Elmは、汎用プログラミング言語ではなくWebフロントエンドに特化していますが、PureScriptよりも使い勝手は良い様です。https://qiita.com/imtaplet/items/71003c4e2fa322f4298f )
とりあえずごく簡単なサンプルということで、Go言語でAPI Serverを実装された https://qiita.com/hirotakan/items/698c1f5773a3cca6193e の記事を大変参考にさせてもらいました。(とても分かりやすい記事でおすすめです!)APIの呼び出し方は、ほぼそのまま同じです。
なお、今回作成したコードは、https://github.com/knight9999/ps-cleanarchitecture-sample においてあります。
アプリケーションの仕様
簡単なAPI Serverで、ユーザーのデータを扱います。一人のユーザーのデータは「id」(整数)「firstName」(文字列)「lastName」(文字列)の3つだけからなります。
ユーザーの作成、ユーザーの一覧、指定したユーザーの閲覧の3つのAPIが使えます。
フロントエンドはないので、curlコマンドなどで直接呼び出して使います。
なお、データベースとしてsqlite3を使います。db/db.sqlite3
ファイルがすでにあることを想定しています。
CREATE TABLE users
( id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE
, firstName Text
, lastName Text
);
初期データも作成しておいてください。
INSERT INTO users (firstName, lastName) VALUES ('Patricia', 'Smith');
INSERT INTO users (firstName, lastName) VALUES ('Linda', 'Johnson');
INSERT INTO users (firstName, lastName) VALUES ('Mary', 'William');
INSERT INTO users (firstName, lastName) VALUES ('Robert', 'Jones');
INSERT INTO users (firstName, lastName) VALUES ('James', 'Brown');
INSERT INTO users (firstName, lastName) VALUES ('Susan', 'Taylor');
ユーザの作成(作成時は、IDは指定しません)
% curl -i -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"firstName": "Hello", "lastName": "PureScript"}' localhost:3000/users
全ユーザーの表示
% curl -i -H 'Content-Type:application/json' localhost:3000/users
指定したユーザーの閲覧
% curl -i -H 'Content-Type:application/json' localhost:3000/users/3
ディレクトリ構成
元記事と同じ様に、Domain
(一番内側の層), Usecase
(内から2番目の層), Interfaces
(内から3番目の層) , Infrasrtucture
(内から4番目の層、一番外側の層) の 4つのディレクトリを作成し、Interfaces
はさらにサブディレクトリとして Controllers
と Database
を作成します。また、アプリのメイン処理 Main.purs
は、App
の直下におきました。(Main.purs
はInfrastructure
においてもいいのかも)
App
┣ Domain
┣ Infrastructure
┣ Interfaces
┣━┳ Controllers
┃ ┗ Database
┣ Usecase
Main.purs
各種実装
Enterprise Business Rules (一番内側の層、Domain
ディレクトリ)
この層にはユーザーデータを扱うエンティティ Users.purs
のみがあります。このアプリでは特にビジネスロジック(ドメインロジック)はないので、単なるレコードとその型のみです。(なのでドメインモデル貧血症ではありません。バリデーションなどの複雑なドメインロジックが必要になれば、この層に追加して、充実したドメイン層にしていきます)
module Domain.User
( UserType
, User(..)
) where
import Data.Maybe (Maybe)
type UserType =
{ id :: Maybe Int
, firstName :: String
, lastName :: String
}
data User = User UserType
Application Business Rules (内から2番目の層、Usecase
ディレクトリ)
この層には、アプリケーションのユースケースを処理するための ユーザーインタラクタ UserInteractor.purs
と、 ユーザーリポジトリの型定義 UserRepository.purs
を置きました。今回アプリは、エンティティが User
型一つしかないので、ユーザーインタラクタと命名していますが、複数の異なるエンティティ型があり、それらを操作する場合、インタラクタはもっとユースケースに応じた命名になると思います。
module Usecase.UserInteractor
(
UserInteractorType
, mkUserInteractor
) where
import Prelude (Unit)
import Data.Maybe (Maybe)
import Effect.Aff (Aff)
import Domain.User (User)
import Usecase.UserRepository (UserRepositoryType)
type UserInteractorType = {
userById :: Int -> Aff (Maybe User)
, users :: Aff (Array User)
, addUser :: User -> Aff Unit
}
mkUserInteractor :: UserRepositoryType -> UserInteractorType
mkUserInteractor userRepository = {
userById: userRepository.userById
, users: userRepository.users
, addUser: userRepository.addUser
}
ユーザインタラクタには、ユーザーの追加(addUser
)、全ユーザー一覧の取得(users
)、指定したユーザーの閲覧(userById
)の3つのメソッドが用意されています。このアプリでは、これは基本的にuserRepository
の機能を呼び出しているだけですが、もっといろんなユースケースが増えてきた場合、この層に実装していきます。
また、mkUserInteractor
は、ユーザーインタラクタを生成するコンストラクタの役割をしています。
UserRepository.purs
は、ユーザインタラクタが外側の層を直接依存しないで済む様に、型だけを定義しています(OOPで言うインターフェースの役割)
module Usecase.UserRepository
( UserRepositoryType
) where
import Prelude
import Data.Maybe (Maybe)
import Effect.Aff (Aff)
import Domain.User (User)
type UserRepositoryType = {
userById :: Int -> Aff (Maybe User)
, users :: Aff (Array User)
, addUser :: User -> Aff Unit
}
Interface Adapters (内から3番目の層、Interfaces
ディレクトリ)
Controllersディレクトリ
ここでは、この層でUser
データを取り扱うためのCUser.purs
と、UIを担当するためのUserController.purs
を置きます。まず、CUser.purs
は次の様にしました。
module Interfaces.Controllers.CUser
( CUser(..)
, fromInputRecord
) where
import Prelude (class Show, bind, pure, ($), (<>))
import Simple.JSON (class ReadForeign, readImpl, class WriteForeign, writeImpl)
import Data.Int (decimal, toStringAs)
import Data.Maybe (Maybe(..), fromMaybe)
import Record.Builder (build, merge)
import Domain.User (User(..), UserType)
type InputUserType =
{ firstName :: String
, lastName :: String
}
newtype CUser = CUser User
instance readCUser :: ReadForeign CUser where
readImpl text = do
(user :: UserType) <- (readImpl text)
pure $ CUser (User user)
instance writeCUser :: WriteForeign CUser where
writeImpl (CUser (User user)) = writeImpl user
instance showCUser :: Show CUser where
show (CUser (User user)) =
"User { id:" <> (toStringAs decimal (fromMaybe 0 user.id))
<> ", firstName: '" <> user.firstName <> "'"
<> ", lastName: '" <> user.lastName <> "' }"
fromInputRecord :: InputUserType -> CUser
fromInputRecord obj = do
let obj' = build (merge { id: Nothing :: Maybe Int }) obj
CUser (User obj')
CUser
は、User
をラップしているだけですが、テキストデータとの相互変換をするために、ReadForeign
クラスと、WriteForeign
クラスのインスタンスにしてあります。エンティティであるUser
と比べて、CUser
はアプリケーションのUIに必要な入力・出力の機能が追加されていることになります。
また、入力時には、IDがないレコードに対してreadCUser
インスタンスがうまく機能しなかったので、IDのないレコード型からもCUser
を生成するためにfromInputRecord
関数を設定しました。(writeCUser
インスタンスは、IDが Nothing
でも問題なく動作するので、toOutputRecord
関数は必要ありませんでした)
さらに、CUser
にはログ表示などに使えるように、Show
クラスのインスタンスにもしています。(今のところ使っていませんが)
次に、UserController.purs
ですが、
module Interfaces.Controllers.UserController
where
import Prelude (Unit, map, ($), (<$>))
import Effect.Aff (Aff)
import Data.Maybe (Maybe)
import Usecase.UserInteractor (mkUserInteractor)
import Interfaces.Controllers.CUser (CUser(..))
import Interfaces.Database.DUser
import Interfaces.Database.SqlHandler (SqlHandlerType)
import Interfaces.Database.UserRepository (mkUserRepository)
type UserControllerType = {
create :: CUser -> Aff Unit
, index :: Aff (Array CUser)
, show :: Int -> Aff (Maybe CUser)
}
mkUserController :: SqlHandlerType DUser -> UserControllerType
mkUserController sqlHandler = {
create: \(CUser user) -> userInteractor.addUser user
, index: map (\user -> CUser user) <$> userInteractor.users
, show: \id -> map (\user -> CUser user) <$> (userInteractor.userById id)
} where
userInteractor = mkUserInteractor $ mkUserRepository sqlHandler
となっていて、mkUserController
によって、ユーザーコントローラを生成します。この層で行う処理があまりないので、内側の層のユーザーインタラクタに丸投げしていますが、ユースケース層のユーザーインタラクタではエンティティであるUser
型を直接扱っているのに対し、このユーザーコントローラーで扱うのはCUser
になっています。
なお、UI出力はJSON返しているだけなので、プレゼンタはありません。
Databaseディレクトリ
ここでは、データベースへの読み書きを担当します。コントローラでCUser
を扱っていた様に、ここではUser
をラップしたDUser
を定義して、データベースへの読み書きを行います。DUser
も、読み書きが出来る様に強化されています。
module Interfaces.Database.DUser
( DUser(..)
) where
import Prelude (bind, pure, ($))
import Simple.JSON (class ReadForeign, readImpl, class WriteForeign, writeImpl)
import Domain.User (User(..), UserType)
newtype DUser = DUser User
instance readDUser :: ReadForeign DUser where
readImpl text = do
(user :: UserType) <- readImpl text
pure $ DUser (User user)
instance writeDUser :: WriteForeign DUser where
writeImpl (DUser (User user)) = writeImpl user
ユーザーリポジトリの実装もこの層で行っています。
module Interfaces.Database.UserRepository
( mkUserRepository
) where
import Prelude (Unit, bind, pure, unit, ($), (<$>))
import Data.Maybe (Maybe(..))
import Data.Array ((!!))
import Effect.Aff (Aff)
import Domain.User (User(..))
import Usecase.UserRepository (UserRepositoryType)
import Interfaces.Database.DUser (DUser (..))
import Interfaces.Database.SqlHandler (SqlHandlerType)
mkUserRepository :: SqlHandlerType DUser -> UserRepositoryType
mkUserRepository sqlHandler = {
userById: \i -> userById sqlHandler i
, users: users sqlHandler
, addUser: \user -> addUser sqlHandler user
}
userById :: (SqlHandlerType DUser) -> Int -> Aff (Maybe User)
userById sqlHandler id = do
let queryString = "SELECT id, firstName, lastName FROM users WHERE id = $id;"
params = { "$id": id }
results <- sqlHandler.query queryString params
pure $ case results !! 0 of
Just (DUser user) -> Just user
Nothing -> Nothing
users :: (SqlHandlerType DUser) -> Aff (Array User)
users sqlHandler = do
let queryString = "SELECT id, firstName, lastName FROM users;"
users' <- sqlHandler.query queryString { }
pure $ ((\(DUser user) -> user) <$> users')
addUser :: (SqlHandlerType DUser) -> User -> Aff Unit
addUser sqlHandler (User record) = do
let queryString = """
INSERT INTO users
( firstName, lastName )
VALUES
( $firstName, $lastName );
"""
params = { "$firstName": record.firstName
, "$lastName": record.lastName }
_ <- sqlHandler.execute queryString params
pure unit
ユーザーリポジトリは、SQLハンドラ SqlHandler
にSQLを送る必要があるのですが、直接、外側の層のSqlHandler
には接続出来ないので、同じ層にSqlHandler
の型定義(インターフェースに相当)だけをしておき、こちらを参照するようにします。
また、このSqlHandler
は、ユーザーエンティティに特化せず、インスタンス化することでいろいろなエンティティを読み書きできる様に、ReaderT
パターンを使った型クラスとして定義してあります。(当初、Tagless Finalにしようとしたのですが、このアプリではそこまで一般化しなくても十分そうなので止めました。もし時間があれば、Tagless Finalにしてみたいと思います。PureScriptだと、Haskellと違って型シノニムをインスタンスに出来ないのでちょっと面倒ですが。なお、Tagless Finalについては、https://qiita.com/lotz/items/a903d3b2aec0c1d4f3ce で非常に詳しく説明されています(言語はHaskellです)。今回も大変参考にさせてもらいました)
module Interfaces.Database.SqlHandler
(
class SqlHandler
, query
, execute
, SqlHandlerType
, mkSqlHandler
) where
import Type.Proxy (Proxy)
import Effect.Aff (Aff)
import Control.Monad.Reader.Trans (ReaderT, runReaderT)
type SqlHandlerType result = {
query :: forall params. String -> Record params -> Aff (Array result)
, execute :: forall params. String -> Record params ->Aff (Proxy result)
}
mkSqlHandler :: forall ds result. (SqlHandler ds result) => ds -> SqlHandlerType result
mkSqlHandler ds = {
query: \queryString params -> runReaderT (query queryString params) ds
, execute: \queryString params -> runReaderT (execute queryString params) ds
}
class SqlHandler ds result where
query :: forall params. String -> Record params -> (ReaderT ds Aff) (Array result)
execute :: forall params. String -> Record params -> (ReaderT ds Aff) (Proxy result)
Infrastructure (内から4番目の層、Infrastructure
ディレクトリ)
次に、一番外側の層について解説します。この層には、SqlHandler
の実態と、サーバーとして動作するためのRouter
が実装されています。
まず、SqhHandler
の実態は、
module Infrastructure.SqlHandler
( DataStoreType
, DataStore(..)
, module IDS
) where
import Prelude (bind, pure, (<$>))
import Data.Either (Either(..))
import Type.Proxy (Proxy(..))
import Effect.Aff (Aff)
import Simple.JSON (read, class ReadForeign)
import SQLite3 (DBConnection, queryObjectDB) as SQ3
import Interfaces.Database.SqlHandler (class SqlHandler, SqlHandlerType, execute, mkSqlHandler, query) as IDS
import Control.Monad.Reader.Trans (ReaderT, ask, lift)
type DataStoreType =
{
conn :: SQ3.DBConnection
}
newtype DataStore = DataStore DataStoreType
instance sqlHandlerImpl ::
( ReadForeign result
) => IDS.SqlHandler DataStore result
where
query queryString params = (query_ queryString params)
execute executeString params = (execute_ executeString params)
query_ :: forall params result. (ReadForeign result) =>
String -> Record params ->
(ReaderT DataStore Aff (Array result))
query_ queryString params = do
(DataStore ds) <- ask
lift do
results <- read <$> SQ3.queryObjectDB ds.conn queryString params
case results of
Right (results' :: Array result) ->
pure results'
Left e ->
pure []
execute_ :: forall params result.
String -> Record params ->
(ReaderT DataStore Aff (Proxy result))
execute_ queryString params = do
(DataStore ds) <- ask
lift do
_ <- SQ3.queryObjectDB ds.conn queryString params
pure Proxy
特に難しいことはしていません。query
と execute
の二つのメソッドだけがあります。でもよく考えたら、DBとのコネクション関係の処理も、この層に記述すればよかったように思います。(SqlHandlerのインターフェースはquery
とexecute
だけしかなくて良いけど、実態の方は Router
から直接呼ばれるので、ここにDBとのコネクションの接続・切断を記述しておいた方が良いですよね)
また、Router
は、サーバーを起動して、リクエストを解析して、CUser
を作成して、それをコントローラーに流しています。フレームワークとしてBucketchain
を使っていますが、もう少し高機能なフレームワークがあれば、Router
はもっと短く記述出来たのではないかと思います。
module Infrastructure.Router
( init
) where
import Prelude (Unit, bind, discard, join, pure, ($), (&&), (<$>), (<<<), (<>), (==))
import Effect (Effect)
import Control.Monad.Reader (ask)
import Effect.Class (liftEffect)
import Data.Maybe (Maybe(..))
import Data.Either (Either(..))
import Data.Int (fromString)
import Effect.Aff.Class (liftAff)
import Simple.JSON (readJSON, writeJSON)
import Data.String.Regex as Regex
import Data.String.Regex.Flags (noFlags)
import Data.Array.NonEmpty as NonEmptyArray
import Foreign.Object (lookup)
import Foreign (MultipleErrors)
import Bucketchain (createServer, listen)
import Bucketchain.Middleware (Middleware)
import Bucketchain.Http (requestMethod, requestURL, requestBody, setStatusCode, setHeader, requestHeaders)
import Bucketchain.ResponseBody (body)
import Node.HTTP (ListenOptions)
import SQLite3 (closeDB, newDB)
import Infrastructure.SqlHandler as SH
import Interfaces.Controllers.UserController as ICU
import Interfaces.Controllers.CUser as CUser
init :: String -> Effect Unit
init dbFile = do
s <- createServer $ middleware dbFile
listen serverOpts s
middleware :: String -> Middleware
middleware dbFile
= (getUsers dbFile)
<<< (getUser dbFile)
<<< (postUsers dbFile)
<<< welcome <<< error404
getUsers :: String -> Middleware
getUsers dbFile next = do
http <- ask
if requestMethod http == "GET" && requestURL http == "/users"
then do
users <- liftAff do
db <- newDB dbFile
let sqlHandler = SH.mkSqlHandler (SH.DataStore { conn: db })
let userController = ICU.mkUserController sqlHandler
users <- userController.index
closeDB db
pure users
liftEffect do
let resBody = writeJSON users
setStatusCode http 200
setHeader http "Content-Type" "text/json; charset=utf-8"
Just <$> body (resBody <> "\n")
else next
getUser :: String -> Middleware
getUser dbFile next = do
http <- ask
if requestMethod http == "GET"
then case (Regex.regex "^/users/(\\d+)$" noFlags) of
Left error -> next
Right regex ->
case Regex.match regex (requestURL http) of
Just list -> do
case join $ fromString <$> (join $ (NonEmptyArray.index) list 1) of
Just userid -> do
user_ <- liftAff do
db <- newDB dbFile
let sqlHandler = SH.mkSqlHandler (SH.DataStore { conn: db })
let userController = ICU.mkUserController sqlHandler
user <- userController.show userid
closeDB db
pure user
case user_ of
Just user -> liftEffect do
let resBody = writeJSON user
setStatusCode http 200
setHeader http "Content-Type" "text/plain; charset=utf-8"
Just <$> body (resBody <> "\n")
Nothing -> (error404 next)
Nothing -> next
Nothing -> next
else next
postUsers :: String -> Middleware
postUsers dbFile next = do
http <- ask
if requestMethod http == "POST" && requestURL http == "/users"
then do
result <- liftAff do
reqBody <- requestBody http
case (readJSON reqBody :: Either MultipleErrors { firstName :: String, lastName :: String }) of
Left error -> pure $ Left "ERROR"
Right obj -> do
db <- newDB dbFile
let sqlHandler = SH.mkSqlHandler (SH.DataStore { conn: db })
let userController = ICU.mkUserController sqlHandler
userController.create (CUser.fromInputRecord obj)
closeDB db
pure $ Right "OK"
case result of
Left _ -> (error503 next)
Right _ -> liftEffect do
setStatusCode http 200
setHeader http "Content-Type" "text/json; charset=utf-8"
Just <$> body (writeJSON { "result" : "ok" } <> "\n")
else next
welcome :: Middleware
welcome next = do
http <- ask
if requestMethod http == "GET" && requestURL http == "/"
then liftEffect do
setStatusCode http 404
setHeader http "Content-Type" "text/plain; charset=utf-8"
Just <$> body "Welcome to PureScript Clean Architecture Page"
else next
error404 :: Middleware
error404 next = do
http <- ask
let obj = requestHeaders http
case lookup "content-type" obj of
Just contentType -> liftEffect do
setStatusCode http 404
setHeader http "Content-Type" "application/json; charset=utf-8"
Just <$> body (writeJSON { "result" : "Not Found" } <> "\n")
Nothing -> liftEffect do
setStatusCode http 404
setHeader http "Content-Type" "text/plain; charset=utf-8"
Just <$> body ("Sorry!!, that page is not found\n")
error503 :: Middleware
error503 next = do
http <- ask
let obj = requestHeaders http
case lookup "content-type" obj of
Just contentType -> liftEffect do
setStatusCode http 503
setHeader http "Content-Type" "application/json; charset=utf-8"
Just <$> body (writeJSON { "result" : "Internal Error" } <> "\n")
Nothing -> liftEffect do
setStatusCode http 503
setHeader http "Content-Type" "text/plain; charset=utf-8"
Just <$> body ("Sorry!!, Internal Error happens\n")
serverOpts :: ListenOptions
serverOpts =
{ hostname: "localhost"
, port: 3000
, backlog: Nothing
}
Main.purs
最後に、アプリを起動するMain.purs
ですが、これは sqlite3
のファイルがあるかどうかをチェックして、あれば、Router
のinit
関数を呼び出してサーバーを起動しています。
module Main where
import Prelude (Unit, bind, discard, pure, unit, ($))
import Effect (Effect)
import Effect.Console (error)
import Effect.Exception (error, throwException) as EE
import Node.Globals as NG
import Node.Path as NP
import Node.FS.Sync as NFS
import Infrastructure.Router as Router
main :: Effect Unit
main = do
-- check DB
let dbFile = NP.concat [NG.__dirname, "..", "db", "db.sqlite3"]
isExist <- NFS.exists dbFile
if isExist
then
pure unit
else do
error "No db.sqlite3 file"
EE.throwException $ EE.error "No db.sqlite3 file"
Router.init dbFile
制御の流れ、データの流れ、依存関係の方向性
アプリケーションやシステムを実装していると、「制御の流れ」「データの流れ」「依存関係の方向性」など、いろいろなベクトルが出てきて混乱しやすいと思うのですが、このサンプルアプリについて、自分なりのレビューしてみます。
このアプリでは、ユースケースが3つ(ユーザー作成、一覧表示、指定したユーザーの表示)ありますが、ここではユーザー作成の場合について、コントロールの流れを見てみます。
- まずユーザーがJSON文字列をポストすると、それをRouterが受け取ります。
- RouterはJSON文字列から
CUser
を作成し、それをユーザーコントローラに渡します。 - ユーザーコントローラーは、これを
User
に変換して、ユーザーインタラクタに投げます。 - ユーザーインタラクタは、ユーザーリポジトリに
User
を渡し、データベースへの登録を催促します。 - ユーザーリポジトリは、受け取った
User
をDUser
に変換して、さらにこれを必要なSQL文とパラメータにした上で、SqlHandler
に渡します。 -
SqlHandler
は、受け取ったSQL文とパラメータに従い、データベースに書き込みます。
制御の流れはこの様になっていて、外側から内側へ流れていき、ユースケース層・ドメイン層まで行った後、再び内側から外側へと流れていきます。ユーザーデータ(エンティティやそのラッパー)の流れも同じです。(ユーザーデータ以外のデータ、例えば、DBの接続情報などは隠蔽された状態で外側から内部に流れていきますが、内部ではこれを読み出すインターフェースがないので、隠蔽されています。また、DBの接続情報などは内部まで行き渡った後は、外側に戻ることはなくて、外側では外側で、DBの接続情報をクロージャで保持しつづけています)
制御やデータの流れとは異なり、依存関係は、外側の層が内側の層に依存しているだけになっています。(制御の流れと逆になっている部分は、DIにより解決しています。というか、クリーンアキーテクチャに従い、依存関係が外側から内側という方向だけなるように、頑張って実装しているわけです)
終わりに
純粋関数型言語のPureScriptで、クリーンアーキテクチャをしてみたい! というモチベーションから、試行錯誤しながら実装をしてみました。OOP言語でいうところのクラスやオブジェクト、特に継承がないPureScriptですが、クリーンアーキテクチャでは継承はほぼ使わない(インターフェースは使いまくりますが)ので、C#やGoと同じ様に実装出来る印象を受けました。PureScriptの強力な型システムは、クリーンアーキテクチャの実装にはプラスに働いているのではないかと思います。
なお、Go言語の既存の記事 (https://qiita.com/hirotakan/items/698c1f5773a3cca6193e) をかなり参考にさせてもらいましたが、今回のアプリの実装はいろいろと試行錯誤しながら進めましたので、まだまだ改善点もあるかと思います。
クリーンアーキテクチャや、PureScriptのプログラミング自体、私の理解が不十分な点もあるかもしれません。(コード内にケース文が多すぎるのとか、かなり不安です、、、。もっと良いやり方があると思います)もし何か気づいた点などありましたら、コメント下さると助かります。
最後の最後に。QiitaのMarkdownエディタ、PureScriptのシンタックスハイライト対応して欲しいです!