昨天在極限編程的群里聊代碼測試方面的,突然話鋒一轉聊到重構之類的話題,云測的一個哥們突然來了一句 “現在很多離開了框架都不會寫代碼了。” 我給回了一句“用 jdbctemplate 寫,領域服務 應用層不用注解,多寫幾遍就會了。”
然后,開始聊各種 ORM、JPA,我一直不喜歡 Hibernate 、 Mybatis 和早些年使用過的 Struts ,很不喜歡那些配置文件,配置的特別繁瑣、麻煩,更喜歡用 JdbcTemplate 簡單封裝進行編程。
今天接著昨天那句 “現在很多離開了框架都不會寫代碼了。” 聊聊設計模式中的 Proxy 代理模式。
Robert C. Martin 的《敏捷軟件開發 · 原則、模式與實踐》對個人的開發影響挺大,就像副標題一樣,原則、模式與實踐。通過原則與設計模式指導我們對軟件代碼的重構進行軟件的代碼異味清掃,從而使得軟件清晰可讀以及可擴展。
簡單案例
商戶上架一個商品,系統需要將該商品保存到數據庫中。
實現如下:
publicinterfaceProductDao{
voidsave(Productproduct);
}@Autowired
privateJdbcTemplatejdbcTemplate;
@Override
publicvoidsave(Productproduct){
//此處省略保存
}
@RestController
@RequestMApping("/proxy")
publicclassProductController{
@Autowired
privateProductDaoproductDao;
@PostMapping
voidsave(){
this.productDao.save(newProduct(1,2d));
}
}
相信大多數人都會按照上面的方式實現,這種實現站在功能的角度并沒有什么問題,假如我們不依賴于 Spring 的注解 @Autowired 該如何去實現上面的業務,同時在 ProductDaoImpl 中,我們依賴了 JdbcTemplate ,能否不依賴呢?
Proxy 模式
代理者是指一個類別可以作為其它東西的接口。代理者可以作任何東西的接口:網絡連接、存儲器中的大對象、文件或其它昂貴或無法復制的資源。
《敏捷軟件開發 · 原則、模式與實踐》書中舉了實際開發中一個很好的案例。這里我們仍使用上面簡單案例進行演示,為了方便看效果,我們增加一個需求是在添加商品的時候,通過名稱驗證商品是否存在,存在返回 false 不保存,反之則保存。
如圖,我們將 JdbcTemplate 交給了代理 PorductProxy,這也就是代理模式解決的問題。代理模式的工作原理,每個要被代理的對象分成 3 個部分。第一部分是一個接口,該接口中聲明了客戶要調用的所有方法。第二部分是一個類,該類在不涉及數據庫邏輯的情況下實現了接口中的方法。第三部分是一個知曉數據庫的代理,也就是 ProductPorxy。
示例代碼中,proxy 類會直接調用 JdbcTemplate ,采用SQL的方式進行存儲。讀者可能會問這樣不就讓 proxy 干了存儲,這里僅是為了演示。我們可將圖修改如下:
圖中的 DB 就是實際進行業務數據存儲數據的類。代碼實現如下:
Product 接口,工具中類名沖突改成 ProductI
publicinterfaceProductI{
Productsave(Productproduct);
ProductgetByName(StringproName);
實現 ProductI 接口
publicclassProductImplimplementsProductI{
privateProductproduct;
publicProductImpl(Productproduct){
this.product=product;
}
@Override
publicProductsave(Productproduct){
//實際中會進行處理product將處理好的數據返回
returnnewProduct(1,100d);
}
@Override
publicProductgetByName(StringproName){
returnthis.product;
}
publicbooleanisProduct(){
if(product==null){
returntrue;
}
returnfalse;
}
}
代理類
publicclassProductProxyimplementsProductI{
privateStringproName;
@Override
publicProductsave(Productproduct){
ProductImplproImpl=newProductImpl(getByName(this.proName));
try{
if(proImpl.isProduct()){
thrownewError("productnamerepeat");
}
//業務忽略,通過name去查庫驗證,可使用jdbcTemplate
//或直接調用已封裝保存業務的DB
}catch(Exceptione){
thrownewError("productnamerepeat");
}
//為測試方便,直接返回商品
returnnewProduct(2,40d);
}
@Override
publicProductgetByName(StringproName){
//業務忽略,通過name去查庫驗證,可使用jdbcTemplate
//或直接調用已封裝保存業務的DB
returnnewProduct(1,20d);
}
}
測試用例
@Test
publicvoidshould_product_proxy(){
ProductProxyproductProxy=newProductProxy();
assertThat(productProxy.getByName("蘋果"),is(newProduct(1,20d)));
assertThat(productProxy.save(productProxy.getByName("蘋果")),is(newProduct(2,40d)));
}
通過案例可以看到,ProductProxy 和 ProductI 的行為是一樣的,區別在于前者是從數據庫中取而不是內存中獲取它的數據。
另外,我們將與數據庫打交道的交給了 proxy 代理,將重要關系進行了分離,將業務規則和數據庫完全分離,ProductImpl 對數據庫沒有任何依賴,如果想要更改數據庫模式或數據庫引擎。我們可以在不影響ProductI、ProductImpl 以及任何其它業務規則的情況下進行更改。