Rust で Abstract Factory パターンを使ってみる
こんにちは、たかしです。
AI の台頭により単純なコーディング作業は自動化されていきそうな昨今です。今後、プログラマは「プロジェクトに適しているコード」を見つけ、AI のコードを洗練する力が重視されると思います。 デザインパターンを覚えておけばこの能力が大きく上がるのではないかと思い立ち、今更ながら GoF 本を読み始めました。
今回は GoF 本にある Abstract Factory パターンを Rust で試してみた時の学習記録です。
作ったものの GitHub リポジトリ:
Abstract Factory パターンの概要
ひとことで表すと以下のような感じなのかなと思います。
「インスタンス群のファクトリについて、インターフェース等で抽象化することで各インスタンスの置き換えを容易にする」
自分で書いといてなんですが、よく分からないですね。。。AI に聞いたりググったりするほうがよく分かると思います。
プロジェクトの概要
今回は、DB とのやり取りを行うリポジトリ構造体群に Abstract Factory パターンを適用していきます。具体的には、「MySQL を扱う DB」と「PostgreSQL を扱う DB」のどちらに接続するかを main 関数の実行時点で判断・切替できるようにします。言葉で説明してもわかりづらいと思うので、今回のコンポーネント構成を画像で示しておきます。
全然関係ない話なんですが、こういう画像を作ると「絶対見づらいよなぁ…。けどこれ以上良くしようとすると時間かかるしなぁ…」という葛藤が生まれます。私だけでしょうか?
使用技術
言語:Rust
使用DB:Mysql, PostgreSQL
動作環境:Docker コンテナ
主要な使用クレート
DB操作:sqlx
web フレームワーク:actix-web
エラーハンドリング:anyhow
最終的なディレクトリ構成
repository 直下の mod.rs と user.rs では抽象化された Repository や Factory を扱います。
├── api
│ ├── src
│ │ ├── endpoints // エンドポイント群
│ │ │ ├── mod.rs
│ │ │ └── user.rs
│ │ ├── main.rs
│ │ └── repository // リポジトリ群
│ │ ├── mod.rs
│ │ ├── user.rs
│ │ ├── postgres // PostgreSQL 用リポジトリの具体的な実装
│ │ │ ├── mod.rs
│ │ │ └── user.rs
│ │ └── mysql // MySQL 用リポジトリの具体的な実装
│ │ ├── mod.rs
│ │ └── user.rs
│ ├── Cargo.lock
│ └── Cargo.toml
├── docker-compose.yml
└── docker
└── Dockerfile
実際に実装していく
以下の手順で勧めていきます。
1. まずはデザインパターンを適用せずに MySQL DB から取得
2. Abstract Factory パターンを適用し、ファクトリを抽象化する
3. 環境変数で PostgreSQL と MySQL を切り替えられるようにする
では、順番に見ていきましょう。
1. まずはデザインパターンを適用せずに MySQL DB から取得
MySQL からユーザーデータを取得できる状態にします。 完成したものが以下になります。
まずは main.rs から見ていきましょう。main.rs では「1. サーバーを起動 ・2. リポジトリを生成・3. 各エンドポイントに渡す」といったことをしています。
mod endpoints;
mod repository;
use actix_web::{web, App, HttpServer};
use anyhow::Result;
use crate::repository::{mysql::user::create_mysql_user_repository};
#[tokio::main]
async fn main() -> Result<()> {
println!("Starting server at http://0.0.0.0:8000");
// リポジトリを作成
let user_repo = create_mysql_user_repository()
.await
.expect("Failed to create user repository");
// リポジトリをアプリケーションのデータとして共有するためにラップ
// web::Data は Arc を内包しているため、スレッドセーフで共有可能
let user_repo = web::Data::new(user_repo);
HttpServer::new(move || {
App::new()
// 各ハンドラ関数で共有可能なデータとしてリポジトリを追加
.app_data(user_repo.clone())
.service(endpoints::root_scope())
})
.bind(("0.0.0.0", 8000))?
.run()
.await
.map_err(|e| anyhow::anyhow!("Failed to start server: {}", e))
}
次に、エンドポイントです。リポジトリからデータを取得してレスポンスしています。
use actix_web::{get, web, HttpResponse, Responder};
use crate::repository::user::UserRepository;
#[get("/users")]
pub async fn get_users(user_repo: web::Data<Box<dyn UserRepository>>) -> impl Responder {
let users = user_repo.find_all().await.expect("Failed to fetch users");
HttpResponse::Ok().json(users)
}
次に、repository 直下の user.rs です。UserRepository のインターフェースとなる Trait を定義しています。
use anyhow::Result;
use async_trait::async_trait;
use serde::Serialize;
use sqlx::prelude::FromRow;
#[derive(Serialize, FromRow, Debug)]
pub struct UserRecord {
pub id: i32,
pub name: String,
pub email: String,
}
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn find_all(&self) -> Result<Vec<UserRecord>>;
}
最後に、repository/mysql 内の user.rs です。以下のようなことをしています。
・UserRepository Trait を実装した MySQLUserRepository を定義
・MySQLUserRepository を生成するためのファクトリ関数を定義
use crate::repository::user::{UserRecord, UserRepository};
use anyhow::Result;
use async_trait::async_trait;
pub struct MySQLUserRepository {
pool: sqlx::Pool<sqlx::MySql>,
}
impl MySQLUserRepository {
pub fn new(pool: sqlx::Pool<sqlx::MySql>) -> Self {
MySQLUserRepository { pool }
}
}
#[async_trait]
impl UserRepository for MySQLUserRepository {
async fn find_all(&self) -> Result<Vec<UserRecord>> {
let users = sqlx::query_as::<_, UserRecord>("SELECT id, name, email FROM users")
.fetch_all(&self.pool)
.await
.map_err(|e| anyhow::anyhow!("Failed to fetch users: {}", e))?;
Ok(users)
}
}
pub async fn create_mysql_user_repository() -> Result<Box<dyn UserRepository>> {
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set in the environment variables");
let pool = sqlx::Pool::<sqlx::MySql>::connect(&database_url)
.await
.map_err(|e| anyhow::anyhow!("Failed to connect to MySQL: {}", e))?;
Ok(Box::new(MySQLUserRepository::new(pool)))
}
2. Abstract Factory パターンを適用し、ファクトリを抽象化する
さて、今回は実行時に PostgreSQL に切り替えられるようにしたいという事でしたね。 MySQL 用のリポジトリと PostgreSQL 用のリポジトリの生成を同一インターフェースを通じて行えるように、Factory を抽象化していきます。
まず、repository/mod.rs に RepositoryFactory Trait を定義します。RepositoryFactory Trait は、アプリケーションに必要な各リポジトリの生成メソッドを要求します。
pub mod mysql;
pub mod user;
use async_trait::async_trait;
use anyhow::Result;
use crate::repository::user::UserRepository;
#[async_trait]
pub trait RepositoryFactory {
async fn create_user_repository() -> Result<Box<dyn UserRepository>>;
}
次に、RepositoryFactory Trait を実装する MySQLRepositoryFactory を定義します。
use crate::repository::{user::UserRepository, RepositoryFactory};
use anyhow::Result;
use async_trait::async_trait;
mod user;
pub struct MySQLRepositoryFactory {}
#[async_trait]
impl RepositoryFactory for MySQLRepositoryFactory {
async fn create_user_repository() -> Result<Box<dyn UserRepository>> {
user::create_mysql_user_repository()
.await
.map_err(|e| anyhow::anyhow!("Failed to create MySQL user repository: {}", e))
}
}
最後に、main 関数で MySQLRepositoryFactory を使うように変更します。
mod endpoints;
mod repository;
use actix_web::{web, App, HttpServer};
use anyhow::Result;
use crate::repository::mysql::MySQLRepositoryFactory;
use crate::repository::RepositoryFactory;
#[tokio::main]
async fn main() -> Result<()> {
println!("Starting server at [http://0.0.0.0:8000](http://0.0.0.0:8000/)");
// ここで MySQLRepositoryFactory を通じてユーザーリポジトリを生成するように変更
let user_repo = MySQLRepositoryFactory::create_user_repository().await?;
let user_repo = web::Data::new(user_repo);
HttpServer::new(move || {
App::new()
.app_data(user_repo.clone())
.service(endpoints::root_scope())
})
.bind(("0.0.0.0", 8000))?
.run()
.await
.map_err(|e| anyhow::anyhow!("Failed to start server: {}", e))
}
3. 環境変数で PostgreSQL と MySQL を切り替えられるようにする
これで安全に PostgreSQL リポジトリを作っていく準備が整いました。
早速、実装していきましょう。
まず、main.rs を以下のように変更していきます。
・どの DB を利用するかの環境変数を取得
・DB ごとに利用するリポジトリファクトリを変更
mod endpoints;
mod repository;
use actix_web::{web, App, HttpServer};
use anyhow::Result;
use crate::repository::mysql::MySQLRepositoryFactory;
use crate::repository::postgres::PostgreSQLRepositoryFactory;
use crate::repository::RepositoryFactory;
#[tokio::main]
async fn main() -> Result<()> {
println!("Starting server at http://0.0.0.0:8000");
// 環境変数から使用するDBを取得
let usage_db = std::env::var("USAGE_DB").unwrap_or_else(|_| "mysql".to_string());
println!("Using database: {}", usage_db);
// DBタイプに応じたリポジトリを作成
let user_repo = match usage_db.as_str() {
"postgres" => PostgreSQLRepositoryFactory::create_user_repository().await?,
_ => MySQLRepositoryFactory::create_user_repository().await?, // デフォルトはMySQL
};
// リポジトリをアプリケーションのデータとして共有するためにラップ
// web::Data は Arc を内包しているため、スレッドセーフで共有可能
let user_repo = web::Data::new(user_repo);
HttpServer::new(move || {
App::new()
// 各ハンドラ関数で共有可能なデータとしてリポジトリを追加
.app_data(user_repo.clone())
.service(endpoints::root_scope())
})
.bind(("0.0.0.0", 8000))?
.run()
.await
.map_err(|e| anyhow::anyhow!("Failed to start server: {}", e))
}
次に、PostgreSQL 用のリポジトリを作成していきます。 まずは、ファクトリ構造体から行きましょう。ほぼほぼ MySQL と同じです。
use crate::repository::{user::UserRepository, RepositoryFactory};
use anyhow::Result;
use async_trait::async_trait;
mod user;
pub struct MySQLRepositoryFactory {}
#[async_trait]
impl RepositoryFactory for MySQLRepositoryFactory {
async fn create_user_repository() -> Result<Box<dyn UserRepository>> {
user::create_mysql_user_repository()
.await
.map_err(|e| anyhow::anyhow!("Failed to create MySQL user repository: {}", e))
}
}
最後に、PostgreSQL 用ユーザーリポジトリの具体的な実装をしていきます。これも MySQL とほぼ同じです。
use crate::repository::user::{UserRecord, UserRepository};
use anyhow::Result;
use async_trait::async_trait;
pub struct PostgreSQLUserRepository {
pool: sqlx::Pool<sqlx::Postgres>,
}
impl PostgreSQLUserRepository {
pub fn new(pool: sqlx::Pool<sqlx::Postgres>) -> Self {
PostgreSQLUserRepository { pool }
}
}
#[async_trait]
impl UserRepository for PostgreSQLUserRepository {
async fn find_all(&self) -> Result<Vec<UserRecord>> {
let users = sqlx::query_as::<_, UserRecord>("SELECT id, name, email FROM users")
.fetch_all(&self.pool)
.await
.map_err(|e| anyhow::anyhow!("Failed to fetch users: {}", e))?;
Ok(users)
}
}
pub async fn create_postgres_user_repository() -> Result<Box<dyn UserRepository>> {
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set in the environment variables");
let pool = sqlx::Pool::<sqlx::Postgres>::connect(&database_url)
.await
.map_err(|e| anyhow::anyhow!("Failed to connect to PostgreSQL: {}", e))?;
Ok(Box::new(PostgreSQLUserRepository::new(pool)))
}
Trait により各メソッドの要件が強制されているため、既存の処理との互換性を保ちながら安全に実装できました。
まとめ
こういうのって、分かってくると面白いですよね。 近いうちに他のデザインパターンも試してみようと思います。
以上、たかしでした。