69
66

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 1 year has passed since last update.

Rust で DDD を実践しながら API サーバーを実装・構築した(つもり)

Last updated at Posted at 2022-01-22

Rust 勉強中の身ですので、何かしら作ってみようと思い立ち、 API サーバーを構築してみました。
自力で一から公開できるサーバーを構築したのは初めてでしたので、試行錯誤の過程を記事にします。

作ったもの

何の変哲もない API サーバーです。
成果物は こちら にアップしました。

作ったもの.png

開発方針

上記のインフラ構成を目標として、以下の開発方針を軸として調査や検証を行いました。

  • ローカルでの開発とサーバーへのデプロイはスムーズにできるようにする。
    • ローカルでテストや動作確認がスムーズにできるようにする:Docker の利用
    • デプロイも GitHub Actions を利用してできるようにする
  • Rust のコードは DDD を実践できるアーキテクチャを採用する

アーキテクチャの検討

一番四苦八苦したのはアーキテクチャでした。試行錯誤の過程を記載します。

クリーンアーキテクチャでの実装と挫折

かの有名な本 も読んだことあったので、こちらの記事を参考に下記の図を忠実に1実装しようとしました。

image.png

内側から実装を順調に進めていたのですが、フレームワーク&ドライバー層の実装方法がわからず詰まってしまいました。詰まった理由としては、「フレームワーク」や「ドライバ」は Diesel や actix-web といった外部ライブラリとして提供されており、エントリポイント以外ではフレームワーク&ドライバー層からインターフェイス層を呼び出す手立てがないためです。

image.png

実際に実装しようとすると、下記のようにインターフェース層からインフラ層を呼び出すという形しか出来ませんでした2

image.png

オニオンアーキテクチャとの出会い

何か別のアプローチがないか調べていたところ、こちらの記事 で紹介されていたオニオンアーキテクチャに出会いました。

image.png

これは DDD のヘキサゴナルアーキテクチャの「Application」部分をより具体化したものとして紹介されています。

313px-Hexagonal_Architecture.svg.png

オニオンアーキテクチャの特徴

従来のレイヤードアーキテクチャは上位の層は直ぐ下の層しか呼び出せないのに対して、オニオンアーキテクチャはさらに下の層の呼び出しも許容しているのが特徴です。

image.png

これにより、ドメインモデルやドメインサービス、ビジネスルールをインフラに依存せずに実装できます。

結局したかったことは何だっけ・・・?

ここまでいろいろ書籍を見返したりネットの記事を調べている中出、目的を見失いそうになったので、整理しました。結局、クリーンアーキテクチャとは、以下の SOLID 原則を満たしているアーキテクチャであることを再確認しました:

  • S:SRP、単一責任の原則
  • O:OCP、解放閉鎖の原則
  • L:LSP、リスコフの置換原則
  • I:ISP、インタフェース分離の原則
  • D:DIP、依存性逆転の原則

オニオンアーキテクチャで API サーバーを実装するにあたっては、 SOLID 原則を満たしているかを注意しながら実装を進めました。

実装内容

アイデアとしては Rust でヘキサゴナルアーキテクチャを実装している こちらも記事 を参考に、ポケモンの情報を CRUD 操作できる API です。動作例は以下の通りです:

$ curl -X GET localhost:8080/pokemon
{"message":"FAILURE Get Pokemon List","type":"get_pokemon_list_error"}

$ curl -X POST -H "Content-Type: application/json" -d '{"number":1, "name":"test_name", "types": [ "Fire" ]}' localhost:8080/pokemon
SUCCESS Register Pokemon
$ curl -X POST -H "Content-Type: application/json" -d '{"number":2, "name":"test_name2", "types": [ "Water", "Electric" ]}' localhost:8080/pokemon
SUCCESS Register Pokemon
$ curl -X GET localhost:8080/pokemon
[{"number":1,"name":"test_name","types":["Fire"]},{"number":2,"name":"test_name2","types":["Water","Electric"]}]

$ curl -X GET localhost:8080/pokemon/1
{"number":1,"name":"test_name","types":["Fire"]}

$ curl -X PUT -H "Content-Type: application/json" -d '{"number":1, "name":"test_name2", "types": [ "Water" ]}' localhost:8080/pokemon/1
SUCCESS Update Pokemon: no 1
$ curl -X GET localhost:8080/pokemon/1
{"number":1,"name":"test_name2","types":["Water"]}

$ curl -X DELETE localhost:8080/pokemon/1
SUCCESS Delete Pokemon: no 1
$ curl -X GET localhost:8080/pokemon
[{"number":2,"name":"test_name2","types":["Water","Electric"]}]

アーキテクチャとしては、先ほどのオニオンアーキテクチャをベースに実装していきました。

図2.png

ドメインモデル層

値オブジェクトやエンティティとビジネスルール、リポジトリを実装します(/server/src/domain/models)。今回は、ドメインサービスに相当するものがありませんでしたが、ある場合は /server/src/domain/services に実装します。
ポケモンオブジェクトは以下のようにします(/server/src/domain/models/pokemon/pokemon.rs):

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Pokemon {
    pub number: PokemonNumber,
    pub name: PokemonName,
    pub types: PokemonTypes,
}

impl Pokemon {
    pub fn new(number: PokemonNumber, name: PokemonName, types: PokemonTypes) -> Self {
        Self {
            number,
            name,
            types,
        }
    }
}

番号や名前、タイプについては値オブジェクトとして定義します。
例えば、番号は以下のようにします(/server/src/domain/models/pokemon/pokemon_number.rs):

use std::convert::TryFrom;

/// ポケモンの図鑑 No を表す。
#[derive(PartialEq, Eq, Clone, PartialOrd, Ord, Debug)]
pub struct PokemonNumber(i32);

/// ポケモンの図鑑 No の振る舞い:u16 から PokemonNumber への変換。
/// 現時点でポケモンの図鑑 No は898 までなので、
/// それ以上にならないように決めている。
impl TryFrom<i32> for PokemonNumber {
    type Error = ();

    fn try_from(n: i32) -> Result<Self, Self::Error> {
        if n > 0 && n < 899 {
            Ok(Self(n))
        } else {
            Err(())
        }
    }
}

/// 図鑑 No から u16 への変換処理の振る舞いを定義。
impl From<PokemonNumber> for i32 {
    fn from(n: PokemonNumber) -> Self {
        n.0
    }
}

番号は構造体として定義するものの、プロパティは1つしかないので、タプル構造体として定義します。
採用する整数型ですが、 ORM の Diesel と PostgreSQL で扱う型の変換の都合上、 i32 を使います。
一般的な整数から PokemonNumber への変換の際にはチェックをしており、 Result 型で返します。

リポジトリについては基本的な CRUD 操作と存在確認を定義します(/server/src/domain/models/pokemon/pokemon_repository.rs):

use anyhow::Result;

/// Pokemon のリポジトリインタフェース
pub trait PokemonRepository {
    /// 番号からポケモンを探す
    fn find_by_number(&self, number: &PokemonNumber) -> Result<Pokemon>;

    /// ポケモン一覧を表示する
    fn list(&self) -> Result<Vec<Pokemon>>;

    /// オブジェクトを永続化(保存)する振る舞い
    fn insert(&self, pokemon: &Pokemon) -> Result<()>;

    /// オブジェクトを再構築する振る舞い
    fn update(&self, pokemon: &Pokemon) -> Result<()>;

    /// オブジェクトを永続化(破棄)する振る舞い
    fn delete(&self, number: &PokemonNumber) -> Result<()>;

    /// 作成したポケモンの重複確認を行う。
    fn exists(&self, pokemon: &Pokemon) -> bool {
        match self.find_by_number(&pokemon.number) {
            Ok(_) => true,
            Err(_) => false,
        }
    }
}

アプリケーションサービス層

いわゆる、ユースケースを定義します。ジェネリクスを利用して DI(依存性の注入)をおこなっています。
以下は、指定された番号のポケモンを取り出してくるユースケースの実装の例です(/server/src/application/pokemon_get_service.rs):

use anyhow::Result;
use std::convert::TryFrom;

/// アプリケーションサービスの構造体。
/// generics でリポジトリへの依存を表し、trait 境界を定義することで、DI を行う。
pub struct PokemonGetService<T>
where
    T: PokemonRepository,
{
    pokemon_repository: T,
}

/// アプリケーションサービスの振る舞いを定義。
impl<T: PokemonRepository> PokemonGetService<T> {
    /// コンストラクタ
    pub fn new(pokemon_repository: T) -> Self {
        Self { pokemon_repository }
    }

    /// 取得処理の実行。
    pub fn handle(&self, no: i32) -> Result<PokemonData> {
        let number = PokemonNumber::try_from(no).unwrap();
        match self.pokemon_repository.find_by_number(&number) {
            Ok(value) => Ok(PokemonData::new(value)),
            Err(_) => Err(anyhow::anyhow!(
                "取得しようとしたポケモンが存在しません: no {:?}",
                number
            )),
        }
    }
}

外部からドメインオブジェクトを直接扱わないように DTO(Data Transfer Object)も定義しています(/server/src/domain/application/pokemon_data.rs):

use crate::domain::models::pokemon::pokemon::Pokemon;
use getset::Getters;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;

#[derive(Serialize, Deserialize, Clone, Getters, PartialEq, Eq, Debug)]
pub struct PokemonData {
    #[getset(get = "pub with_prefix")]
    number: i32,
    #[getset(get = "pub with_prefix")]
    name: String,
    #[getset(get = "pub with_prefix")]
    types: Vec<String>,
}

impl PokemonData {
    pub fn new(source: Pokemon) -> Self {
        Self {
            number: source.number.try_into().unwrap(),
            name: source.name.try_into().unwrap(),
            types: source.types.try_into().unwrap(),
        }
    }
}

インフラ層

DB とのやりとりと Actor を定義します。外部ライブラリを利用して、 Adaptor としての役割を担います。

DB とのやりとり:Diesel を利用

Diesel の使いから、DB のマイグレーションについては(手前味噌ですが)私が過去に書いた記事を参考にしていただければと思います。

下層であるドメイン層のリポジトリの実装を行います。
これにより、依存性の逆転を実現しています:

ソースコードのうち、ポケモンデータの取得処理の部分を示します(/server/src/infra/diesel/pokemon/pokemon_repository.rs):

use anyhow::{Context, Result};
use diesel::pg::PgConnection;
use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, Pool};
use std::convert::TryInto;

/// Diesel が直接利用するデータモデル。
#[derive(Debug, Queryable, Clone)]
pub struct PokemonEntity {
    pub no: i32,
    pub name: String,
    pub type_: Vec<String>,
}

#[derive(Debug, Insertable)]
#[table_name = "pokemon"]
pub struct NewPokemon {
    pub no: i32,
    pub name: String,
    pub type_: Vec<String>,
}

/// Pokemon の振る舞い: PokemonEntity から Pokemon への変換処理。
impl From<PokemonEntity> for Pokemon {
    fn from(entity: PokemonEntity) -> Pokemon {
        Pokemon {
            number: entity.no.try_into().unwrap(),
            name: entity.name.try_into().unwrap(),
            types: entity.type_.try_into().unwrap(),
        }
    }
}

pub struct PokemonRepositoryImpl {
    pub pool: Box<Pool<ConnectionManager<PgConnection>>>,
}

impl PokemonRepository for PokemonRepositoryImpl {
    ...

    /// 引数で渡した図鑑 No のポケモンを返却する
    fn find_by_number(&self, number: &PokemonNumber) -> Result<Pokemon> {
        let conn = self.pool.get().context("failed to get connection")?;
        let target_num: i32 = number.clone().try_into().unwrap();
        match pokemon
            .filter(pokemon::no.eq(target_num))
            .load::<PokemonEntity>(&conn)
        {
            Ok(result) => match result.get(0) {
                Some(value) => Ok(Pokemon::from(value.clone())),
                None => Err(anyhow::anyhow!("Not Found Pokemon number:{}", target_num)),
            },
            Err(e) => Err(anyhow::anyhow!(e)),
        }
    }
    ...
}

各メソッド内でコネクションプールからコネクションを取得して利用するようにします。

外部からのリクエストの受付(Actor):actix-web の利用

受け付けた HTTP リクエストを actix-web を利用して処理するようにします。
リクエストの JSON データを格納する型を以下のように定義します(/server/src/infra/actix/request.rs):

use std::convert::TryInto;
use crate::domain::models::pokemon::pokemon::Pokemon;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Deserialize, Serialize)]
pub struct PokemonRequest {
    pub number: i32,
    pub name: String,
    pub types: Vec<String>,
}

impl PokemonRequest {
    pub fn of(&self) -> Pokemon {
        Pokemon::new(
            self.number.try_into().unwrap(),
            self.name.clone().try_into().unwrap(),
            self.types.clone().try_into().unwrap(),
        )
    }
}

(今記事で扱っているポケモンデータの取得処理ではこれは使いません)。
リクエストのパス、メソッドに応じた処理は以下のように実装できます(/server/src/infra/actix/handler.rs):

use crate::infra::actix::request::PokemonRequest;
use actix_web::{delete, get, post, put, web, web::Json, HttpResponse, Responder};
use serde::Serialize;

#[derive(Serialize)]
struct ErrorResponse {
    message: String,
    r#type: String,
}

...

#[get("/pokemon/{number}")]
async fn get_pokemon(
    data: web::Data<RequestContext>,
    path_params: web::Path<(i32,)>,
) -> impl Responder {
    let pokemon_application = PokemonGetService::new(data.pokemon_repository());
    let no = path_params.into_inner().0.into();
    match pokemon_application.handle(no) {
        Ok(pokemon) => HttpResponse::Ok().json(pokemon),
        Err(_) => {
            let response = ErrorResponse {
                message: format!("FAILURE Get Pokemon: no {:?}", no),
                r#type: "get_pokemon_list_error".to_string(),
            };
            HttpResponse::InternalServerError().json(response)
        }
    }
}

...

エラーについては、メッセージとタイプを定義して、レスポンスで返却できるようにしています。
サーバーの起動処理は以下のように実装できます(/server/src/infra/actix/router.rs):

use super::handlers;
use crate::{config::CONFIG, domain::models::pokemon::pokemon_repository::PokemonRepository};
use actix_web::{middleware::Logger, App, HttpServer};
use diesel::{
    r2d2::{ConnectionManager, Pool},
    PgConnection,
};

#[actix_web::main]
pub async fn run() -> std::io::Result<()> {
    dotenv::dotenv().ok();
    let port = std::env::var("PORT")
        .ok()
        .and_then(|val| val.parse::<u16>().ok())
        .unwrap_or(CONFIG.server_port);

    HttpServer::new(|| {
        App::new()
            .data(RequestContext::new())
            .wrap(Logger::default())
            .service(handlers::health)
            .service(handlers::post_pokemon)
            .service(handlers::get_pokemon)
            .service(handlers::update_pokemon)
            .service(handlers::delete_pokemon)
            .service(handlers::get_pokemon_list)
    })
    .bind(format!("{}:{}", CONFIG.server_address, port))?
    .run()
    .await
}

#[derive(Clone)]
pub struct RequestContext {
    pool: Pool<ConnectionManager<PgConnection>>,
}

impl RequestContext {
    pub fn new() -> RequestContext {
        let manager = ConnectionManager::<PgConnection>::new(&CONFIG.database_url);
        let pool = Pool::builder()
            .build(manager)
            .expect("Failed to create DB connection pool.");
        RequestContext { pool }
    }

    pub fn pokemon_repository(&self) -> impl PokemonRepository {
        use crate::infra::diesel::pokemon_repository::PokemonRepositoryImpl;

        PokemonRepositoryImpl {
            pool: Box::new(self.pool.to_owned()),
        }
    }
}

これで、main 関数から run 関数を呼び出すと、Web サーバーが起動します。

補足:環境変数の値取り出しについて

Rust の dotenv を利用し、 .env ファイルに書かれた環境変数を読み出すようにしていますが、アプリ起動時に環境変数を static 変数 CONFIG に格納するようにしています。

詳しい方法は(手前味噌ですが)私が過去に書いたメモをご参照ください。

ローカルでの動作確認

DB とのやりとりを含むコードですので、そのまま cargo run しただけでは動作確認ができません。 DB とのやりとりも含めた動作確認ができる環境を Docker(docker-compose)を用いて構築します。

DB 設定のための Dockerfile

/db/Dockerfile に DB のための設定を書きます:

ENV LANG ja_JP.utf8
FROM postgres:14-alpine AS db

/db/docker-entrypoint-initdb.d のディレクトリの下に DB 立ち上げ直後に実行する SQL を置いておきます3

CREATE ROLE root WITH LOGIN;
CREATE DATABASE root;

API サーバのための Dockerfile

/server/Dockerfile に API サーバを立ち上げるための設定を書きます:

  • マルチステージビルドを利用します。
    • 開発環境(develop-stage)ではローカル環境でサーバーを稼働させるためのコマンド cargo-watch 、Diesel で PostgreSQL を扱う際に必要な libpg-dev 、Diesel でマイグレーションなどを行うのに必要なコマンド diesel_cli をインストールします。また、本番環境のために成果物をコピーしておきます。
    • ビルド環境(build-stage)では、開発環境を用いて本番環境向けのビルドを実行します。
    • 本番環境(production-stage) では、ビルド環境の成果物をコピーしてきて実行します。
# 開発環境
FROM rust:1.57.0 as develop-stage
WORKDIR /app
RUN cargo install cargo-watch
RUN apt install -y libpq-dev
RUN cargo install diesel_cli
COPY . .

# ビルド環境
FROM develop-stage as build-stage
RUN update-ca-certificates
RUN cargo build --release

# 本番環境
FROM rust:1.57.0-slim-buster as production-stage
RUN apt-get update
RUN apt-get install libpq-dev -y
COPY --from=build-stage /app/target/release/actix_web_sample .
CMD ["./actix_web_sample"]

docker-compose.yml の記述

DB とサーバーの両方をローカル環境で立ち上げるために、 docker-composed.yml を用意します。

  • DB は pg_isready コマンドでヘルスチェックを行い、正常に起動していることを確認します。
  • DB の環境変数は後に紹介する .env ファイルの内容に合わせて設定します。
  • サーバーは DB が正常に起動したことを確認してから起動します。
  • 起動の際、diesel によるマイグレーション(テーブル作成)と cargo watch によるアプリ起動を行います。
    • cargo watch により、ソースの保存を自動で検知して再ビルドしてくれるため、開発が楽になります。
version: '3.7'
services:
  server:
    build:
      context: ./server
    target: 'develop-stage'
    ports:
      - "8080:8080"
    depends_on:
      db:
    condition: service_healthy
    volumes:
      - ./server:/app
      - cargo-cache:/usr/local/cargo/registry
      - target-cache:/app/target
    command: /bin/sh -c "diesel setup && cargo watch -x run"
    tty: true
  
   db:
     build:
       context: ./db
     ports:
       - '5432:5432'
     environment:
       POSTGRES_USER: admin
       POSTGRES_PASSWORD: password
       POSTGRES_DB: postgres
     volumes:
       - ./db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
     healthcheck:
       test: ["CMD-SHELL", "pg_isready", "-U", "${POSTGRES_USER}", "-d", "postgres"]
       interval: 10s
       timeout: 5s
       retries: 5
       
  volumes:
    cargo-cache:
    target-cache:

.env の用意

ローカルで立ち上げる際に利用する環境変数の設定を記述します。

SERVER_PORT=8080
DATABASE_URL=postgres://admin:password@db:5432/postgres
SERVER_ADDRESS=0.0.0.0

ローカル開発環境でのサーバ&DB立ち上げ

docker-compose を用いてローカル環境を立ち上げます。
前述の通り、開発中は立ち上げっぱなしにしておけば、Rust のコードを変更した場合でも自動的にビルドして反映してくれます。

docker-compose up -d

Heroku へのデプロイ

実装した API サーバーを Heroku にデプロイします。Heroku のアカウントは作成済みで、 Heroku CLI もインストール済みであるとします。

Heroku の設定

コマンドラインから Heroku の設定をします。アプリの追加と、 PostgreSQL のアドオンを追加します。
PostgreSQL のアドオンを追加すると自動的にアプリの環境変数 DATABASE_URL が追加されます。

# Heroku に CLI でログイン(ブラウザが立ち上がるのでログインを行う)
$ heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/XXXXX
Logging in... done
Logged in as hoge@mail.com
heroku create rust-web-api-server-sample

# アプリの作成
$ heroku create rust-web-api-sample
Creating ⬢ rust-web-api-sample... done
https://rust-web-api-sample.herokuapp.com/ | https://git.heroku.com/rust-web-api-sample.git

# アプリに PostgreSQL のアドオン(Hobby プラン)を追加
$ heroku addons:create heroku-postgresql:hobby-dev --app rust-web-api-sample
Creating heroku-postgresql:hobby-dev on ⬢ rust-web-api-sample... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-contoured-25420 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation

GitHub Actions の設定

ここから、手でコマンドをポチポチ打ってデプロイをしても良いのですが、せっかくなので、 GitHub Action でデプロイを自動化します。GitHub のリポジトリに push したら Heroku にデプロイするように設定します。

以下のように .github/workflows/deploy-to-heroku.yml を作成します:

  • AkhileshNS/heroku-deploy を利用してデプロイします。
  • Heroku へのデプロイは Docker を利用する設定にしています。
  • あらかじめ、リポジトリのシークレットに以下を設定しておきます:
    • HEROKU_API_KEY: Heroku の Account Settings 画面 -> API Key で取得できる API キーを指定
    • HEROKU_EMAIL: Heroku のアカウントで使用しているメールアドレスを指定
  • 起動したサーバーの IP アドレスは環境変数で設定します。
  • 環境変数の設定は heroku config:set コマンドで行います。
    • Github Actions 内で heroku コマンドを利用するためには Heroku へのログイン処理を行う必要があるのですが、heroku-deploy を一度でも使うことで Heroku へのログインを行なってくれます。
    • このツールを使ってログイン処理だけ行うということもできます(justlogin オプション)
  • ヘルスチェックも入れています。
name: Deploy to heroku

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: akhileshns/heroku-deploy@v3.12.12
        with:
          heroku_api_key: ${{secrets.HEROKU_API_KEY}}
          heroku_app_name: "rust-web-api-server-sample"
          heroku_email: ${{secrets.HEROKU_EMAIL}}
          buildpack: "https://github.com/emk/heroku-buildpack-rust.git"
          usedocker: true
          appdir: server
          healthcheck: "https://rust-web-api-server-sample.herokuapp.com/health"
          checkstring: "Ok"
          rollbackonhealthcheckfailed: true
      - run: |
          heroku config:set SERVER_ADDRESS=0.0.0.0

これでデプロイが成功したら、 https://rust-web-api-server-sample.herokuapp.com/ が利用できるようになります。

補足:Heroku へのデプロイにあたってのポート指定の環境変数について

Heroku でのサーバー起動の際のポートの指定は、環境変数 PORT の値を使用するようにすればOKですので、以下のように書けばOKです。

 let port = std::env::var("PORT")
        .ok()
        .and_then(|val| val.parse::<u16>().ok())
        .unwrap_or(CONFIG.server_port)

今回は、ローカルでの起動も考慮に入れているので、環境変数 PORT の指定がなければ、 .env ファイルの SERVER_PORT の値を利用するという設定にしています。

まとめ

  • 一から Web API サーバーを初めて実装してみました。
  • アーキテクチャについては色々悩みましたが、 SOLID 原則を満たし、かつ、実装の方法が分かりやすかったオニオンアーキテクチャを採用しました。
  • ローカル開発からデプロイまで、 Docker を利用することでラクに環境構築をすることができました。
  • 今後はこの方法・実装方法をテンプレートとして利用できればと思います。

追記1: 「これって DDD やなくない?」(2022/01/25)

Twitter で下記のコメントいただきました:

ご指摘の通り、タイトルには「DDD を実践」と書いたのですが、ドメインルールの実装がまだまだ甘いのでこれは DDD ではないです。本文で

今回は、ドメインサービスに相当するものがありません

と書いた通りです。

ユースケースやビジネスルールを明確に定義することが実際の場面では必要になってくると思います。

追記2: 似たような実践例(2022/01/25)

似たような実践例がついこないだのアドベントカレンダーに投稿されていたということをお聞きしましたので紹介します:

Rust の新しい HTTP サーバーのクレート Axum をフルに活用してサーバーサイドアプリケーション開発をしてみる - Don't Repeat Yourself

Web アプリフレームワークには新しめの Axum を使ってたり、アーキテクチャとしては Explicit Architecture を採用しているなどの違いはあるものの、かなり参考になる記事でした。

これを参考に、リファクタリングしてみようかと思います。

参考資料

  1. 「忠実な」の主な対象はユースケース層を指します。あちこちで見られるユースケース層の実装を見ると、ユースケースを「Output/Input Port」「Interactor」に分割して実装している(もしくは、名前を明示的にそうしている)ものがあまり見られなかったので、敢えて明示的にこれらを分割して実装しようとしてました。

  2. もしかすると、上手いやり方が存在するのかもしれないのですが、調べきれていない/思いついていないです。

  3. 今回は、DB 立ち上げ時にロール root とデータベース root がないよ、というエラーメッセージが出るのに対応するための SQL を置いてます。なぜこのエラーが出るのかはよく分かってません。

69
66
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
69
66

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?