Nimでアプリケーション開発をするための設計のベストプラクティス

Nimでは低レイヤーのカーネルの開発からアプリケーションの開発まで幅広くできますが、特にアプリケーションの開発では保守性と変更容易性を高くするための設計が重要です。
言語の仕様によって採用できるデザインパターン、できないデザインパターンがありますが、ここではNimでアプリケーション開発をするために取り入れるべきデザインパターンと、その実装方法について説明します。

今回作るものは

- WebのAPIサーバーとCLIから呼び出される
- 機能はユーザーデータの保存と全件呼び出し
- ユーザーデータは`uid`、`苗字`、`名前`、`苗字（カナ）`、`名前（カナ）`、`メールアドレス`、`生年月日`、`年齢`を持つ
- データベースは`Sqlite`と`JSONファイル`の2通りがある

を題材にして説明していきます。

## オニオンアーキテクチャ
![20171011062558.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295826/1855e7ff-8935-df2a-76e0-0315b0fc0088.png)


オニオンアーキテクチャは、アプリケーションの仕様をレイヤー（層）に分けて責務を切り分け、責務毎のクラスで表現するオブジェクト指向のデザインパターンです。

オニオンアーキテクチャは以下のように層を切り分けます

- プレゼンテーション層
- アプリケーション層
- ドメイン層
- インフラ層

プレゼンテーション層はwebとのリクエスト・レスポンスをやり取りしたり、CLIから呼ばれるエントリーポイントになるファイルです。
アプリケーション層はドメイン層のクラスや関数を呼び出して、ユーザーのやりたいことを実現します。
ドメイン層はアプリケーションの仕様やルールが定義されています。
インフラ層はデータベースとの接続や、外部APIとの接続、データのやり取りを行います。

更にドメイン層は以下のように分割されます。

- 値オブジェクト（value object）
- エンティティ（entity）
- ドメインサービス

値オブジェクトは単一の値についての値とふるまいが定義されています。
エンティティはフィールドとして複数の値オブジェクトを持ち、値とふるまいが定義されています。
ドメインサービスは複数のエンティティを比較したり、エンティティが持つ値を使った計算や処理が定義されています。

Nimにはクラスはありませんが、構造体（type）を使うことで実装できます。

## Nimでのオブジェクト指向
アプリケーションを作るデザインパターンはオブジェクト指向の仕組みを使って実装されます。例としてユーザーオブジェクトを実装してみましょう。

それぞれの値はその値が正しく存在するためのルールを持っています。
`苗字`、`名前`であれば1文字以上の文字列である必要がありますし、`苗字（カナ）`、`名前（カナ）`はカタカナで、`メールアドレス`はEmailのフォーマットでなければいけないし、`生年月日`は`yyyy-MM-dd`のフォーマットです。
こうした値のチェックをコンストラクタで行い、不正な値が決して存在しないようにするコンストラクタを**完全コンストラクタ**と呼びます。
ドメインモデルである値オブジェクトとエンティティは完全コンストラクタで作る必要があります。  
またコンストラクタ以外で値を直接渡されないように、フィールドはプライベートにして、取り出す時は`getter`の関数を経由するようにしましょう。

### 値オブジェクト
`domain/user/user_value_objects.nim`

```nim
import
  std/nre,
  std/options,
  std/times,
  std/oids,
  std/sha1,
  ../../errors

type Uid* = ref object
  value:string

proc new*(_:type Uid):Uid =
  return Uid(
    value: genOid().`$`.secureHash().`$`
  )

proc `$`*(self:Uid):string =
  return self.value


type Name* = ref object
  value:string

proc new*(_:type Name, value:string):Name =
  if value.len == 0:
    raise newException(DomainError, "名前が空です")
  
  return Name(value:value)

proc `$`*(self:Name):string =
  return self.value


type KanaName* = ref object
  value:string

proc new*(_:type KanaName, value:string):KanaName =
  if value.len == 0:
    raise newException(DomainError, "カナ名前が空です")
  if not value.match(re"(*UTF8)^[ァ-ン]+").isSome:
    raise newException(DomainError, "カナ名前がカタカナでありません")
  
  return KanaName(value:value)

proc `$`*(self:KanaName):string =
  return self.value


type Email* = ref object
  value:string

proc new*(_:type Email, value:string):Email =
  if value.len == 0:
    raise newException(DomainError, "メールアドレスが空です")
  if not value.match(re".+@.+").isSome:
    raise newException(DomainError, "メールアドレスが正しくありません")

  return Email(value:value)

proc `$`*(self:Email):string =
  return self.value


type BirthDate* = ref object
  value: DateTime

proc new*(_:type BirthDate, value:string):BirthDate =
  if not value.match(re"^[0-9]{4}\-[0-9]{2}\-[0-9]{2}$").isSome:
    raise newException(DomainError, "生年月日は1990-01-10の形式です")

  return BirthDate(value:value.parse("yyyy-MM-dd"))

proc `$`*(self:BirthDate):string =
  return self.value.format("yyyy-MM-dd")

proc get*(self:BirthDate):DateTime =
  return self.value


type YearsOld* = ref object
  value:int

proc new*(_:type YearsOld, value:int):YearsOld =
  if value < 0:
    raise newException(DomainError, "年齢は0以上です")
  return YearsOld(
    value: value
  )

proc get*(self:YearsOld):int =
  return self.value
```

```nim
import
  std/nre,
  std/options,
  std/times,
  std/oids,
  std/sha1,
  ../../errors
```
import文は

- 標準ライブラリは`std/`を付けて呼び出す
- 上から順番に標準ライブラリ、nimbleでインストールした3rdパーティーライブラリ、同一アプリケーション内の他のファイルになるようにする
- ライブラリの追加削除で行単位の差分が発生しないように横に書かず縦に並べる

にしましょう。

```nim
type Email* = ref object
  value:string

proc new*(_:type Email, value:string):Email =
  if value.len == 0:
    raise newException(DomainError, "メールアドレスが空です")
  if not value.match(re".+@.+").isSome:
    raise newException(DomainError, "メールアドレスが正しくありません")

  return Email(value:value)

proc `$`*(self:Email):string =
  return self.value
```

オブジェクトの宣言には`ref object`を使います。  
Nimでは関数の引数に入れられた変数の容量に応じてコンパイラが自動で値渡し/参照渡しを調節しますが、これは挙動の予測が付かずバグの原因になりえます。`ref object`でオブジェクトを宣言していれば必ず参照渡しになるので、アプリケーション開発ではこちらに統一しましょう。  
`object`はCの構造体を呼ぶなど低レイヤーでの開発に必要になります。

[Status社のスタイルガイド](https://status-im.github.io/nim-style-guide/language.objconstr.html)では`init`と`new`の関数をコンストラクタとして使うように書いてあります。   
型`Hoge`のコンストラクタはNimでは一般的に`newHoge()`関数を使いますが、第一引数を`type Hoge`にすることで、`Hoge.new(arg)`の形で呼び出すことができます。  
Githubでは現在でも議論が続いています。  
[[RFC] Another proposal for a standardized object construction/initialization.](https://github.com/nim-lang/RFCs/issues/48)

文字列を返すgetterの関数は、他のライブラリに合わせて「$」にしましょう。

### エンティティ
では値オブジェクトをフィールドに持つ、`Userオブジェクト`を実装していきます。  
エンティティとはテーブルの行データのようなものです。しかしRDBのテーブルと完全に一致するものではありません。例えば今回の`Userオブジェクト`であれば、`年齢`というフィールドがありますが、これは`生年月日`フィールドから計算されて作られるフィールドです。このような値はコンストラクタで生成されるようにします。  
エンティティも値オブジェクトと同じように、**完全コンストラクタ**で値の整合性を保証し、フィールドはプライベートにし、不正な値が存在しないようにします。

`domain/user/user_entity.nim`

```nim
import
  std/times,
  user_value_objects

type User* = ref object
  uid:Uid
  lastName:Name
  firstName:Name
  kanaLastName:KanaName
  kanaFirstName:KanaName
  email:Email
  birthDate:BirthDate
  yearsOld:YearsOld

proc new*(_:type User,
  uid:Uid,
  lastName: Name,
  firstName: Name,
  kanaLastName: KanaName,
  kanaFirstName: KanaName,
  email: Email,
  birthDate: BirthDate
):User =
  return User(
    uid: uid,
    lastName: lastName,
    firstName: firstName,
    kanaLastName: kanaLastName,
    kanaFirstName: kanaFirstName,
    email: email,
    birthDate: birthDate,
    yearsOld: YearsOld.new(
      now().year - birthDate.get.year
    )
  )

proc uid*(self:User):Uid =
  return self.uid

proc lastName*(self:User):Name =
  return self.lastName

proc firstName*(self:User):Name =
  return self.firstName

proc kanaLastName*(self:User):KanaName =
  return self.kanaLastName

proc kanaFirstName*(self:User):KanaName =
  return self.kanaFirstName

proc email*(self:User):Email =
  return self.email

proc birthDate*(self:User):BirthDate =
  return self.birthDate

proc yearsOld*(self:User):YearsOld =
  return self.yearsOld
```

## ポリモーフィズム
ここまで値オブジェクトとエンティティの実装を通してドメインモデルの作り方を見てきました。ドメインモデルとはデータとその振る舞いを集約し、ビジネスルールが外部から切り離して存在するようにしたものです。  
ここからはデータの永続化について見ていきましょう。  
データの永続化は`リポジトリ`パターンを使います。`リポジトリ`は**エンティティを永続化するもの**です。  
データの保存先が`RDB`なのか`JSONファイル`なのか`CSV`なのか`外部API`なのかということはドメインの関心事ではありません。ドメインは知っていてはいけないことです。  
今回は`User`オブジェクトの保存とデータベースからの取り出しという2つの仕様が存在すると考えてみましょう。保存先が`RDB`なのか`JSONファイル`なのかで保存と取り出しの具体的な実装は大きく変わりますが、存在する関数が呼び出される時の見え方は同じです。すなわち`User`オブジェクトを引数に持つ`save`関数によって保存し、`getAll`関数によってデータを取り出すということです。  
`インターフェース`を定義し、リポジトリの具象クラスがインターフェースに沿った仕様になることを保証しましょう。

`domain/user/user_repository_interface.nim`

```nim
import
  std/asyncdispatch,
  std/json,
  user_entity

type IUserRepository* = tuple
  getAll:proc():Future[JsonNode]
  save:proc(user:User):Future[void]
```

`libs/db.nim`

```nim
import
  std/os,
  std/db_sqlite

let conn* = open(getCurrentDir() / "data.sqlite", "", "", "")
```

`infra/user/rdb_user_repository.nim`

```nim
import
  std/json,
  std/db_sqlite,
  std/asyncdispatch,
  interface_implements,
  ../../libs/db,
  ../../domain/user/user_value_objects,
  ../../domain/user/user_entity,
  ../../domain/user/user_repository_interface

type RdbUserRepository* = ref object
  db:DbConn

proc new*(_:type RdbUserRepository):RdbUserRepository =
  return RdbUserRepository(
    db: conn
  )

implements RdbUserRepository, IUserRepository:
  proc getAll(self:RdbUserRepository):Future[JsonNode] {.async.} =
    let query = sql"select * from users"
    let rows = self.db.getAllRows(query)
    let res = newJArray()
    for row in rows:
      res.add(%*{
        "id": row[0],
        "lastName": row[1],
        "firstName": row[2],
        "kanaLastName": row[3],
        "kanaFirstName": row[4],
        "email": row[5],
        "birthDate": row[6],
      })
    return res

  proc save(self:RdbUserRepository, user:User):Future[void] {.async.} =
    let query = sql"""insert into users (
      id, last_name, first_name,
      kana_last_name, kana_first_name,
      email, birth_date
    ) values (?, ?, ?, ?, ?, ?, ?)"""
    self.db.exec(query, [
      $user.uid, $user.lastName, $user.firstName,
      $user.kanaLastName, $user.kanaFirstName,
      $user.email, $user.birthDate
    ])
```

`infra/user/json_user_repository.nim`

```nim
import
  std/os,
  std/json,
  std/asyncdispatch,
  interface_implements,
  ../../domain/user/user_value_objects,
  ../../domain/user/user_entity,
  ../../domain/user/user_repository_interface

type JsonUserRepository* = ref object
  path:string

proc new*(_:type JsonUserRepository):JsonUserRepository =
  return JsonUserRepository(
    path: getCurrentDir() / "data.json"
  )

implements JsonUserRepository, IUserRepository:
  proc getAll(self:JsonUserRepository):Future[JsonNode] {.async.} =
    var f = open(self.path, fmRead)
    defer: f.close()
    let stream = f.readAll()
    if stream.len == 0:
      return newJArray()
    let jsonContent = stream.parseJson()
    if not jsonContent.hasKey("users"):
      return newJArray()
    return jsonContent["users"]

  proc save(self:JsonUserRepository, user:User):Future[void] {.async.} =
    var f = open(self.path, fmRead)
    defer: f.close()
    let stream = f.readAll()
    var jsonContent =
      if stream.len == 0:
        let j = newJObject()
        j["users"] = newJArray()
        j
      else:
        stream.parseJson()
    if not jsonContent.hasKey("users"):
      jsonContent["users"] = newJArray()  
    jsonContent["users"].add(%*{
      "id": $user.uid,
      "lastName": $user.lastName,
      "firstName": $user.firstName,
      "kanaLastName": $user.kanaLastName,
      "kanaFirstName": $user.kanaFirstName,
      "email": $user.email,
      "birthDate": $user.birthDate,
    })
    self.path.writeFile($jsonContent)
```

`di_container.nim`

```nim
import
  domain/user/user_repository_interface,
  infra/user/rdb_user_repository,
  infra/user/json_user_repository

type DiContainer* = tuple
  userRepository: IUserRepository

proc new*(_:type DiContainer):DiContainer =
  return (
    userRepository: RdbUserRepository.new().toInterface(),
    # userRepository: JsonUserRepository.new().toInterface(),
  )

let di* = DiContainer.new()
```

ここでは私が作ったライブラリの`interface-implements`を使っています。このライブラリの`implementsマクロ`を使うことで、具象クラスがインターフェースに定義されているメソッドの実装漏れや引数や返り値の不一致があるとコンパイルエラーが起きるようになります。

[nim-interface-implements](https://github.com/itsumura-h/nim-interface-implements)

[NimでインターフェースとDIコンテナを使う](https://qiita.com/dumblepy/items/ef51eb2647df75f36c7b)


## アプリケーション層からドメイン層を呼び出す
アプリケーション層とはドメインモデルを呼び出し、ユーザーのやりたいことを実現する層です。`User`オブジェクトを保存するというユースケースを実装していきましょう。

`application/crate_new_user_usecase.nim`

```nim
import
  std/asyncdispatch,
  ../domain/user/user_value_objects,
  ../domain/user/user_entity,
  ../domain/user/user_repository_interface,
  ../di_container

type CreateNewUserUsecase* = ref object
  repository:IUserRepository

proc new*(_:type CreateNewUserUsecase):CreateNewUserUsecase =
  return CreateNewUserUsecase(
    repository: di.userRepository
  )

proc invoke*(
  self:CreateNewUserUsecase,
  lastName:string,
  firstName:string,
  kanaLastName:string,
  kanaFirstName:string,
  email:string,
  birthDate:string
){.async.} =
  let uid = Uid.new()
  let lastName = Name.new(lastName)
  let firstName = Name.new(firstName)
  let kanaLastName = KanaName.new(kanaLastName)
  let kanaFirstName = KanaName.new(kanaFirstName)
  let email = Email.new(email)
  let birthDate = BirthDate.new(birthDate)
  let user = User.new(
    uid, lastName, firstName, kanaLastName, kanaFirstName,
    email, birthDate
  )
  await self.repository.save(user)
```

次にこちらはユーザーデータ全件を取得するユースケースです

```nim
import
  std/asyncdispatch,
  std/json,
  ../domain/user/user_repository_interface,
  ../di_container

type GetAllUsersUsecase* = ref object
  repository: IUserRepository

proc new*(_:type GetAllUsersUsecase):GetAllUsersUsecase =
  return GetAllUsersUsecase(
    repository: di.userRepository
  )

proc invoke*(self:GetAllUsersUsecase):Future[JsonNode] {.async.} =
  let users = await self.repository.getAll()
  return users
```

ユースケースが持つフィールドの`repository`は`IUserRepository`という抽象に依存しています。`repository`の実体である具象クラスが何かは、`di_container`で管理されています。このような呼び出すメソッドの具象クラスを外部から受け渡すことを`依存性の注入`と言います。これを使って`リポジトリ`を外部から注入することをDDDなどの文脈では`依存性の逆転`と言います。

## プレゼンテーション層からアプリケーション層を呼び出す
WebやCLIと言ったアプリケーションの入出力固有の責務を担当する層を`プレゼンテーション層`と言います。Webであればhttp通信のリクエスト・レスポンスという固有のプロトコルに則った入出力になりますし、CLIであれば標準入出力というプロトコルになります。そういった入出力のやり取りを担当し、アプリケーション層にstring型やint型と言ったプリミティブな値でやり取りするのが責務です。
外部とアプリケーションのエントリーポイントであるアプリケーション層を切り分けることで、ビジネスルールの変更がIOに影響されることなく、Webからの呼び出し口とCLIからの呼び出し口を共通化することができます。

### Webから呼び出す
Webは標準ライブラリの`asynchttpserver`を使ってAPIサーバーを実装していきます。

`presentation/web.nim`

```nim
import
  std/asynchttpserver,
  std/asyncdispatch,
  std/json,
  ../errors,
  ../application/create_new_user_usecase,
  ../application/get_all_users_usecase

let htmlResponseHeader = {"Content-type": "text/html; charset=utf-8"}.newHttpHeaders()
let jsonResponseHeader = {"Content-type": "application/json; charset=utf-8"}.newHttpHeaders()

proc main {.async.} =
  var server = newAsyncHttpServer()
  proc cb(req: Request) {.async, gcsafe.} =
    if req.reqMethod == HttpGet and req.url.path == "/":
      await req.respond(Http200, "Hello World", htmlResponseHeader)
    elif req.reqMethod == HttpGet and req.url.path == "/users":
      try:
        let usecase = GetAllUsersUsecase.new()
        let resp = %*{
          "users": await usecase.invoke()
        }
        await req.respond(Http200, $resp, jsonResponseHeader)
      except DomainError:
        let resp = %*{
          "error": getCurrentExceptionMsg()
        }
        await req.respond(Http400, $resp, jsonResponseHeader)
      except InfraError:
        let resp = %*{
          "error": getCurrentExceptionMsg()
        }
        await req.respond(Http500, $resp, jsonResponseHeader)
    elif req.reqMethod == HttpPost and req.url.path == "/users":
      let params = req.body.parseJson()
      let lastName = params["lastName"].getStr
      let firstName = params["firstName"].getStr
      let kanaLastName = params["kanaLastName"].getStr
      let kanaFirstName = params["kanaFirstName"].getStr
      let email = params["email"].getStr
      let birthDate = params["birthDate"].getStr
      let usecase = CreateNewUserUsecase.new()
      try:
        await usecase.invoke(lastName, firstName, kanaLastName, kanaFirstName, email, birthDate)
        let resp = %*{
          "msg": "success"
        }
        await req.respond(Http200, $resp, jsonResponseHeader)
      except DomainError:
        let resp = %*{
          "error": getCurrentExceptionMsg()
        }
        await req.respond(Http400, $resp, jsonResponseHeader)
      except InfraError:
        let resp = %*{
          "error": getCurrentExceptionMsg()
        }
        await req.respond(Http500, $resp, jsonResponseHeader)
    else:
      await req.respond(Http404, "", htmlResponseHeader)

  server.listen(Port(8000))
  while true:
    if server.shouldAcceptRequest():
      await server.acceptRequest(cb)
    else:
      await sleepAsync(500)

waitFor main()
```

実行します

```sh
cd /app/src
nim c -r presentation/web
```

### CLIから呼び出す
同じようにCLIから呼び出します。ここでは`cligen`という3rdパーティーライブラリを使います。
[NimのCLIツール作成用ライブラリcligenがとても便利](https://qiita.com/jiro4989/items/d5476b3dce3c4b3d6523)

```nim
import
  std/asyncdispatch,
  std/json,
  ../application/create_new_user_usecase,
  ../application/get_all_users_usecase

proc createUser(args: seq[string]) {.async.} =
  var lastName = ""
  var firstName = ""
  var kanaLastName = ""
  var kanaFirstName = ""
  var email = ""
  var birthDate = ""
  for i in 0..<6:
    case i
    of 0:
      echo "苗字を入力して下さい"
      lastName = stdin.readLine
    of 1:
      echo "名前を入力して下さい"
      firstName = stdin.readLine
    of 2:
      echo "苗字（カナ）を入力して下さい"
      kanaLastName = stdin.readLine
    of 3:
      echo "名前（カナ）を入力して下さい"
      kanaFirstName = stdin.readLine
    of 4:
      echo "メールアドレスを入力して下さい"
      email = stdin.readLine
    of 5:
      echo "生年月日をハイフン区切りで入力して下さい"
      birthDate = stdin.readLine
    else:
      discard
  let usecase = CreateNewUserUsecase.new()
  await usecase.invoke(
    lastName, firstName, kanaLastName, kanaFirstName, email, birthDate
  )

proc getUsers(args:seq[string]) {.async.} =
  let usecase = GetAllUsersUsecase.new()
  echo await usecase.invoke()


when isMainModule:
  import cligen
  dispatchMulti([createUser], [getUsers])
```

実行します

```sh
cd /app/src
nim c -r presentation/cli createUser
nim c -r presentation/cli getUsers
```

## おわりに
アプリケーション全体の構成としてはこうなりました。

```
├── application
│   ├── create_new_user_usecase.nim
│   └── get_all_users_usecase.nim
├── data.json
├── data.sqlite
├── di_container.nim
├── domain
│   └── user
│       ├── user_entity.nim
│       ├── user_repository_interface.nim
│       └── user_value_objects.nim
├── errors.nim
├── infra
│   └── user
│       ├── json_user_repository.nim
│       └── rdb_user_repository.nim
├── libs
│   └── db.nim
├── migrate.nim
└── presentation
    ├── cli.nim
    └── web.nim
```
ソースコードはこちらから  
https://github.com/itsumura-h/nim-oop-bestpractice

このようにNimではオニオンアーキテクチャの実装方針を採用することができ、`単一責任原則`と`一方向の依存性`、`依存性の注入`と言ったオブジェクト指向のエッセンスを取り入れて、保守性の高いアプリケーション開発をすることができます。  
Web開発にこのようなオニオンアーキテクチャの要素を取り入れたNim製Webフレームワークである`Basolato`を開発していますので、そちらもご覧頂けたらと思います。  
ありがとうございました。

[Basolatoフレームワーク](https://github.com/itsumura-h/nim-basolato)