Spring中如何使用工廠模式實現程式解耦?
目錄
- 1、 啥是耦合、解耦?
- 2、 jdbc程式進行解耦
- 3、傳統dao、service、controller的程式耦合性
- 4、使用工廠模式實現解耦
- 5、工廠模式改進
- 6、結語
@
1、 啥是耦合、解耦?
既然是程式解耦,那我們必須要先知道啥是耦合,耦合簡單來說就是程式的依賴關係,而依賴關係則主要包括
1、 類之間的依賴
2、 方法間的依賴
比如下面這段程式碼:
public class A{ public int i; } public class B{ public void put(A a){ System.out.println(a.i); } }
上面這個例子中A類和B類之間存在一種強耦合關係,B
類直接依賴A
類,B
類的put
方法非A
類型別不可,我們把這種情況叫做強耦合關係。
實際開發中應該做到:編譯期不依賴,執行時才依賴。怎麼理解呢?我們很容易想到多型向上轉型,是的,編譯時不確定,執行時才確定,當然接觸面更廣一點的童鞋會想到介面回撥,是的介面回撥方式也能有效的解耦!如下程式碼:
//一個介面叫做Inter,裡面定義了一個happy()方法,有兩個類A、B實現了這個介面 interface Inter{ void happy(); } class A implements Inter{ @Override public void happy() { System.out.println("happy...A"); } } class B implements Inter{ @Override public void happy() { System.out.println("happy...B"); } } public class Test{ public void happys(Inter inter){ inter.happy(); } }
是的,如上程式碼正是典型的介面回撥,Test
類中的happys
方法引數變的相對靈活起來,程式碼中Test類
與A類
、B類
之間就存在一種弱耦合關係,Test類
的happys
方法的引數可以使A類
型別也可以是B類
型別,不像強耦合關係中非A類
型別不可的情形。
從某一意義上來講使用類的向上轉型或介面回撥的方式進行解耦都是利用多型的思想!
當然解耦的方式還有很多,從根本意義上講實現低耦合就是對兩類之間進行解耦,解除類之間的直接關係,將直接關係轉換成間接關係,從而也有很多設計模式也對程式進行解耦,比如:介面卡模式、觀察者模式、工廠模式....總之,必須明確一點:耦合性強的程式獨立性很差!
2、 jdbc程式進行解耦
先來看一段程式碼:
//1、註冊驅動
DriverManager.registerDriver(new com.mysql.jdbc.Driver()); //如果把jdbc的MySQLjar包依賴去除直接編譯失敗提示沒有mysql
//2、獲取連線
Connection conn=DriverManager.getConnection("jdbc:mysql://localhost:3306/ufida","root","root");
//3、獲取操作資料庫的預處理物件
PreparedStatement pstm=conn.prepareStatement("select * from client");
//4、執行SQL,得到結果集
ResultSet rs=pstm.executeQuery();
//5\遍歷結果集
while(rs.next()){
System.out.println(rs.getString("name"));
}
//6、釋放資源
rs.close();
pstm.close();
conn.close();
等等等等,好熟悉好懷念的程式碼.....
沒錯就是jdbc的程式碼,不是用來懷舊的,而是如果這樣設計,你會覺得這樣的程式耦合性如何?又如何進行解耦?先仔細思考一番。
一分鐘過去了.....
兩分鐘過去了.....
好了,我們都知道jdbc連線MySQL需要一個mysql-connector
的jar包,如果我們把這個jar包依賴或者這個jar包給去掉,顯然上面的這個程式會編譯報錯,如下圖
顯然這樣的程式耦合性過高!於是我們可以這樣設計,將第一步的註冊驅動程式碼new的方式改成反射的方式如下:
//1、new的方式註冊驅動
DriverManager.registerDriver(new com.mysql.jdbc.Driver()); //如果把jdbc的MySQLjar包依賴去除直接編譯失敗提示沒有mysql相關的jar包
改為如下方式
//2、反射的方式註冊驅動
Class.forName("com.mysql.jdbc.Driver"); //改用這種方式註冊驅動會發現不會編譯失敗,相比上面的方式相對解耦,但是依然存在缺陷:若連線改為Oracle資料庫,這裡的字串又要進行改動!
正如註釋的解釋一樣,又一個缺陷就浮現了:若連線改為Oracle資料庫,這裡的字串又要進行改動!
於是對於這個jdbc程式來說就有這樣的一個解耦思路:
第一步:通過反射來建立物件,儘量避免使用new關鍵字
第二步:通過讀取配置檔案來獲取建立的物件全限定類名
3、傳統dao、service、controller的程式耦合性
順著jdbc程式的解耦思路,我們再來看看傳統dao、service、controller的程式耦合性分析
由於只是一個demo,省去dao層的操作.....
定義一個Service介面
public interface IAccountOldService{
public void save();
}
Service介面實現類
public class AccountServiceOldImpl implements IAccountOldService{
@Override
public void save() {
System.out.println("save成功一個賬戶....");
}
}
controller程式碼:
public class AccountCencollertOld {
public static void main(String[] args) {
IAccountOldService iaccount=new AccountServiceOldImpl ();
iaccount.save(); //執行結果:save成功一個賬戶....
}
}
到這裡,有何想法?表面上來看是沒有一點問題的,So Beautiful,但仔細的看。表現層與業務層、業務層與持久層緊緊的互相依賴關聯,這與我們開發程式的高內聚低耦合原則相違背,哦My God,So Bad!我們順著jdbc程式的解耦思路,我們應該儘量避免使用new關鍵字,我們發現這些層裡面service層new 持久層dao,controller表現層new 業務層service....太糟糕了
那麼對此,你有何解耦思路?
4、使用工廠模式實現解耦
別想了,工廠模式實現程式解耦你值得擁有!順著jdbc程式的解耦思路:
1、通過讀取配置檔案來獲取建立的物件全限定類名
2、通過反射來建立物件,儘量避免使用new關鍵字
首先在resources目錄下中寫一個bean.properties配置類,具體內容如下
accountServiceOld=com.factory.service.impl.AccountServiceOldImpl
接著使用工廠方法程式碼:
/**
* 一個建立Bean物件的工廠
*
* 1、需要一個配置檔案來配置我們的service和dao 配置檔案的內容:唯一標識=全限定類名(key-value)
* 2、通過讀取配置檔案中配置的內容,反射建立物件
*
* 場景:主要是service呼叫dao,controller呼叫service的程式。這裡面耦合性非常的高,互相new互相依賴
*
* 為了解耦,利用工廠模式進行
*/
public class BeanFactoryOld {
private static Properties props;
static{
try {
//例項化物件
props = new Properties();
//獲取properties檔案的流物件
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);//載入其對應路徑下的配置檔案
}catch (Exception e){
throw new ExceptionInInitializerError("初始化properties失敗!");
}
}
//根據bean的名稱獲取bean物件
public static Object getBean(String beanName){
Object bean=null;
try {
String beanPath= props.getProperty(beanName);
bean = Class.forName(beanPath).newInstance(); //這裡的newInstance建立例項(預設無參構造器)每次執行都需要建立一次
} catch (Exception e) {
e.printStackTrace();
}
return bean;
}
}
此時,controller的程式碼就可以編寫為
/**
* 這裡模擬一個controller呼叫service
*
*/
public class AccountCencollertOld {
public static void main(String[] args) {
// IAccountOldService iaccount=new AccountServiceOldImpl (); //使用工廠方法不再通過new方式
IAccountOldService iaccount= (IAccountOldService) BeanFactoryOld.getBean("accountServiceOld");
iaccount.save(); //執行結果:save成功一個賬戶.... 說明成功呼叫了service
}
}
通過執行結果,屬實沒毛病,成功降低程式耦合!So Beautiful!先高興一會吧,因為馬上出現.....但是,隨之而來的問題又出現了,我們對這個controller進行一下改寫
for(int i=0;i<5;i++){
IAccountOldService iaccount= (IAccountOldService) BeanFactoryOld.getBean("accountServiceOld");
iaccount.save();
}
列印結果:
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
com.factory.service.impl.AccountServiceImpl@677327b6
save成功一個賬戶....
com.factory.service.impl.AccountServiceImpl@14ae5a5
save成功一個賬戶....
com.factory.service.impl.AccountServiceImpl@7f31245a
save成功一個賬戶....
com.factory.service.impl.AccountServiceImpl@6d6f6e28
save成功一個賬戶....
列印的是五個不同的物件,說明是多例的,每次呼叫getBean的時候都會newInstance出一個新物件,如下
多例每次都要建立物件,資源浪費、效率低下
針對單例多例情況,我們再對service業務層程式碼進行修改:
public class AccountServiceImpl implements IAccountService {
//定義類成員
private int i=1;
@Override
public void save() {
System.out.println("save成功一個賬戶....");
System.out.println(i);
i++;
}
}
執行controller程式碼,執行結果
save成功一個賬戶....
1
save成功一個賬戶....
1
save成功一個賬戶....
1
save成功一個賬戶....
1
save成功一個賬戶....
1
why?多例因為每次都是新的物件,上面也驗證過了,因此每次建立新物件都會初始化一次,重新賦值,所以都是1,如果我們把類成員改為區域性成員變數如下
public class AccountServiceOldImpl implements IAccountOldService {
// private int i=1;
@Override
public void save() {
int i=1; //改為區域性變數
System.out.println("save成功一個賬戶....");
System.out.println(i);
i++;
}
}
不用猜,執行結果同樣是1。算了還是執行一下吧哈哈哈
save成功一個賬戶....
1
save成功一個賬戶....
1
save成功一個賬戶....
1
save成功一個賬戶....
1
save成功一個賬戶....
1
說了這麼多,通過觀察service和dao之間單不單例好像無所謂,因為他們之間並沒有業務方法中改變的類成員,所以並不需要多例來保證執行緒安全。那說這些有何意義?不要忘了,由於使用了工廠改進如下中的.newInstance
建立例項(預設無參構造器)每次執行都需要建立一次,這樣就不好了(浪費資源),因此我們要設計出只newInstance
建立一次例項就很完美了,這也是我為啥要在service
和controller
中都新增一個Old
關鍵字的原因了,接下來我們來看看工廠是如何改進的!
5、工廠模式改進
為了不被搞暈,我們重新寫程式碼,也就是重頭開始寫程式碼~其實就是把Old去掉~
由於只是一個demo,省去dao層的操作.....
定義一個Service介面
public interface IAccountService {
public void save();
}
Service介面實現類
public class AccountServiceImpl implements IAccountService{
@Override
public void save() {
System.out.println("save成功一個賬戶....");
}
}
controller程式碼:
/**
* 這裡模擬一個controller呼叫service
*
*/
public class AccountCencollert {
public static void main(String[] args) {
// IAccountService iaccount=new AccountServiceImpl();
IAccountService iaccount= (IAccountService) BeanFactory.getBean("accountService");
iaccount.save(); //執行結果:save成功一個賬戶.... 說明了成功呼叫了service
}
}
改進的工廠方法程式碼:
public class BeanFactory {
private static Properties props;
//定義一個map容器,用於存放建立的物件
private static Map<String,Object> beans; //改進的程式碼============
static{
try {
//例項化物件
props = new Properties();
//獲取properties檔案的流物件
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);//載入其對應路徑下的配置檔案
////////////////////以下是改進的程式碼=======================
//例項化容器
beans=new HashMap<String,Object>();
//取出配置檔案中所有的key值
Enumeration<Object> keys = props.keys();
//遍歷列舉
while(keys.hasMoreElements()){
//取出每個key
String key = keys.nextElement().toString();
//根據key取出對應的value (這裡因為每個value值對應著類路徑)
String beanPath = props.getProperty(key);
//反射建立物件
Object value = Class.forName(beanPath).newInstance();
//把key和value存入容器中
beans.put(key,value);
}
}catch (Exception e){
throw new ExceptionInInitializerError("初始化properties失敗!");
}
}
//隨著程式碼的改進,我們就可以簡化下面的獲取bean物件的方法,如下程式碼
/**
* 根據bean的名稱獲取物件(單例)
*/
public static Object getBean(String beanName){
//通過Map容器對應key來獲取對應物件
return beans.get(beanName); //這裡通過Map容器中獲取,這樣就不會每次都建立一次例項!
}
//不再使用下面的方法
/*
//根據bean的名稱獲取bean物件
public static Object getBean(String beanName){
Object bean=null;
try {
String beanPath= props.getProperty(beanName);
bean = Class.forName(beanPath).newInstance(); //這裡的newInstance建立例項(預設無參構造器)每次執行都需要建立一次,這樣就不好了
} catch (Exception e) {
e.printStackTrace();
}
return bean;
}*/
}
從上面改進的工廠程式碼,我們可以發現一開始就定義一個Map容器,用於存放建立的物件,為啥要先定義一個Map容器呢?用一個容器將這個例項裝起來,這是由於不把這個物件裝存起來的話,這個物件不使用很容易被GC掉,何況我們現在只使用這一個物件!
定義一個Map容器存放配置好的檔案中的每個物件,之後我們就直接提供一個根據Map的key來取value的getBean方法,這樣不僅僅擴充套件了程式的配置檔案的靈活性而且還保證了只產生一個物件,保證資源不浪費,So Beautiful !
那如何證明已經是單例的模式了呢?很簡單,如下設計一下service業務層、controller表現層程式碼即可:
service業務層:新增一個類成員屬性,並在方法內部 i++;
public class AccountServiceImpl implements IAccountService {
private int i=1; //類成員屬性
@Override
public void save() {
System.out.println("save成功一個賬戶....");
System.out.println(i);
i++;//二次改革程式碼
}
}
controller表現層: 建立呼叫工廠5次建立物件的方法
/**
* 這裡模擬一個controller呼叫service
*
*/
public class AccountCencollert {
public static void main(String[] args) {
for(int i=0;i<5;i++){
IAccountService iaccount= (IAccountService) BeanFactory.getBean("accountService");
System.out.println(iaccount); //列印的是五個不同的物件,說明是多例的
iaccount.save(); //會發現列印的i值都是1,並沒有自增成功
}
}
執行程式碼結果:
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
1
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
2
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
3
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
4
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
5
發現,確實5個物件都是同一個,並且出現了改變類成員屬性的現象。
如果我們把類成員屬性改為區域性成員屬性呢?
public class AccountServiceImpl implements IAccountService {
@Override
public void save() {
int i=1; //區域性成員屬性
System.out.println("save成功一個賬戶....");
System.out.println(i);
i++;
}
}
執行結果
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
1
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
1
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
1
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
1
com.factory.service.impl.AccountServiceImpl@1540e19d
save成功一個賬戶....
1
看到這個結果,我們就能聯想到,之前為什麼servlet中為啥要避免定義類成員,原因就在這裡!多例情況下,就不會出現這種情況!!!!
6、結語
到這裡我們已經基本瞭解了程式間的耦合與解耦,並且對單例多例也一併進行了進一步的瞭解。而spring中正是使用工廠模式來實現程式解耦的,spring是一個大工廠, 或許你並沒有察覺哈哈哈哈.....
如果本文對你有一點點幫助,那麼請點個讚唄,你的贊同是我最大的動力,謝謝~
最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回覆!
歡迎各位關注我的公眾號,裡面有一些java學習資料和一大波java電子書籍,比如說周志明老師的深入java虛擬機器、java程式設計思想、核心技術卷、大話設計模式、java併發程式設計實戰.....都是java的聖經,不說了快上Tomcat車,咋們走!最主要的是一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔...