読者のターゲット
- コードをひたすら大量に書く事から脱したい人
- テストが可能な個所を増やしたい人
- コードを書き始めてから少し経つ初心者
この3つだけ考えておくと良い
- データの形とそのデータに対する大規模な制御は分ける事
- 制御を書くときはその制御のインターフェイスも書くと、それに依存している制御のテストを書くときに、制御のモックを作りやすい
- 制御と外部通信用のインターフェイスは分ける事
これに「SOLID原則」という、更にソフトウェアをきれいに設計するための原則がありますが、僕が把握する限りではこの「SOLID原則」に従っているソフトウェアはかなり少ないため、上の3つだけに留め置く事にしました。
こうする事でどんな利点があるか
- 制御のインターフェイスを作ることによって、テストのためのモックが作りやすくなります
- コードが読みやすくなります
具体例
なおコードは何を重視すれば良いか伝わればよいため、かーなーり適当です。
モデル(データの形)
use crate::crypto::hash;
// データの形を表現する。(巷ではモデルとかエンティティとか言われている。)
pub struct Model {
pub id: UUID,
pub username: String,
pub pw_hash: String,
}
impl Model {
// パスワードをチェックする。
// 依存する実装は自分自身以外ないので、このようなメソッドとして定義しても良い。
pub fn verify(&self, pw: &str) -> bool {
return self.pw_hash == hash(pw);
}
}
制御
use ::std::sync::Arc;
use ::mockall::automock;
use ::uuid::Uuid;
use super::model::Model;
use crate::database::IDatabase;
// DBからModelをナニするための制御インターフェイス
// これで、外部のインターフェイスと「関心事を分離」する
// 外部からの制御はこのインターフェイスを通じて受け付ける
#[automock]
pub trait IRepo {
fn get(id: &Uuid) -> Model;
}
// 制御の本実装
pub struct Repository {
db: Arc<dyn IDatabase + Send + Sync>,
}
impl Repository {
pub fn new(&self, db: Arc<dyn IDatabase + Send + Sync>) -> Self {
return Self { db: db.clone() };
}
}
// IRepo インターフェイスを実装する
impl IRepo for Repository {
fn get_by_id(id: &Uuid) -> Model {
return self.id.get(id);
}
}
#[cfg(test)]
mod test {
use ::std::sync::Arc;
use crate::database::MockIDatabase;
use super::*
#[test]
fn test_get_by_id() {
let db = MockIDatabase::new();
let repo = Repository::new(Arc::new(db));
// ...何かのテスト
}
}
データベース(外部通信用の制御)
use ::uuid::Uuid;
use ::mockall::automock;
#[automock]
pub trait IDatabase {
fn get<T>(id: &Uuid) -> T;
}
#[derive(Clone)]
pub struct Database {
db: Database
}
impl Database {
pub fn new(db: &Database) -> Self {
return Self { db: db.clone() }
}
}
impl IDatabase for Database {
fn get<T>(id: &Uuid) -> T {
//DBのクエリを書く・・・
}
}
こうする事で、外部からRepository
のメソッドを呼び出すときに、そのインターフェイスIRepository
を通じてメソッドを呼び出すことができます。言い換えれば、テストを書くときに IRepository
を実装したモックを噛ませることができる。という事は、俗世でいう事の「関心事の分離」ができるという事になります。 はいここ、嘘です。 関心事を分離するには、少なくともSOLID原則における「単一責務原則」と「インターフェイス分離原則」の両方を満たしていなければなりません。
use ::std::sync::Arc;
use ::mockall::automock;
use ::futures::StreamExt;
use crate::user::IRepository as IUserRepo;
use crate::transaction::{IRepository as ITransactionRepo, Model as Transaction};
#[automock]
pub trait IUserTransactionService {
fn get_transaction(&self, uid: &Uuid) -> Transaction
}
pub struct UserTransactionService {
user_repo: Arc<dyn IUserRepo + Send + Sync>,
tr_repo: Arc<dyn ITransactionRepo + Send + Sync>,
}
impl UserTransactionService {
pub fn new(
user_repo: Arc<dyn IUserRepo + Send + Sync>,
tr_repo: Arc<dyn ITransactionRepo + Send + Sync>
) -> Self {
return Self { user_repo, tr_repo };
}
}
impl IUserTransactionService for UserTransactionService {
fn get_transaction(&self, uid: &Uuid) -> Vec<Transaction> {
let user = self.user_repo.get(uid);
let transactions = self.tr_repo.by_user(user);
return transactions.collect();
}
}
#[cfg(test)]
mod test {
// 以下の2つのモックはmockall::automockによって自動生成されたもの。
use crate::user::MockIRepository as MockIUserRepo;
use crate::transaction::MockIRepository as MockITrRepo;
use super::TransactionService;
fn test() {
let user_repo_mock = Arc::new(MockIUserRepo::new());
let tr_repo_mock = Arc::new(MockITrRepo::new());
// インターフェイスによって、「関心事の分離」が行われているため、モックを噛ませることができる。
let svc = TransactionService::new(user_repo_mock, tr_repo_mock);
// あとはテストするだけ...
}
}
まとめ - このクソ記事を書いた背景
僕は本業の正社員プログラマと、副業として業務委託のプログラマをしています。今回、この記事を書くきっかけとなったのは、後者の業務委託のプログラマとして、委託者候補の企業さんとSES業界では常態化してしまっているアレをしていた最中に、
「もっと、こう、テクニック的な・・・TDDとか」
等という話題が上がりまして、そういえば、僕は20年近くソフトウェア畑で肥溜めになっているのにソフトウェアの設計手法的な事を記事として書いてないな・・・という事で書きました。
ちなみにその企業の案件は僕にトーク力ないしはカリスマ性がないのでお流れになりました。
あと、ここまで読んで下さったら是非ともLGBT?だっけ?LGTMだっけ?をして頂けると嬉しいです。