1. 程式人生 > >[Effective Java]考慮用靜態工廠方法代替構造器

[Effective Java]考慮用靜態工廠方法代替構造器

本文主要介紹如何使用靜態工廠方法已經在那種場合來使用這種方式代替構造方法。       

眾所周知,對於類而言,我們為了獲得一個類的例項物件,通常情況下會提供一個公有的(public) 的構造器。當然除了這種方法以外,我們還可以通過給類提供一個public的靜態工廠方法(static factory method)的方式來完成,讓它返回一個類的例項。

先看一個簡單的Boolean的示例,這個示例將boolean基本型別值轉換成一個Boolean物件的引用。

public static Boolean valueOf(boolean b){
       return b ? Boolean.TRUE : Boolean.FALSE;
}
需要注意的是這裡的靜態工廠方法與設計模式中所說的工廠方法模式不同。

那麼,靜態工廠方法相比於公有的構造器有哪些優勢呢?

1. 靜態工廠方法可以有不同的名稱,程式碼更清楚,更易讀。

簡單解釋一下:

      場景:構造器的命名都一致,一個類只能有一個指定簽名的構造器。

      當一個類需要提供多個構造器時,通常只是通過不同的形參型別的順序加以區分,但其函式名還是相同的,無法提供較高的區分度。

  結論:當一個類需要多個帶有相同簽名的構造器時,不妨考慮用靜態工廠方法代替構造器,並且慎重地選擇名稱以便突出它們之間的區別。這樣該靜態工廠方法有名稱,通過賦予有意義的名稱,使用該方法的程式設計師可以清晰的知道該方法的含義。

     方法的簽名(signature)由它的名稱和所有引數的返回型別組成,而不包括它的返回型別。

2. 靜態工廠方法不必在每次呼叫它們的時候都建立一個新物件。

比如你可以在靜態工廠方法裡限定建立該物件的個數,當超出規定的個數時,返回快取裡的物件。

      場景:呼叫構造器時每次都建立新物件。

      這一點很顯然,我們知道Singleton其實也提供一個靜態工廠方法獲取例項。

   除此之外,可以用==代替equals()方法,達到效能的提升。

3. 靜態工廠方法可以返回原返回型別的任何子型別的物件。

      場景:構造器只能返回當前類的例項,無法返回子類的例項。

      這個優點的確很有用,能夠充分利用多型,使程式碼更具有可擴充套件性。

      設計模式中的工廠方法也有體現,可根據入參type返回型別為Types的不同子類例項。

public Types getType(String type) {...}
這樣我們在選擇返回物件的類時就有更大的靈活性,這種靈活性的應用就是API可以返回物件,同時又不會使物件的類變成公有的。以這種方式隱藏實現類會使API變得簡潔。這項技術適合於基於介面的框架(interface-based framework)。這種面向介面的程式設計也提高了軟體的可維護性和效能。

4. 靜態工廠方法在建立引數化型別例項的時候使程式碼變得更加簡潔。

      場景: Java的泛型類在例項化時,還是需要寫兩次型別引數,非常冗長

      靜態工廠方法可以幫你改善這種情況:

      舉個例子,假設在HashMap類中提供如下靜態工廠方法:

public static <k, v> HashMap<k, v> newInstance(){
   return new HashMap<k, v>();  
}
那麼,呼叫時應該是這樣的:
HashMap<Stirng, List<String>> m = HashMap.newInstance();
而在呼叫引數化的構造器時,則通常需要這樣做,顯得比較繁瑣。
HashMap<String, List<String>> m = new HashMap<String, List<String>>();

另外值得一提的是,靜態工廠方法返回的物件所屬的類,在編寫包含該靜態工廠方法的類時不必存在。這種靈活的靜態工廠方法構成了服務提供者框架(Service Provider Framework)的基礎,例如JDBC的API。

所謂服務提供者框架是指這樣的一個系統:多個服務提供者實現同一個服務,由框架(即系統)來負責為客戶端提供這種服務的多個實現並將這些實現解耦。

服務提供者框架有三個主要的元件:1.服務介面(Service interface),這是提供者實現的。2.提供者註冊API(Provider Registration API),這是系統用來註冊實現,讓客戶端訪問它們的。3.服務訪問API(Service Access API),這是客戶端獲取服務例項用的。

另外還有第四個可選介面,服務提供者介面(Service Provider Interface),負責建立其服務實現的例項。若沒有服務提供者介面,實現就按照類名稱進行註冊,並通過反射方式進行例項化。對JDBC而言,Connection就是它的服務介面,DriverManager.registerDriver就是它的提供者註冊API,DriverManager.getConnection是服務訪問API,Driver就是服務提供者介面。

下面是一個簡單的服務提供者框架的實現:

// Service provider framework sketch - Service interface
package com.xjtu.cruise.chapter02.item01;
public interface Service {
// Service-specific methods go here
}
// Service provider framework sketch 
//Noninstantiable class for service registration and access
package com.xjtu.cruise.chapter02.item01;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Services {
	private Services() {
	} // Prevents instantiation (Item 4)

	// Maps service names to services
	private static final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>();
	public static final String DEFAULT_PROVIDER_NAME = "<def>";

	// Provider registration API
	public static void registerDefaultProvider(Provider p) {
		registerProvider(DEFAULT_PROVIDER_NAME, p);
	}

	public static void registerProvider(String name, Provider p) {
		providers.put(name, p);
	}

	// Service access API
	public static Service newInstance() {
		return newInstance(DEFAULT_PROVIDER_NAME);
	}

	public static Service newInstance(String name) {
		Provider p = providers.get(name);
		if (p == null)
			throw new IllegalArgumentException(
					"No provider registered with name: " + name);
		return p.newService();
	}
}
// Service provider framework sketch - Service provider interface 
package com.xjtu.cruise.chapter02.item01;

public interface Provider {
	Service newService();
}
// Simple test program for service provider framework

package com.xjtu.cruise.chapter02.item01;

public class Test {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Services.registerDefaultProvider(DEFAULT_PROVIDER);
		Services.registerProvider("comp", COMP_PROVIDER);
		Services.registerProvider("armed", ARMED_PROVIDER);

		Service s1 = Services.newInstance();
		Service s2 = Services.newInstance("comp");
		Service s3 = Services.newInstance("armed");
		System.out.printf("%s %s %s\n", s1, s2, s3);
	}	
	
	private static Provider DEFAULT_PROVIDER = new Provider() {
		@Override
		public Service newService() {
			return new Service() {
				@Override
				public String toString() {
					return "Default service";
				}
			};
		}
	};
	
	
	private static Provider COMP_PROVIDER = new Provider() {
		@Override
		public Service newService() {
			return new Service() {
				@Override
				public String toString() {
					return "Complementary service";
				}
			};
		}
	};
	
	
	private static Provider ARMED_PROVIDER = new Provider() {
		@Override
		public Service newService() {
			return new Service() {
				@Override
				public String toString() {
					return "Armed service";
				}
			};
		}
	};

}

但是,我們也應該很清楚的認識到使用靜態工廠方法帶來的隱患

1. 如果類中沒有提供public或protected的構造器,將造成該類不能子類化

不能子類化在某些情況下是無法接受的,但是,這樣也有好處:策略模式教導我們應該多用組合而不是繼承,當一個類不能子類化後,組合將是你唯一的選擇。

2. 靜態工廠方法和其他靜態方法本質上並沒有區別

前已經述及,靜態工廠方法本質上就是一個靜態方法。這使得使用人員在查閱JavaDoc時,可能將其和一般的靜態方法混淆,造成使用上的困擾。

總結:

  鑑於構造器有各種各樣的不便,可以考慮用靜態方法代替。它會給我們帶來以下優缺點:

   優點:

    1) 有名稱;

    2)不必在每一次建立時都提供新物件;

    3) 返回子型別;

    4) 使程式碼更加簡單。

   缺點:

    1)如果類中沒有提供public或protected的構造器,將造成該類不能子類化;

    2)靜態工廠方法和其他靜態方法本質上並沒有區別。