今天去淘宝面试,感觉淘宝很有朝气。面试官大哥问到多态的具体应用。当时一下也想不起来,回来细细思量,发现其实有很多情况下我们不知不觉用了多态的。
这里用这篇文章来概述一下:
作为对象的创建模式,多态模式中的多态类可有多个实例;而且多态类必须自己创建、管理自己的实例,并向外界提供自己的实例。读者在阅读本文的时候,可以参考阅读笔者的《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 States |
Locale代码
一个 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; } } |
这个多态类的构造子是私有的,因此不能用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); } } |
如果用户是美国用户,那么在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")); } } |
在运行时,程序回 打印出下面的结果:
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")); } } |
在运行时,程序回打印出下面的结果:
$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")); } } |
在运行时,程序回打印出下面的结果:
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]的相关例子,在这里我作了一些改动。