エンティティの取得

前回のセットアップでツイートの取得ができましたが、今後のためにもう少しコードを整理しておきましょう。

前回のコントローラー実装の問題点

現状ではコントローラーがデータベースからのデータ取得の役割も担っていますが、データをどのように取得するかは 技術的な関心事 であり、コントローラーの役割としては少し責任が重くなってしまっています。

例えば、PostgreSQLをやめて全く別種のデータベースであるMongoDBを使うことにしたとしたらどうでしょうか。本来的には「保存されているツイートを返す」という役割を持っているだけのコントローラーが、技術的な関心事であるデータベースシステムの変更に対応しなければならなくなってしまいます。

今回は依存性逆転の原則1というテクニックを利用して、この問題にアプローチしてみます。

リポジトリの定義

まずは、データ取り扱いのインターフェースとなる リポジトリ をトレイトとして用意します。

src/repositories/tweets.rs
use crate::entities::Tweet;

#[axum::async_trait]
pub trait Tweets {
    async fn list(&self) -> Vec<Tweet>;
}

このトレイトでは

  • ツイートを全て取得することができ、その返却値は Vec<Tweet> である

だけが定義されており、技術的なことは明示されていません。これにより、コントローラは技術的な関心事に振り回されることなく、コントローラーとしてやるべきことだけに専念することができます。

Note: src/repositories.rs の作成と src/lib.rs へのmodの記載も必要です。今までと同じ要領で変更するだけなので割愛します。

リポジトリの実装

今までコントローラーに記載していたものはコントローラーとは別の場所で実装する必要があります。別途、構造体を定義し、上記で定義したリポジトリを実装してみましょう。

src/repositories_impl/tweets.rs
use crate::database::ConnectionPool;
use crate::entities::Tweet;
use crate::repositories;
use chrono::{DateTime, Utc};

pub struct Tweets<'a> {
    pub pool: &'a ConnectionPool,
}

#[axum::async_trait]
impl<'a> repositories::Tweets for Tweets<'a> {
    async fn list(&self) -> Vec<Tweet> {
        let conn = self.pool.get().await.unwrap();
        let rows = conn.query("SELECT * FROM tweets", &[]).await.unwrap();
        rows.into_iter()
            .map(|r| {
                Tweet::new(
                    r.get("name"),
                    r.get("message"),
                    r.get::<&str, DateTime<Utc>>("posted_at")
                        .format("%F")
                        .to_string(),
                )
            })
            .collect()
    }
}

データベースからデータを取り出すためにはコネクションプールの情報が必要です。なので、構造体の生成時に接続情報の参照を持たせることにします。また、この構造体のインスタンスが存在する間は、このコネクションプールの情報を参照できる必要があるので、構造体と参照それぞれにライフタイム指定子をつけておきます。

これにより、リポジトリを実装した構造体の準備もできました。

Note: src/repositories_impl.rs の作成と src/lib.rs へのmodの記載も必要です。今までと同じ要領で変更するだけなので割愛します。

コントローラへの注入

前回、ExtensionLayerを使ってコネクションプールの情報をコントローラーに渡す方法を実装しましたが、今回は更にそれを拡張子し、このリポジトリをコントローラに渡すことにします。

src/database.rs
use crate::repositories_impl::Tweets as TweetsImpl;
use bb8::Pool;
use bb8_postgres::PostgresConnectionManager;
use std::env;
use tokio_postgres::NoTls;
use tower_http::add_extension::AddExtensionLayer;

pub type ConnectionPool = Pool<PostgresConnectionManager<NoTls>>;

pub async fn repository_layer() -> AddExtensionLayer<RepositoryProvider> {
    dotenv::dotenv().ok();
    let manager =
        PostgresConnectionManager::new_from_stringlike(env::var("DATABASE_URL").unwrap(), NoTls)
            .unwrap();
    let pool = Pool::builder().build(manager).await.unwrap();
    AddExtensionLayer::new(RepositoryProvider(pool))
}

#[derive(Clone)]
pub struct RepositoryProvider(ConnectionPool);

impl RepositoryProvider {
    pub fn tweets(&self) -> TweetsImpl {
        TweetsImpl { pool: &self.0 }
    }
}

RepositoryProvider という構造体を用意しました。ExtensionLayerではこの構造体のインスタンスを持たせるようにしています。 RepositoryProviderは tweets というメソッドを持っているので、コントローラー側ではこのメソッドを呼び出すことで Tweets リポジトリを実装したインスタンスを取得することができます。

Note: ExtensionLayerに持たせるデータは Clone トレイトを実装している必要があります。

メイン処理でも、このレイヤーを使うように修正しましょう。

src/lib.rs
mod controllers;
mod database;
mod entities;
mod forms;
mod repositories;
mod repositories_impl;
mod services;
mod views;

pub use controllers::routes;
pub use database::*;  
src/main.rs
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    let routes = rustwi::routes().layer(rustwi::repository_layer().await);  // repository_layerを呼び出す
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(routes.into_make_service())
        .await
        .unwrap();
}

コントローラーの修正

コントローラーでは、ExtensionLayerからRepositoryProviderを受け取り、リポジトリを操作します。

src/controllers/root.rs
use crate::repositories::Tweets;
use crate::views::Index;
use crate::RepositoryProvider;
use axum::extract::Extension;
use axum::response::IntoResponse;
use axum::routing;
use axum::Router;

pub fn root() -> Router {
    Router::new().route("/", routing::get(get))
}

async fn get(Extension(repository_provider): Extension<RepositoryProvider>) -> impl IntoResponse {
    let tweets = repository_provider.tweets().list().await;
    Index::new(tweets).into_html()
}

しかしこれでは repository_provider.tweets() で取得できるものがリポジトリの 実装 になっているので、リポジトリの 定義 をした意味がよく分からなくなってしまいます。

実は、更にコントローラーの実装を分割することができます。新たにサービスというモジュールを作成します。

src/services/tweets.rs
use crate::repositories::Tweets;
use crate::views::Index;

pub async fn list_tweets(repo: &impl Tweets) -> Index {
    let tweets = repo.list().await;
    Index::new(tweets)
}

ここで、ようやく定義した repositories::Tweets が引数の型として現れました。この処理だけを見た場合、repositories::Tweets の参照を存在型2としてもらい、 views::Index 型を返す関数と見ることができます。 技術的な関心事 を排除し、「どのリポジトリを使い、どんなビューを返すか」という役割に専念することができるようになりました。

コントローラーも見てみましょう。

src/controllers/root.rs
use crate::services;
use crate::RepositoryProvider;
use axum::extract::Extension;
use axum::response::IntoResponse;
use axum::routing;
use axum::Router;

pub fn root() -> Router {
    Router::new().route("/", routing::get(get))
}

async fn get(Extension(repository_provider): Extension<RepositoryProvider>) -> impl IntoResponse {
    let tweet_repo = repository_provider.tweets();
    let view = services::list_tweets(&tweet_repo).await;  // サービスを利用する
    view.into_html()
}

コントローラーについては先ほどとあまり変わっていないように感じられます。むしろ、サービスを作ったので、余計にコード量が増えているように感じるかも知れません。しかし、コントローラーはaxumを取り扱っているため、技術的な関心事(Webフレームワーク)を含んでいます。そのため、コントローラは技術的な関心事に集中できるようになったと考えることもできます。

役割の整理

ここまで様々な役割を持ったモジュールが登場したので、もう一度役割を整理してみましょう。

モジュール役割利用されている技術的関心事
コントローラーWebフレームワークを取り扱う-Webフレームワーク
サービスユースケースを実装するコントローラー-
エンティティビジネスモデルを実装するリポジトリ-
リポジトリ(定義)データ入出力の仕様を定義するサービス-
リポジトリ(実装)データベースを取り扱いデータ入出力を実装する-データベース
フォームフォームデータのデータ構造を定義するコントローラー-
ビューレスポンスのデータ構造を定義するサービス-

技術的関心事を持つモジュールが他のモジュールからは利用されておらず、技術な変更に柔軟に対応できるようになっていることが分かります。


1

依存性逆転の原則とはオブジェクト指向におけるソフトウェア設計の5原則(SOLID)の一つです。ここでは詳細な説明は割愛しますが、よく分かってない方は こちら をご覧ください。

2

impl トレイト型 のような型を 存在型 といいます。関数・メソッドの引数や戻り値に存在型を使えます。存在型を使うことで、具体的な型の指定を宣言側ではなく利用側に決定させることができます。存在型は コンパイル時に具体的な型に解決される ため、実行時に条件分岐で渡す/返す型が決まるような場合には使えません。 Box<T> 型など、ヒープ領域への参照として取り扱う型を利用する必要があります。