我們都知道面向接口編程的主要優點有: 提高編程的靈活性和模塊解耦,降低維護成本。
但是,對於平時我們使用的單例來說,我們是無法做到上面的事情的。如果,我們想讓單例對象也可以面向對象編程的話。
我們就需要在其他的地方來保證對象的唯一性(單例),不能通過傳統的方式,在對象內部保證對象的唯一性。
通過下面的目錄,我們來實現一個可擴展的單例。
普通單例
使用以前的方法,我們創建一個單例。比如,我們創建一個UserManager單例。
public class UserManager {
private static final UserManager manager = new UserManager();
public static UserManager getInstance() {
return manager;
}
private UserManager() {
}
}
優點:
- 內存中只有一個對象,節約系統資源。
- 對唯一實例的訪問控制。
缺點:
- 單例沒有抽象層,基本沒有擴展能力。
- 生命週期長,容易引發內存泄漏。
- 每個調用者獲取到單例對象,對可以對其內部進行修改。
普通的單例,不管是餓漢式還是懶漢式,我們都是通過在對象內部的方式,私有構造器,來保證唯一性的,這樣也就不能滿足我們的擴展性,並且每個調用者,都可以修改對象內部的邏輯。
設計新單例
我們設計單例的目的,就是爲了解決上面的問題:
- 沒有擴展能力
- 控制調用者對單例對象的功能限制。
1,我們先創建一個反射工具類
這個類主要是通過反射的方法來創建實例對象
public class ProtoFactory {
/**
* 不帶參數的構造方法
*/
public static <T> T newInstance(Class<T> clazz) {
try {
return clazz.newInstance();
} catch (IllegalAccessException e) {
//這裏是自定義的異常處理
throw new ServiceException(ServiceExceptionCode.instantiationException,
"create " + clazz.getName() + " failed.");
} catch (InstantiationException e) {
throw new ServiceException(ServiceExceptionCode.instantiationException,
"create " + clazz.getName() + " failed.");
}
}
/**
* 帶參數的構造方法
*/
public static <T> T newInstance(Class<T> clazz, Class<?>[] paramTypes, Object[] paramValues) {
try {
Constructor<T> constructor = clazz.getConstructor(paramTypes);
T result = constructor.newInstance(paramValues);
return result;
} catch (InstantiationException e) {
//這裏是自定義的異常處理
throw new ServiceException(ServiceExceptionCode.instantiationException,
"create " + clazz.getName() + " failed.");
} catch (Exception e) {
throw new ServiceException(ServiceExceptionCode.instantiationException,
"create " + clazz.getName() + " failed.");
}
}
}
這個類,主要是通過反射來創建一個類對象。
2,我們創建一個單例的工廠
public class SingletonFactory {
//單例對象的緩存集合。這裏是線程安全的
private static Map<String, Object> objectMap = Collections
.synchronizedMap(new HashMap<String, Object>());
//創建對象
public static <T> T getSingleton(Class<T> tClass) {
T result;
synchronized (SingletonFactory.class) {
if (objectMap == null) {
objectMap = Collections.synchronizedMap(new HashMap<String, Object>());
}
}
synchronized (objectMap) {
result = (T) objectMap.get(tClass.getName());
if (result == null) {
result = ProtoFactory.newInstance(tClass);
}
objectMap.put(tClass.getName(), result);
}
return result;
}
}
這個類的主要作用:
- 創建一個線程安全的緩存集合
- 創建對象。首先從緩存中檢查,有就返回;沒有,就通過反射創建。
- 因爲集合是線程安全的,這裏就保證了對象的唯一性。
目的只有一個就是確保對象的唯一性。
3,創建一個IUserManager的接口
創建接口。提供UserManager的操作方法
public interface IUserManager {
User getCurUser();
void updateUser(User user);
}
4,改造UserManager
普通的單例,幾乎沒有擴展性。我們現在,通過SingletonFactory就可以實現單例,並且不用限制UserManager對象。
現在,我們的UserManager就不需要使用單例的方式了,我們改造下。
public class UserManager implements IUserManager {
public UserManager() {
}
@Override
public User getCurUser() {
//TODO: 寫獲取User的邏輯
User use = new User();
use.name = "liu";
return null;
}
@Override
public void updateUser(User user) {
//TODO: 更新User的邏輯
}
}
這裏,主要是
- 去掉UserManager對象的普通單例模式。
- 讓它實現接口,我們通過這個接口來限制調用者可以操作的範圍。
5,測試
改造完了以後,我們看下怎麼調用。
public class Test {
public static void main(String[] args) {
IUserManager userManager = SingletonFactory.getSingleton(UserManager.class);
}
}
這裏可以看到,我們可以聲明接口的方式來調用了。這樣,我們就可以通過面向接口的方式來進行編碼。
這樣的好處有:
- 別人只能通過我們接口提供的方法操作。
- 如果,我們對外暴露的是具體的對象的話,那麼別人也就可以對這個對象進行修改了。
- 調用者不知道你具體的對象,內部邏輯。
其實,上面的話,別人調用的時候,還是需要傳入操作的對象。
這樣的壞處是:
- 如果使用的地方特別多,如果,我們需要替換類的話,涉及的地方太多。
6,創建一個對應功能的Factory類
給每一個需要操作的類,創建一個Factory類。這樣,我們對外就不會暴露具體的做操作,並且方便修改。
public class UserFactory {
public static IUserManager getUserManager() {
return SingletonFactory.getSingleton(UserManager.class);
}
}
這樣,外面調用的時候,直接調用getUserManager()方法就可以了,不會引用UserManager類,方便修改,也不會暴露具體的操作類。
public static void main(String[] args) {
// IUserManager userManager = SingletonFactory.getSingleton(UserManager.class);
//新的調用方式
IUserManager userManager = UserFactory.getUserManager();
}
這樣做的好處:
- 徹底解耦,外部調用不需要引入UserManager對象
- 對外部調用者來說,內部的邏輯是完全隔離的
這樣就完了嗎?
如果調用者嫌麻煩,直接通過new 對象來操作呢,我們怎麼控制。。。。。。
7,訪問控制符的使用
上面的對外控制已經做到了。但是,就像上面說的,我想換個姿勢來使用呢…比如
public static void main(String[] args) {
UserManager userManager1 = new UserManager();
}
我們爲了控制這樣的行爲。我們就需要給它的構造器,設置一個合適的訪問控制符了。
public class UserManager implements IUserManager {
protected UserManager() {
}
}
這裏,我們使用protected控制構造器,只讓其在子類及相同包下可見。這樣,其他人如果想使用,只能按照我們提供的規則來使用啦。
到這裏,我們是不是就完成了呢?
還沒有,不過,只差臨門一腳了。。。。。
因爲我們給構造器設置了訪問控制符限制以後,我們的反射類ProtoFactory也需要對應的修改一下纔可以。
因爲通過class.newInstance來獲取非public修飾的構造器的類是實例化不了的。再這樣調用,就會報錯了,因爲我們的構造器已經不是public的了。
下面我們修改ProtoFactory類
public class ProtoFactory {
/**
* 不帶參數的構造方法
*/
public static <T> T newInstance(Class<T> clazz) {
try {
Constructor<T> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
/**
* 帶參數的構造方法
*/
public static <T> T newInstance(Class<T> clazz, Class<?>[] paramTypes, Object[] paramValues) {
try {
Constructor<T> constructor = clazz.getDeclaredConstructor(paramTypes);
constructor.setAccessible(true);
T result = constructor.newInstance(paramValues);
return result;
} catch (InstantiationException e) {
throw new ServiceException(ServiceExceptionCode.instantiationException,
"create " + clazz.getName() + " failed.");
} catch (Exception e) {
throw new ServiceException(ServiceExceptionCode.instantiationException,
"create " + clazz.getName() + " failed.");
}
}
}
通過上面的修改,我們反射獲取類對象就算完成啦。整個單例的設計也就完成了。
通過上面的實現,我們既實現了對象的單例,又通過接口控制了調用者的使用範圍,並且具有了擴展性。