11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PureScriptでもクリーンアーキテクチャしようよ!!!

Last updated at Posted at 2020-08-30

ここ最近5年くらい、クリーンアーキテクチャという言葉をよく目にするようになりました。特に有名なのが、同心円上に描かれたレイヤー構造です。この図はいわゆるイメージ図ですが、初見ではこれに驚いた方は多いのではないでしょうか? 少なくとも私には驚きでした。
というのも、私のこれまでのソフトウェア開発のイメージでは、むしろ中心の方にハードウェア寄りの処理があり、外側がアプリケーション寄りという風に捉えることが多かったからです。例えばLinuxなどは、まさにLinux Kernelという核(Kernel = 核)が中心にあり、その上にミドルウェアなどが乗り、アプリケーションは一番外側という風に理解していたからです。

しかし、それでもいろいろな記事や解説を読み進めていくうちに、1つのアプリケーションを開発する立場からすると、この図に示されるように、中心にアプリケーションのコアとなるビジネスロジックを配置し、画面処理やデータベースなどの外界とのやり取りを外側に配置した発想というのは、非常に理にかなっていると思う様になりました。

CleanArchitecture.jpg
(図は 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はさらにサブディレクトリとして ControllersDatabase を作成します。また、アプリのメイン処理 Main.pursは、Appの直下におきました。(Main.pursInfrastructureにおいてもいいのかも)

App
┣ Domain
┣ Infrastructure
┣ Interfaces
┣━┳ Controllers
┃ ┗ Database
┣ Usecase
Main.purs

各種実装

Enterprise Business Rules (一番内側の層、Domainディレクトリ)

この層にはユーザーデータを扱うエンティティ Users.purs のみがあります。このアプリでは特にビジネスロジック(ドメインロジック)はないので、単なるレコードとその型のみです。(なのでドメインモデル貧血症ではありません。バリデーションなどの複雑なドメインロジックが必要になれば、この層に追加して、充実したドメイン層にしていきます)

Domain/User.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型一つしかないので、ユーザーインタラクタと命名していますが、複数の異なるエンティティ型があり、それらを操作する場合、インタラクタはもっとユースケースに応じた命名になると思います。

Usecase/UserInteractor.purs
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で言うインターフェースの役割)

Usecase/UserRepository.purs
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は次の様にしました。

Interfaces/Controllers/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ですが、

Interfaces.Controllers.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も、読み書きが出来る様に強化されています。

Interfaces/Database/DUser.purs
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

ユーザーリポジトリの実装もこの層で行っています。

Interfaces/Database/UserRepository.purs
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です)。今回も大変参考にさせてもらいました)

Interfaces/Database/SqlHadnler.purs
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 の実態は、

Infrastructure/SqlHandler.purs
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

特に難しいことはしていません。queryexecute の二つのメソッドだけがあります。でもよく考えたら、DBとのコネクション関係の処理も、この層に記述すればよかったように思います。(SqlHandlerのインターフェースはqueryexecuteだけしかなくて良いけど、実態の方は Routerから直接呼ばれるので、ここにDBとのコネクションの接続・切断を記述しておいた方が良いですよね)

また、Routerは、サーバーを起動して、リクエストを解析して、CUserを作成して、それをコントローラーに流しています。フレームワークとしてBucketchainを使っていますが、もう少し高機能なフレームワークがあれば、Routerはもっと短く記述出来たのではないかと思います。

Infrastructure.Router.purs
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 のファイルがあるかどうかをチェックして、あれば、Routerinit関数を呼び出してサーバーを起動しています。

Main.purs
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つ(ユーザー作成、一覧表示、指定したユーザーの表示)ありますが、ここではユーザー作成の場合について、コントロールの流れを見てみます。

  1. まずユーザーがJSON文字列をポストすると、それをRouterが受け取ります。
  2. RouterはJSON文字列からCUserを作成し、それをユーザーコントローラに渡します。
  3. ユーザーコントローラーは、これをUserに変換して、ユーザーインタラクタに投げます。
  4. ユーザーインタラクタは、ユーザーリポジトリにUserを渡し、データベースへの登録を催促します。
  5. ユーザーリポジトリは、受け取ったUserDUserに変換して、さらにこれを必要なSQL文とパラメータにした上で、SqlHandlerに渡します。
  6. SqlHandlerは、受け取ったSQL文とパラメータに従い、データベースに書き込みます。

制御の流れはこの様になっていて、外側から内側へ流れていき、ユースケース層・ドメイン層まで行った後、再び内側から外側へと流れていきます。ユーザーデータ(エンティティやそのラッパー)の流れも同じです。(ユーザーデータ以外のデータ、例えば、DBの接続情報などは隠蔽された状態で外側から内部に流れていきますが、内部ではこれを読み出すインターフェースがないので、隠蔽されています。また、DBの接続情報などは内部まで行き渡った後は、外側に戻ることはなくて、外側では外側で、DBの接続情報をクロージャで保持しつづけています)

制御やデータの流れとは異なり、依存関係は、外側の層が内側の層に依存しているだけになっています。(制御の流れと逆になっている部分は、DIにより解決しています。というか、クリーンアキーテクチャに従い、依存関係が外側から内側という方向だけなるように、頑張って実装しているわけです)

終わりに

純粋関数型言語のPureScriptで、クリーンアーキテクチャをしてみたい! というモチベーションから、試行錯誤しながら実装をしてみました。OOP言語でいうところのクラスやオブジェクト、特に継承がないPureScriptですが、クリーンアーキテクチャでは継承はほぼ使わない(インターフェースは使いまくりますが)ので、C#やGoと同じ様に実装出来る印象を受けました。PureScriptの強力な型システムは、クリーンアーキテクチャの実装にはプラスに働いているのではないかと思います。

なお、Go言語の既存の記事 (https://qiita.com/hirotakan/items/698c1f5773a3cca6193e) をかなり参考にさせてもらいましたが、今回のアプリの実装はいろいろと試行錯誤しながら進めましたので、まだまだ改善点もあるかと思います。

クリーンアーキテクチャや、PureScriptのプログラミング自体、私の理解が不十分な点もあるかもしれません。(コード内にケース文が多すぎるのとか、かなり不安です、、、。もっと良いやり方があると思います)もし何か気づいた点などありましたら、コメント下さると助かります。

最後の最後に。QiitaのMarkdownエディタ、PureScriptのシンタックスハイライト対応して欲しいです!

11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?