rustでクリーンアーキテクチャ

# はじめに
[こちらに](https://qiita.com/shogomuranushi/items/d2d1b189b19d484bb003)触発されて、同じようなものをrustで作ってみました。
awsの利用料金をslackに通知するアプリケーションです。
https://github.com/pokotyan/aws_cost_rs

こんな感じでコマンドを実行すると、

```terminal
$ cargo run -- cost
```

今月分のaws利用料金が確認できます。
<img width="650" alt="スクリーンショット_2020-05-27_16_56_19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/338ab009-c5c4-2a1d-3d8f-7fbcfd4b967b.png">


# ディレクトリ構成
こんな感じのモノレポの構成になっています。
rustのパッケージマネージャであるcargoには[ワークスペース](https://doc.rust-jp.rs/book/second-edition/ch14-03-cargo-workspaces.html)という機能があり、モノレポの構成を簡単に作れます。

```
.
├── Cargo.lock
├── Cargo.toml
├── README.md
├── cli
│   ├── Cargo.toml
│   └── src
│       ├── get_cost
│       └── main.rs
├── infra
│   ├── Cargo.toml
│   └── src
│       ├── aws
│       └── lib.rs
├── module
│   ├── Cargo.toml
│   └── src
│       ├── lib.rs
│       └── slack_module.rs
├── presenter
│   ├── Cargo.toml
│   └── src
│       ├── lib.rs
│       └── slack_presenter.rs
└── server
    ├── Cargo.toml
    └── src
        ├── main.rs
        └── router.rs
```

それぞれのパッケージの役割は以下のような感じです。

- cli
コマンド実行のエントリーポイント

- infra
リポジトリ。awsのsdkを使って料金情報を取ってくる

- module
モジュール置き場。webhookを使ってslackへメッセージを送信するモジュールを置いてる。

- presenter
sdkから取得した料金情報をslack用の構造体に整形するコードを置いてる。

- server
使ってない。今後、APIでも料金情報を取得したいとなったら、ここにコードを書く。


# 依存関係
依存関係はこんな感じになっています。
エントリーポイントとなるcliのところで各クレートを利用しており、他のinfra, module, presenterはどこの層にも依存しない形になっています。

```toml:cli/Cargo.toml
[dependencies]
presenter = {path = "../presenter"}
infra = {path = "../infra"}
module = {path = "../module"}
structopt = "0.3.14"
tokio = { version = "0.2", features = ["full"] }
async-trait = "0.1.31"
```

```toml:infra/Cargo.toml
[dependencies]
tokio = { version = "0.2", features = ["full"] }
dotenv = "0.15.0"
rusoto_core = "0.43.0"
rusoto_credential = "0.43.0"
rusoto_ce = "0.43.0"
futures = "0.3.5"
anyhow = "1.0"
chrono = "0.4"
async-trait = "0.1.31"
serde = "1.0.110"
serde_json = "1.0.53"
```

```toml:module/Cargo.toml
[dependencies]
slack-hook = "0.7"
```

```toml:presenter/Cargo.toml
[dependencies]
anyhow = "1.0"
rusoto_ce = "0.43.0"
slack-hook = "0.7"
```

# DI
cliのところでinfra, module, presenterを利用していますが、infra(AwsRepository)とmodule(SlackModule)についてはDIをしています。

```rs:cli/src/get_cost/mod.rs
pub struct GetCost<T, U>
where
    T: AwsRepository,
    U: SlackModule,
{
    aws_repository: T,
    slack: U,
}

#[async_trait]
pub trait UseCase {
    async fn new() -> Self;
    async fn run(&self, start: Option<String>, end: Option<String>, channel: Option<String>);
}

#[async_trait]
impl<T: Sync + Send + AwsRepository, U: Sync + Send + SlackModule> UseCase for GetCost<T, U> {
    async fn new() -> Self {
        let aws_repository = AwsRepository::new().await;
        let slack = SlackModule::new();

        GetCost {
            aws_repository,
            slack,
        }
    }

    async fn run(&self, start: Option<String>, end: Option<String>, channel: Option<String>) {
        // slackへaws料金を送信するコード。。。
    }
}
```

GetCostという構造体は、
- awsの料金情報を取得するためのAwsRepository
- slackへ情報を送信するためのSlackModule
の二つをプロパティを持っています。どちらとも具象的な処理には依存しておらず、traitに依存するようになっています。
なので、traitさえ満たしていればAwsRepository、SlackModuleは別の実装に切り替えることができる状態です。

そのため、依存関係の向きとしてはCargo.tomlのdependencies的にはこの方向になってますが、

```
cli → infra
cli → module
```

DIを行ったことにより、こんな感じになり依存関係が逆転している感じになります。

```
(cli → trait) ← infra
(cli → trait) ← module
```

presenterについてはDIをしませんでした。slack用にデータを整形するためのコードとなるので、slackの仕様にどっぷり浸かったコードになってしまいます。そのためどう抽象化すればいいかよくわからず、traitは定義せずそのまま処理を呼ぶようにしました。

## 実装を入れ替えてみる
では、SlackModuleの実装を `slackへ料金情報を送信する` から `ターミナルに料金情報を標準出力する` に切り替えてみます。

まず、現在のslackのモジュールはこうなってます。

```rs
use slack_hook::{Attachment, PayloadBuilder, Slack};
use std::env;

pub struct Client {
    slack: Slack,
}

pub trait SlackModule {
    fn new() -> Self;
    fn send(&self, channel: String, user_name: String, text: String, attachments: Vec<Attachment>);
}

impl SlackModule for Client {
    fn new() -> Self {
        let webhook_url = env::var("SLACK_WEBHOOK_URL")
            .unwrap_or_else(|_| panic!("SLACK_WEBHOOK_URL is not found."));

        Client {
            slack: Slack::new(webhook_url.as_str()).unwrap(),
        }
    }

    fn send(&self, channel: String, user_name: String, text: String, attachments: Vec<Attachment>) {
        let p = PayloadBuilder::new()
            .channel(channel)
            .username(user_name)
            .text(text)
            .attachments(attachments)
            .build()
            .unwrap();

        let _ = self.slack.send(&p);
    }
}
```

traitはこれです。

```rs
pub trait SlackModule {
    fn new() -> Self;
    fn send(&self, channel: String, user_name: String, text: String, attachments: Vec<Attachment>);
}
```

なので、このtraitを満たすようにして `ターミナルに料金情報を標準出力する` 実装をつくります。

```rs
use crate::slack_module::SlackModule;
use slack_hook::Attachment;

pub struct MockClient {}

impl SlackModule for MockClient {
    fn new() -> Self {
        MockClient {}
    }

    fn send(&self, channel: String, user_name: String, text: String, attachments: Vec<Attachment>) {
        println!(
            "slackへメッセージを送信: channel: {}, user_name: {}, text: {}, attachments: {:?}",
            channel, user_name, text, attachments
        );
    }
}
```

別の実装ができたので、利用する側を切り替えます。

現在のslackへ送信する処理はこんな感じ（抜粋）になっており、通常のClientを利用するようになってます。

```rs

use module::slack::Client;

let get_cost: GetCost<AWS, Client> = UseCase::new().await;

get_cost.run(start, end, channel).await;
```

これをMockのClientを利用するように変更します。

```rs

use module::slack::MockClient;

let get_cost: GetCost<AWS, MockClient> = UseCase::new().await;

get_cost.run(start, end, channel).await;
```

実行してみます。
![スクリーンショット_2020-05-29_5_18_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/b3a52aa0-6024-164d-b38f-c89f3935e9bd.png)

標準出力に料金情報が表示され、実装が切り替わっていることが分かります。

# 最後に
rustは難しいですがやってて楽しいですね！