邪惡的字段注入
繼《給 Java gradle 工程添加 git hooks》之后,再次延續(xù)《后端工程圣殿形象的崩塌以及重建》一文,用 JavaScript 工程師的視角,來(lái)改造理論上本該高大上但是現(xiàn)實(shí)中卻大面積倒塌的 java 后端工程。
今天面試了一個(gè)號(hào)稱(chēng)有著 8 年 java 開(kāi)發(fā)經(jīng)驗(yàn)的工程師(據(jù)說(shuō)所謂的 java 工程師,事實(shí)上就是 Spring 工程師而已)。我問(wèn)她這兩種寫(xiě)法有什么區(qū)別?你更傾向哪一種?
class ServiceX {...}// 寫(xiě)法 1class A1 {private ServiceX service;...}// 寫(xiě)法 2class A2 {private final ServiceX service;public A2 ( ServiceX service) {this.service = service;}...}
她看了大驚失色:啊?還有 A2 這種寫(xiě)法?沒(méi)見(jiàn)過(guò)啊,都是像第一種那樣去寫(xiě)啊!
我不驚訝她的回答,因?yàn)槲覀児镜膶?shí)際項(xiàng)目中也是這樣的,所有人都是像第一種那樣去寫(xiě)的,從來(lái)沒(méi)有人覺(jué)得任何不適。
但是我很不適,首先 IDE 會(huì)給第一種寫(xiě)法畫(huà)上波浪線(xiàn),給一個(gè)黃色警告。我不明白那些看不起 JavaScript 工程師的 java 工程師們,為什么從來(lái)不去注意這種警告?
其次是我在寫(xiě)測(cè)試時(shí),覺(jué)得更加不爽。原來(lái)沒(méi)有人覺(jué)得不適,因?yàn)轫?xiàng)目中根本沒(méi)有測(cè)試。通過(guò)寫(xiě)測(cè)試,我切身體會(huì)到了兩種寫(xiě)法的區(qū)別和各自的優(yōu)劣,結(jié)論是強(qiáng)烈建議使用第二種寫(xiě)法,應(yīng)該沒(méi)有不得不使用第一種寫(xiě)法的場(chǎng)景。
以下是個(gè)人粗淺的理解的總結(jié)
以上的代碼就是要寫(xiě)一個(gè)類(lèi),該類(lèi)依賴(lài)一個(gè) Service,使用 Spring 框架實(shí)現(xiàn)這個(gè)依賴(lài) Service 的類(lèi),當(dāng)然要使用依賴(lài)注入,這個(gè) @Autowired 注解就是用來(lái)注入依賴(lài)的。但是注入的方式有兩種,以上第一種寫(xiě)法是字段注入,而第二種寫(xiě)法是構(gòu)造器注入。
為什么不建議采用字段注入?
一、測(cè)試不僅難寫(xiě),而且難以運(yùn)行
測(cè)試時(shí)需要控制依賴(lài)項(xiàng),所以往往需要將真正的依賴(lài)項(xiàng)模擬掉。如果使用字段注入,測(cè)試就很難寫(xiě)。因?yàn)橐M這個(gè)依賴(lài)項(xiàng),就要寫(xiě)更多的代碼。測(cè)試也很難運(yùn)行,因?yàn)橐ǜL(zhǎng)時(shí)間運(yùn)行,更耗機(jī)器資源,還讓反饋?zhàn)兟?/span>
比如對(duì)于字段注入的代碼進(jìn)行測(cè)試,你需要先在測(cè)試類(lèi)上注解上 Spring 相關(guān)的環(huán)境,而且在運(yùn)行測(cè)試時(shí)會(huì)真的啟動(dòng) Spring 容器,所以運(yùn)行起來(lái)很慢:
@RunWith(SpringRunner.class)@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class A1Test {@MockBeanprivate Service mockService;@Autowiredprivate A1 sut;@Testvoid testIt() {...}}
事實(shí)上,除非是寫(xiě)集成測(cè)試,根本沒(méi)有必要啟動(dòng) Spring 環(huán)境。所以對(duì)于構(gòu)造器注入,測(cè)試代碼不僅更加簡(jiǎn)潔且運(yùn)行更快:
class A2Test {private final A2 sut;private final Service mockService;{mockService = Mockito.mock(Service.class);// 直接 new,避開(kāi)了 Spring 容器的開(kāi)銷(xiāo)sut = new A2(mockService);}void testIt() {when(mockService.method(any(Object.class)).thenReturn(0);...}}
二、字段注入方式違反了不可變性原則
對(duì)比 A1 的實(shí)現(xiàn),注意在 A2 的實(shí)現(xiàn)中(構(gòu)造器注入),使用了 final 關(guān)鍵字。這帶來(lái)了很大的好處,因?yàn)檫@個(gè)字段內(nèi)容在應(yīng)用的整個(gè)生命周期中不能再被改變,從而可以避免編程錯(cuò)誤(比如忘掉初始化這個(gè)字段會(huì)導(dǎo)致編譯報(bào)錯(cuò))。
三、字段注入實(shí)現(xiàn)的代碼不夠安全
當(dāng)構(gòu)造器執(zhí)行完畢,對(duì)象就準(zhǔn)備好被使用了。采用構(gòu)造器注入方式,對(duì)象只有準(zhǔn)備好或者沒(méi)有準(zhǔn)備好的狀態(tài),不存在中間態(tài)。但是采用字段注入,導(dǎo)致對(duì)象存在一個(gè)中間狀態(tài),這個(gè)對(duì)象會(huì)比較脆弱。
四、字段注入方式對(duì)依賴(lài)的表述不夠清晰
采用構(gòu)造器注入,使得類(lèi)的必要依賴(lài)一目了然。
五、逼迫開(kāi)發(fā)者思考類(lèi)的設(shè)計(jì)
如果你的構(gòu)造器里出現(xiàn)了很多參數(shù),就是非常明顯的一個(gè)壞的設(shè)計(jì),實(shí)際上是一種上帝對(duì)象這樣的反模式。不管類(lèi)通過(guò)構(gòu)造器還是字段的方式依賴(lài)多個(gè)其他服務(wù),這都是錯(cuò)的,但是通過(guò)構(gòu)造器注入更能讓人在依賴(lài)變多時(shí)停下來(lái)思考代碼結(jié)構(gòu)的設(shè)計(jì)。
總結(jié)
綜上所述,如果非要說(shuō)構(gòu)造器注入有什么不好的地方,那就是增加了實(shí)現(xiàn)上代碼量,因?yàn)樽侄巫⑷胫恍枰獙?xiě)一個(gè)字段,而構(gòu)造器注入既要寫(xiě)構(gòu)造器,還免不了要寫(xiě)字段。但作為 JavaScript 工程師,不得不說(shuō)這是 java 語(yǔ)言本身的問(wèn)題,在 JavaScript 或者 TypeScript 的世界里,采用構(gòu)造器注入,連這個(gè)缺點(diǎn)都沒(méi)有。
舉個(gè)例子
如果你使用 NestJs(https://docs.nestjs.com/fundamentals/injection-scopes),那么可以這樣寫(xiě):
class Service {}class A2 {constructor( private service) {}}
注意到在構(gòu)造器里可以直接寫(xiě)上 private,這樣 A2 類(lèi)就自動(dòng)有了 service 這個(gè)私有字段。
如果你不用 NestJs,那么使用 InversifyJs(https://doc.inversify.cloud/zh_cn/classes_as_id.html)也類(lèi)似:
class Service {...}class A2 {constructor(private readonly service: Service) {}}
最后再次強(qiáng)調(diào),如果你發(fā)現(xiàn)自己的項(xiàng)目中還在用字段注入,趕緊改成構(gòu)造器注入的方式吧!
