🐈

Rustでクリーンアーキテクチャによる依存関係逆転の原則について

2021/12/31に公開

まず始めに

Rustでクリーンアーキテクチャの設計し、依存関係逆転の法則で長く躓いてしまったのを解決できたので書き残しておきたいと思います。
そして先にお伝えしておきたいのが、DB接続は何のフレームワークを使うか、どのDBを使用するかはまだ未実装となります。そしてdomain層もまだ使用はしていません。

クリーンアーキテクチャの概念

まず有名なこの図から

四つの層からそれぞれ、

  • infrastructure
    Frameworks & Drivers:フレームワークやデータベース、外部システムとのやりとりを行う。
  • interfaces
    Interface Adapters:ユーザーに情報を表示、usecaseとの接続する。
  • usecase
    Application Business Rules:もっとも重要で、ビジネスロジックを記述する。プロダクトに合った仕様にデータを変換する。
  • domain
    Enterprise Business Rules:どこにも依存せず、オブジェクトが所属するレイヤー。
    の四つの層から成り立って、それぞれ矢印の方向、内側の円に対して依存し、お互いが実装の内容は知らず独立している関係にあります。
    僕個人としては、
  • お互いが実装の内容を知らない
  • 依存関係
    の二点がコードを書く中で大事なのかなと思っています。

依存関係逆転の原則

僕が一番理解できなかったのがこの部分です。(他にもわからない事は多いですが。)
先述したように依存関係は
infrastructure -> interface -> usecase -> domain
の方向にしてね。との話になっていますが、このまま処理をするのはなかなか難しいと思います。そこで図の右下の図が依存関係逆転の原則を表しています。

ピンク字のFlow of controlが実際の処理の流れです。矢印は依存の向きを表しています。
<I>との表示がありますがこれはinterface(この記事のinterface層ではありません)を表しています。
つまり、依存関係逆転の原則を使用するにはinterface機能を活用するということになります。

どうRustで実装したか

/src内部になります。
これから実装内容を説明していきます。

ファイル構成

.
├── domain
│     ├── mod.rs
│     └── users.rs
├── infrastructure
│     ├── config.rs
│     ├── mod.rs
│     └── routing.rs
├── interfaces
│     ├── controllers
│     │      ├── mod.rs
│     │      └── product
│     │       ├── mod.rs
│     │      └── users_controller.rs
│     ├── gateways
│     │     ├── database
│     │     │     ├── mod.rs
│     │     │     └── user_repository.rs
│     │     └── mod.rs
│     └── mod.rs
├── main.rs
└── usecase
    ├── mod.rs
    ├── product
    │     ├── mod.rs
     │    └── user_interactor.rs
    └── user_interface.rs

コマンドcargo runでmain()が起こされます。

main.rs
#![feature(proc_macro_hygiene)]
#![feature(decl_macro)]

#[macro_use] extern crate rocket;

mod domain;
mod infrastructure;
mod interfaces;
mod usecase;

fn main() {
    let cfg = infrastructure::config::AppConfig::new_config();

    let routing = infrastructure::routing::Routing::new_routing(&cfg);
    routing.run(routing.port);
}

new_config()でConfigインスタンスが作成され、設定値がConfigインスタンスに入ります。

config.rs
pub struct AppConfig {
	pub enviroment: String,
	pub routing: Routing,
}

pub struct Routing {
	pub port: u16,
}

impl AppConfig {
	pub fn new_config() -> AppConfig {
		AppConfig {
			enviroment: String::from("development"),
			routing: Routing {
				port: 8080
			},
		}
	}
}

次にConfigインスタンスを引数にnew_routing(&cfg)が起こされサーバーを立ち上げます。

routing.rs
use rocket;

use super::config::AppConfig;
use crate::interfaces::controllers::product;
use rocket::config::{ Config, Environment };

#[derive(Debug)]
pub struct Routing {
	pub port: u16,
}

impl Routing {
	pub fn new_routing(cfg: &AppConfig) -> Routing {
		Routing {
			port: cfg.routing.port.to_owned(),
		}
	}

	pub fn run(&self, port: u16) {
		let config = Config::build(Environment::Staging)
			.port(port)
			.unwrap();

		let routing =  rocket::custom(config);

		routing.mount("/api",
		 routes![
				index,
				get_users,
				])
			.launch();
	}
}

#[get("/hello")]
fn index() -> &'static str {
    "Hello, world!"
}

#[get("/users")]
pub fn get_users() -> &'static str {
	let users_controller = product::users_controller::new_users_controller();
	users_controller.get_user()
}

少し補足なのですが僕が使ったRocketというフレームワークにはもともとConfigインスタンスが存在しているようで、port(8000)などデフォルト値が指定されています。
Rocket Config page url -> https://api.rocket.rs/v0.4/rocket/struct.Config.html

let config = Config::build(Environment::Staging)
			.port(port)
			.unwrap();

この部分で、portをデフォルトの8000 -> 8080に変更しています。
今回ルーティングは二つ用意して/usersを叩けば文字列を返すという処理としています。
そして/usersにアクセスしたらnew_controller()が起こされインスタンスが作成されます。
この部分が一番苦労しました。

users_controller.rs

use std::sync::Arc;

use crate::interfaces::gateways::database;
use crate::usecase::product::user_interactor::UserInteractor;

pub struct UsersController {
	pub interactor: UserInteractor,
}

pub fn new_users_controller() -> UsersController {
	UsersController {
		interactor: UserInteractor{
			user: Arc::new(database::user_repository::UserRepository::new()),
		},
	}
}

impl UsersController {

	// #[get("/users")]
	pub fn get_user(&self) -> &'static str {
		let w = "Hello Rust! Very hard";
		// let res = interactor::user_interactor::get(w);
		self.interactor.get_users(w)
	}
}

new_controller()が呼ばれたら、UserControllerインスタンスを返します。

interactor: UserInteractor{
			user: Arc::new(database::user_repository::UserRepository::new()),
		},

この部分でArc::new()することでサイズの決まらずコンパイルエラーが出ていたものがなくなりました。
下のuser_interactor.rsではトレイトオブジェクトとしてUserInterfaceを呼び出しているのでArc::new()をしておかないと
the size for values of type (dyn UserInterface + 'static)cannot be known at compilation time withinUsersController, the trait std::marker::Sizedis not implemented for(dyn UserInterface + 'static) the return type of a function must have a statically known size
とエラーが表示されていました。

user_interactor.rs
use std::sync::Arc;

use crate::usecase::user_interface::UserInterface;

pub struct UserInteractor {
	pub user: Arc<dyn UserInterface + Sync + Send>,
}

// #[async_trait]
impl UserInteractor {

	pub fn get_users(&self, w: &'static str) -> &'static str {
		self.user.find_by_id(w)
	}
}

user_interface.rsではUserInterfaceインスタンスをトレイト実装しました。
traitは他言語のinterfaceの機能だということになり、ここで依存関係の向きが
user_interactor.rs -> user_interface.rsとなるのがわかるかと思います。

user_interface.rs
pub trait UserInterface {
	fn find_by_id(&self, w: &'static str) -> &'static str;
}

しかしこのinterface.rsでは実装内容は記述するのではなくて、あくまで抽象として捉えてなければいけません。
users_controller.rsで呼ばれていたuser_repository.rsの実装内容がこちらになります。

user_repository.rs
use crate::usecase::user_interface::UserInterface;

pub struct UserRepository {}

impl UserRepository {
	pub fn new() -> Self {
		Self { /* fields */ }
	}
}

impl UserInterface for UserRepository {
	fn find_by_id(&self,
		w: &'static str) -> &'static str {
		let wg = w;
		wg
	}
}

impl UserInterface for UserRepositoryこの部分で、
user_interface.rs <- user_repository.rsとの依存関係ができました。

この上記の内容で、
user_interactor.rs -> user_interface.rs <- user_repository.rs
つまり
usecase層 -> interface <- interface層
となり、依存関係逆転の原則が出来上がりました。

以上が今回実装することができた内容になります。
まだ始めたばかりですがこれから理解を深めていくとともに、来年もよろしくお願いいたします。

Discussion