作為對象的創建模式,多態模式中的多態類可有多個實例;而且多態類必須自己創建、管理自己的實例,並向外界提供自己的實例。讀者在閱讀本文的時候,可以參考閱讀筆者的《Java與模式》一書(剛由電子工業出版社出版)中的相關章節。
引言 一個真實的項目
這是一個真實的、面向全球消費者的華爾街金融網站項目的一部份。按照項目計劃書,這個網站系統是要由數據庫驅動的,並且要支持十九種不同的語言;而且在將來支持更多的語言。消費者在登錄到系統上時可以選擇自己所需要的語言,系統則根據用戶的選擇將網站的靜態文字和動態文字全部轉換為用戶所選擇的語言。
經過討論,設計師們同意對靜態文字和動態文字采取不同的解決方案:
把所有的網頁交給翻譯公司對上面的靜態文字進行翻譯, 而網頁上面的動態內容則需要程序解決。
在進行了研究後,設計師們發現,他們需要解決的動態文字的“翻譯”問題,實際上是將數據庫中的一些靜態或者半靜態的數據進行“翻譯”。下面就是一個典型的數據表:
貨幣代碼 貨幣名稱 貨幣尾數 USD America (United States of America), Dollars 2 CNY China, Yuan Renminbi 2 EUR France, Euro 2 JPY Japan, Yen 0代碼清單1、為英文用戶的准備的貨幣列表。
貨幣代碼永遠是上面所看到的英文代碼,但是貨幣名稱應當根據用戶所選擇的語言不同而不同。比如對中文讀者就應當翻譯成為下面的表:
貨幣代碼 貨幣名稱 貨幣尾數 USD 美國 (美利堅合眾國), 美元 2 CNY 中國,人民幣元 2 EUR 法國, 歐元 2 JPY 日本, 日元 0代碼清單2、為中文用戶准備的貨幣列表。
這樣的表會在網頁上作為下拉菜單出現,用戶看到的是貨幣名稱,而系統內部使用的是貨幣代碼。
國際化解決方案
這樣的問題就是國際化的問題,所謂國際化就是Internationalization,簡稱作i18n(請參見本章最後的問答題)。
設計師所采取的實際方案是分層方案,也就是MVC模式。MVC模式將系統分為三個層次,也就是模型(Model)、視圖(View)、控制器(Control)三個部份。國際化是視圖部份的問題,因此應當在視圖部份得到解決。
圖1、MVC模式的示意圖。
換言之,系統的內核可以是純英文的;在內核外部增加一個殼層負責語言翻譯工作。請見下面的概念圖:
圖2、英文內核和翻譯殼層的概念圖。
所謂內核就是系統的模型,而翻譯殼層便是視圖的一部份。對多語言的支持屬於視圖功能,因此不應當在內核解決,而應當在視圖解決。這就是設計師們達成的總體方案。
多態模式 多態模式的特點
所謂的多態模式(Multiton Pattern),實際上就是單態模式的自然推廣。作為對象的創建模式,多態模式或多態類有以下的特點:
多態類可有多個實例; 多態類必須自己創建、管理自己的實例,並向外界提供自己的實例。
單態類一般情況下最多只可以有一個實例,請見下面的結構圖:
圖3、單態類的結構圖。
但是單態模式的精神是允許有限個實例,並不是僅允許一個實例;這種最多只允許有限多個實例,並向整個JVM提供自己實例的類叫做多態類(Multiton),這種模式叫做多態模式(Multiton Pattern),請參見下面的結構圖。
圖4、多態類的結構圖。
本章就需要用多態模式來實現資源對象,需要構造出能提供有限個實例,每個實例有各不相同的屬性(即Locale代碼)。
有上限多態類
一個實例數目有上限的多態類已經把實例的上限當作邏輯的一部份建造到了多態類的內部;這種多態模式叫做有上限多態模式。
比如每一麻將牌局都需要兩個色子,因此色子就應當是雙態類。這裡就以這個系統為例,說明多態模式的結構。
圖5、色子的類圖。
下面就是多態類Die(色子)的源代碼:
package com.javapatterns.multilingual.dice;
import java.util.Random;
import java.util.Date;
public class Die
{
private static Die die1 = new Die();
private static Die die2 = new Die();
/**
* 私有的構造子保證外界無法
* 直接將此類實例化
*/
private Die()
{
}
/**
* 工廠方法
*/
public static Die getInstance(int whichOne)
{
if (whichOne == 1)
{
return die1;
}
else
{
return die2;
}
}
/**
* 擲色子,返還一個在1到6之間的
* 隨機數。
*/
public synchronized int dice()
{
Date d = new Date();
Random r = new Random( d.getTime() );
int value = r.nextInt();
value = Math.abs(value);
value = value % 6;
value += 1;
return value;
}
}
代碼清單3、多態類的源代碼。
在多態類Die中,使用了餓漢方式創建了兩個Die的實例。根據靜態工廠方法的參數,工廠方法返還兩個事例中的一個。Die對象的dice()方法代表擲色子,這個方法會返還一個在1到6之間的隨機數,相當於色子的點數。
package com.javapatterns.multilingual.dice;
public class Client
{
private static Die die1, die2;
public static void main(String[] args)
{
die1 = Die.getInstance(1);
die2 = Die.getInstance(2);
die1.dice();
die2.dice();
}
}
代碼清單4、客戶端的源代碼。
由於有上限的多態類對實例的數目有上限,因此有上限的多態類在這個上限等於1時,多態類就回到了單態類。因此多態類是單態類的推廣,而單態類是多態類的特殊情況。
一個有上限的多態類可以使用靜態變量儲存所有的實例;特別是在實例數目不多的時候,可以使用一個個的靜態變量儲存一個個的實例。在數目較多的時候,就需要使用靜態聚集儲存這些事例。
無上限多態模式
多態類的實例數目並不需要有上限[CAMP02];實例數目沒有上限的多態模式就叫做無上限多態模式。
由於沒有上限的多態類對實例的數目是沒有限制的,因此雖然這種多態模式是單態模式的推廣,但是這種多態類並不一定能夠回到單態類。
由於事先不知道要創建多少個實例,因此必然是使用聚集管理所有的實例。本章要討論的多語言支持方案就需要應用到多態模式,關於沒有上限的多態模式的實現可以參見下面的討論。
圖6、沒有上限的多態模式(左)和有上限的多態模式(右)的類圖。其中N就是實例數目的上限。
有狀態的和沒有狀態的多態類
如同單態類可以分成有狀態的和沒有狀態的兩種一樣,多態類也可以分成有狀態的和沒有狀態的兩種。
多態對象的狀態如果是可以在加載後改變的,那麼這種多態對象叫做可變多態對象(Mutable Singleton);如果多態對象的狀態在加載後就不可以改變,那麼這種多態對象叫做不變多態對象(Immutable Singleton)。顯然不變多態類的情形較為簡單,而可變單態類的情形較為復雜。
如果一個系統是建立在諸如EJB和RMI等分散技術之上的,那麼多態類有可能會出現數個實例;因此在這種情況下除非提供有效的協調機制,不然最好不要使用有狀態的和可變的單態類,以避免出現狀態不自恰的情況。讀者可以參考本書的“單態(Singleton)模式”一章中的相關討論。
多語言項目的設計
由於熟悉了多態模式,系統的設計實際上並不復雜。
語言代碼
下面就是幾個常見的語言代碼:
語言代碼說明
de German en English fr French ja Japanese jw Javanese ko Korean zh Chinese地區代碼
下面就是幾個常見的地區代碼:
地區代碼說明
CN China DE Germany FR France IN India US United StatesLocale代碼
一個 Locale 代碼由語言代碼和地區代碼組合而成,比如:
語言代碼 地區代碼 Locale代碼 說明 en US en_US 美國英語 en GB en_GB 英國英語 fr FR fr_FR 法國法語 fr CA fr_CA 加拿大法語 de DE de_DE 德國德語 zh CH zh_CH 簡體漢語代碼清單3、Locale代碼、語言代碼和地區代碼。
Resource文件及其命名規范
一個Resource文件是一個簡單的文本文件。一個Resource文件的名字是由一個短文件名和文件的擴展名properties組成,而Resource文件的短文件名則是Java程序在調用此文件時使用的文件名。
一個Resource文件和一個普通的properties文件並無本質區別,但Java語言對兩者的支持是有區別的。java.util.Properties類不支持多語言,而java.util.ResourceBundle類則支持多語言。
當Locale代碼是en_US時,Resource文件的文件名應當是短文件名加上Locale代碼,就是en_US。當Locale代碼是zh_CH時,Resource文件的文件名應當是短文件名加上Locale代碼,就是zh_CH。
怎樣使用Locale對象和ResourceBundle對象。
那麼怎樣使用 ResourceBundle 讀取一個Resource文件呢?下面就是一個例子:
Locale locale = new Locale("fr","FR"); ResourceBundle res = ResourceBundle.getBundle("shortname",locale);
代碼清單4、怎樣使用Locale對象和ResourceBundle對象。
在上面的例子裡面,res對象會加載一個名為shortname_fr_FR.properties的Resource文件。
系統的設計
這裡給出系統的結構圖。其中LingualResourceTester是一個示意性的客戶端類,而LingualResource是一個多態類。
圖7、多態類LingualResource和客戶端類的類圖結構。
下面就是這個多態類的源代碼:
package com.javapatterns.multilingual;
import java.util.HashMap;
import java.util.Locale;
import java.util.ResourceBundle;
public class LingualResource
{
private String language = "en";
private String region = "US";
private String localeCode = "en_US";
private static final String FILE_NAME = "res";
private static HashMap instances =
new HashMap(19);
private Locale locale = null;
private ResourceBundle resourceBundle = null;
private LingualResource lnkLingualResource;
/**
* 私有的構造子保證外界無法直接將此類實例化
*/
private LingualResource(
String language, String region)
{
this.localeCode = language;
this.region = region;
localeCode =
makeLocaleCode(language , region);
locale = new Locale(language, region);
resourceBundle =
ResourceBundle.getBundle(FILE_NAME, locale);
instances.put( makeLocaleCode(language, region) ,
resourceBundle);
}
/**
* 私有的構造子保證外界無法直接將此類實例化
*/
private LingualResource()
{
file://do nothing
}
/**
* 工廠方法,返還一個具有指定的內部狀態的實例
*/
public synchronized static LingualResource
getInstance(String language, String region)
{
if (instances.containsKey(
makeLocaleCode(language , region )))
{
return (LingualResource) instances.get(
makeLocaleCode(language , region ));
}
else
{
return new
LingualResource(language, region);
}
}
public String getLocaleString(String code)
{
return resourceBundle.getString(code);
}
private static String makeLocaleCode(
String language, String region)
{
return language + "_" + region;
}
}
代碼清單5、多態類LingualResource的源代碼。其中的makeLocaleCode()是一個輔助性的方法,在傳入語言代碼和地區代碼時,此方法可以返回一個Locale代碼。
這個多態類的構造子是私有的,因此不能用new關鍵字來實例化。所有的實例必須通過調用靜態getInstance()方法來得到。在getInstance()方法被調用時,程序會首先檢查傳入的Locale代碼是否已經在instances集合中存在;如果已經存在,即直接返回它所對應的LingualResource對象,否則就會首先創建一個這個Locale代碼所對應的LingualResource對象,將之存入instances集合,並返回這個實例。
下面給出一個客戶端的源代碼:
package com.javapatterns.multilingual;
public class LingualResourceTester
{
public static void main(String[] args)
{
LingualResource ling =
LingualResource.getInstance("en" , "US");
String usDollar = ling.getLocaleString("USD");
System.out.println("USD=" + usDollar);
LingualResource lingZh =
LingualResource.getInstance("zh" , "CH");
String usDollarZh = lingZh.getLocaleString("USD");
System.out.println("USD=" + usDollarZh);
}
}
代碼清單6、客戶端類LingualResourceTester的源代碼。
如果用戶是美國用戶,那麼在JSP網頁中可以通過調用getLocaleString()方法得到相應的英文說明。比如:
LingualResource ling = LingualResource.getInstance("en" , "US");
String usDollar = ling.getLocaleString("USD");
就會返還
US Dollar
相應地,如果用戶是中國大陸的用戶,那麼在JSP網頁中可以通過調用getLocaleString()方法得到相應的中文說明。比如,
LingualResource ling = LingualResource.getInstance("zh" , "CH");
String usDollar = ling.getLocaleString("USD");
就會返還
美元
Resource文件的內容
為美國英文准備的Resource文件res_en_US.properties的內容如下:
USD=US Dollar
JPY=Japanese Yen
代碼清單7、Resource文件res_en_US.properties的內容。
為簡體中文准備的Resource文件res_zh_CH.properties的內容如下:
USD=美元
JPY=日元
代碼清單8、Resource文件res_zh_CH.properties的內容。
問答題
第一題、請問為什麼Internationalization又簡稱作i18n?
第二題、請給出一個根據語言代碼和地區代碼將數目字格式化的例子。
第三題、請給出一個根據語言代碼和地區代碼將貨幣數目字格式化的例子。
第四題、請給出一個根據語言代碼和地區代碼將百分比格式化的例子。
問答題答案
第一題答案、在英文字Internationalization中,第一個字母i和最後一個字母n之間有18個字母,因此Internationalization又簡稱作i18n。
第二題答案、Java庫java.text.NumberFormat類提供了對數目字格式的支持,下面給出的就是解答的類圖:
圖8、對數目字格式支持的解答。
程序的源代碼如下:
package com.javapatterns.multilingual.number;
import java.util.Locale;
import java.text.NumberFormat;
public class NumberFormatTester
{
static public void displayNumber(
Double amount, Locale currentLocale)
{
NumberFormat formatter;
String amountOut;
formatter =
NumberFormat.getNumberInstance(currentLocale);
amountOut = formatter.format(amount);
System.out.println(amountOut + " "
+ currentLocale.toString());
}
static public void main(String[] args)
{
displayNumber(new Double(1234567.89),
new Locale("en", "US"));
displayNumber(new Double(1234567.89),
new Locale("de", "DE"));
displayNumber(new Double(1234567.89),
new Locale("fr", "FR"));
}
}
代碼清單9、Resource文件res_zh_CH.properties的內容。
在運行時,程序回打印出下面的結果:
456,789% en_US
456.789% de_DE
456 789% fr_FR
代碼清單10、Resource文件res_zh_CH.properties的內容。
第三題答案、Java庫java.text.NumberFormat類提供了對貨幣數目格式的支持。下面給出的就是解答的類圖:
圖9、對貨幣數目格式支持的解答。
程序的源代碼如下:
package com.javapatterns.multilingual.number;
import java.util.Locale;
import java.text.NumberFormat;
public class CurrencyFormatTester
{
static public void displayCurrency(Double amount,
Locale currentLocale)
{
NumberFormat formatter;
String amountOut;
formatter =
NumberFormat.getCurrencyInstance(currentLocale);
amountOut = formatter.format(amount);
System.out.println(amountOut + " "
+ currentLocale.toString());
}
static public void main(String[] args)
{
displayCurrency(new Double(1234567.89),
new Locale("en", "US"));
displayCurrency(new Double(1234567.89),
new Locale("de", "DE"));
displayCurrency(new Double(1234567.89),
new Locale("fr", "FR"));
}
}
代碼清單11、Resource文件res_zh_CH.properties的內容。
在運行時,程序回打印出下面的結果:
$1,234,567.89 en_US
1.234.567,89 DM de_DE
1 234 567,89 F fr_FR
代碼清單12、Resource文件res_zh_CH.properties的內容。
第四題答案、Java庫java.text.NumberFormat類提供了對百分比格式的支持,下面給出的就是解答的類圖:
圖10、對百分比式支持的解答。
程序的源代碼如下:
package com.javapatterns.multilingual.number;
import java.util.Locale;
import java.text.NumberFormat;
public class PercentFormatTester
{
static public void displayPercent(
Double amount, Locale currentLocale)
{
NumberFormat formatter;
String amountOut;
formatter =
NumberFormat.getPercentInstance(currentLocale);
amountOut = formatter.format(amount);
System.out.println(amountOut + " "
+ currentLocale.toString());
}
static public void main(String[] args)
{
displayPercent(new Double(4567.89),
new Locale("en", "US"));
displayPercent(new Double(4567.89),
new Locale("de", "DE"));
displayPercent(new Double(4567.89),
new Locale("fr", "FR"));
}
}
代碼清單13、Resource文件res_zh_CH.properties的內容。
在運行時,程序回打印出下面的結果:
1,234,567.89 en_US
1.234.567,89 de_DE
1 234 567,89 fr_FR
代碼清單14、Resource文件res_zh_CH.properties的內容。
(本章問答題第二、三、四題的解答參考了[GREEN]的相關例子,在這裡我作了一些改動。)