Péntek este van. A mai fejlesztés kicsit nagyobbra nőtt, mint eredetileg terveztem. Menet közben találtam valamit, aminek majd egyszer örülni fogok, de ma még csak a gép előtt töltött órákat növelte.
Builderek! Builderek mindenhol! Link to heading
Nézzük hát, mit hoz a jövőben ez a sok száz sor kód, amit ma fejlesztettem bele a korábbi modulokba.
Adott a TenantsModule
stuktúra és annak a jelenleg még kicsit körülményes inicializációja
1pub struct TenantsModule {
2 pub pool_manager: Arc<dyn PgPoolManagerTrait>,
3 pub config: Arc<AppConfig>,
4 pub repo_factory: Box<dyn Fn() -> Box<dyn TenantsRepository + Send + Sync> + Send + Sync>,
5 pub migrator_factory: Box<dyn Fn() -> Box<dyn DatabaseMigrator + Send + Sync> + Send + Sync>,
6 pub connection_tester_factory:
7 Box<dyn Fn() -> Box<dyn ConnectionTester + Send + Sync> + Send + Sync>,
8}
9
10#[test]
11fn some_important_test() {
12 // ...
13 let mut pool_manager_mock = Arc::new(MockPgPoolManagerTrait::new());
14 // some long pool_manager_mock init...
15
16 let mut migrator_factory = MockDatabaseMigrator::new();
17 // some long migrator_factory init...
18
19 let mut connection_tester_factory = MockConnectionTester::new();
20 // some long connection_tester_factory init...
21
22 let config = Arc::new(AppConfigBuilder::default().build().unwrap());
23 // let's say I want to use the default test config
24
25 let repo_factory = Box::new(|| {
26 let mut repo = MockTenantsRepository::new();
27 repo.expect_setup_self_hosted()
28 .times(1)
29 .withf(|name, _, _| name == "test")
30 .returning(|_, _, _| {
31 Ok(Tenant {
32 id: Uuid::new_v4(),
33 name: "test".to_string(),
34 db_host: "example.com".to_string(),
35 db_port: 5432,
36 db_name: "tenant_1234567890".to_string(),
37 db_user: "tenant_1234567890".to_string(),
38 db_password: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".to_string(),
39 db_max_pool_size: 5,
40 db_ssl_mode: "verify-full".to_string(),
41 created_at: Local::now(),
42 updated_at: Local::now(),
43 deleted_at: None,
44 })
45 });
46 Box::new(repo) as Box<dyn TenantsRepository + Send + Sync>
47 });
48 let tenants_module = TenantsModule {
49 pool_manager: pool_manager_mock.clone(),
50 config: config.clone(),
51 repo_factory,
52 migrator_factory,
53 };
54 // ...
55}
A probléma ezzel az, hogy abban a pillanatban, amikor létre hozom ezt a struktúrát az összes értékkel rendelkeznem kell, amit bele szeretnék tenni, de ez a megkötés gyakran elég kényelmetlen helyzeteket teremt
Erre a legegyszerűbb megoldás, amit sok más nyelvben is használnak: A builder pattern. Ennek lényege,
hogy a Builder struktúrában engedem, hogy anélkül létezzenek az értékek, hogy teljesen inicializálva lenne az egész. Ha
minden érték a helyén, akkor a végén egy build függvénnyel létre tudom hozni a TenantsModule
stuktúrát.
Hogy néz ki ez a builder a gyakorlatban?
1pub struct TenantsModuleBuilder {
2 pub pool_manager: Option<Arc<dyn PgPoolManagerTrait>>, // Option!
3 pub config: Option<Arc<AppConfig>>, // Option!
4 pub repo_factory:
5 Option<Box<dyn Fn() -> Box<dyn TenantsRepository + Send + Sync> + Send + Sync>>, // Option!
6 pub migrator_factory:
7 Option<Box<dyn Fn() -> Box<dyn DatabaseMigrator + Send + Sync> + Send + Sync>>, // Option!
8 pub connection_tester_factory:
9 Option<Box<dyn Fn() -> Box<dyn ConnectionTester + Send + Sync> + Send + Sync>>, // Option!
10}
11
12impl TenantsModuleBuilder {
13 pub fn new() -> Self {
14 Self {
15 pool_manager: None,
16 config: None,
17 repo_factory: None,
18 migrator_factory: None,
19 connection_tester_factory: None,
20 }
21 }
22 pub fn pool_manager(mut self, pool_manager: Arc<dyn PgPoolManagerTrait>) -> Self {
23 self.pool_manager = Some(pool_manager);
24 self
25 }
26 pub fn config(mut self, config: Arc<AppConfig>) -> Self {
27 self.config = Some(config);
28 self
29 }
30 pub fn repo_factory(
31 mut self,
32 repo_factory: Box<dyn Fn() -> Box<dyn TenantsRepository + Send + Sync> + Send + Sync>,
33 ) -> Self {
34 self.repo_factory = Some(repo_factory);
35 self
36 }
37 pub fn migrator_factory(
38 mut self,
39 migrator_factory: Box<dyn Fn() -> Box<dyn DatabaseMigrator + Send + Sync> + Send + Sync>,
40 ) -> Self {
41 self.migrator_factory = Some(migrator_factory);
42 self
43 }
44 pub fn connection_tester_factory(
45 mut self,
46 connection_tester_factory: Box<
47 dyn Fn() -> Box<dyn ConnectionTester + Send + Sync> + Send + Sync,
48 >,
49 ) -> Self {
50 self.connection_tester_factory = Some(connection_tester_factory);
51 self
52 }
53
54 pub fn build(self) -> Result<TenantsModule, String> {
55 Ok(TenantsModule {
56 pool_manager: self
57 .pool_manager
58 .ok_or("pool_manager is required".to_string())?,
59 config: self.config.ok_or("config is required".to_string())?,
60 repo_factory: self
61 .repo_factory
62 .ok_or("repo_factory is required".to_string())?,
63 migrator_factory: self
64 .migrator_factory
65 .ok_or("migrator_factory is required".to_string())?,
66 connection_tester_factory: self
67 .connection_tester_factory
68 .ok_or("connection_tester is required".to_string())?,
69 })
70 }
71}
És mire jó ez? Link to heading
Tegyük fel, hogy sok tesztet kell megírnom, ahol a pool_manager
, a config
, a migrator_factory
és a
connection_tester_factory
mindig ugyan az, de repo_factory
-t tesztről tesztre változtatnom kell. Egy builderrel
sokkal rugalmasabban tudom ezt megtenni olyan struktúrák esetében is, amiken privát mezők vannak. (A TenantsModule
nem
ilyen egyelőre)
A végeredmény builderrel valami ilyesmi lesz:
1#[cfg(test)]
2pub(crate) mod tests {
3 use super::*;
4 use crate::app::config::AppConfigBuilder;
5
6 use crate::app::database::{
7 MockConnectionTester, MockDatabaseMigrator, MockPgPoolManagerTrait,
8 };
9 use crate::tenants::repository::MockTenantsRepository;
10
11 fn default_tenants_module_for_self_hosted_tests() -> TenantsModuleBuilder {
12 let mut pool_manager_mock = Arc::new(MockPgPoolManagerTrait::new());
13 // some long pool_manager_mock init...
14
15 let mut migrator_factory = MockDatabaseMigrator::new();
16 // some long migrator_factory init...
17
18 let mut connection_tester_factory = MockConnectionTester::new();
19 // some long connection_tester_factory init...
20
21 let config = Arc::new(AppConfigBuilder::default().build().unwrap());
22 // let's say I want to use the default test config
23
24 // notice: I expect the caller to finish the repo_factory
25 TenantsModuleBuilder::default()
26 .pool_manager(pool_manager_mock.clone())
27 .config(config)
28 .migrator_factory(migrator_factory)
29 .connection_tester_factory(connection_tester_factory)
30 }
31
32 #[test]
33 fn some_important_test1() {
34 let repo_factory = Box::new(|| {
35 let mut repo = MockTenantsRepository::new();
36 repo.expect_setup_self_hosted()
37 .times(1)
38 .withf(|name, _, _| name == "test")
39 .returning(|_, _, _| {
40 Ok(Tenant {
41 id: Uuid::new_v4(),
42 name: "test".to_string(),
43 db_host: "example.com".to_string(),
44 db_port: 5432,
45 db_name: "tenant_1234567890".to_string(),
46 db_user: "tenant_1234567890".to_string(),
47 db_password: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".to_string(),
48 db_max_pool_size: 5,
49 db_ssl_mode: "verify-full".to_string(),
50 created_at: Local::now(),
51 updated_at: Local::now(),
52 deleted_at: None,
53 })
54 });
55 Box::new(repo) as Box<dyn TenantsRepository + Send + Sync>
56 });
57
58 let tenants_module = default_tenants_module_for_self_hosted_tests()
59 .repo_factory(repo_factory)
60 .build()
61 .unwrap();
62 }
63
64 #[test]
65 fn some_important_test1() {
66 let repo_factory = Box::new(|| {
67 let mut repo = MockTenantsRepository::new();
68 repo.expect_setup_managed()
69 .times(0)
70 .withf(|_, name, _, _, _| name == "test")
71 .returning(|uuid: Uuid, _, _, _, _| {
72 Ok(Tenant {
73 id: uuid,
74 name: "test".to_string(),
75 db_host: "localhost".to_string(),
76 db_port: 5432,
77 db_name: "database".to_string(),
78 db_user: "user".to_string(),
79 db_password: "password".to_string(),
80 db_max_pool_size: 5,
81 db_ssl_mode: "disable".to_string(),
82 created_at: Local::now(),
83 updated_at: Local::now(),
84 deleted_at: None,
85 })
86 });
87 Box::new(repo) as Box<dyn TenantsRepository + Send + Sync>
88 });
89
90 let tenants_module = default_tenants_module_for_self_hosted_tests()
91 .repo_factory(repo_factory)
92 .build()
93 .unwrap();
94 }
95}
Ezzel a megoldással el tudom kerülni, hogy feleslegesen újra inicializáljam a tenants_module azon részeit, amiknek minden tesztben egyezniük kell. Ha javítanom kell az alapértelmezett részeken valamit, akkor csak egy helyen kell megtennem és utána az összes teszben javítva lesz.
Ezt a bejegyzés tegnap terveztem közzétenni, csak késő volt már. :)
Ha találsz hibát, írj egy e-mail!