设计模式与国际化:策略模式选语言、工厂模式建资源、观察者更新界面
国际化(i18n)这事儿,说白了就是让软件能说多种语言。我早年做第一个跨国项目时,天真地以为搞几个 properties 文件就完事了。结果呢?界面语言切换后,部分组件没刷新,日期格式还乱套了。嗯,那时候我才意识到,国际化不是配个翻译文件那么简单。
今天咱们聊聊怎么用三个设计模式,把国际化这件事做得干净利落。你想想看,一个完整的国际化方案,至少需要解决三个问题:语言怎么选、资源怎么建、界面怎么更新。正好,策略模式、工厂模式、观察者模式,一个萝卜一个坑。
1. 策略模式:让语言选择可插拔
语言选择策略,说白了就是「当前该用哪种语言」。我见过不少项目把语言判断写死在 if-else 里,比如:
if (locale.equals("zh_CN")) {
// 中文逻辑
} else if (locale.equals("en_US")) {
// 英文逻辑
}
这种写法,每加一种语言就要改代码。我个人习惯用策略模式把它抽出来:
// 策略接口
public interface LocaleStrategy {
Locale getLocale();
String getLanguageTag();
}
// 具体策略
public class ChineseLocaleStrategy implements LocaleStrategy {
@Override
public Locale getLocale() { return Locale.CHINA; }
@Override
public String getLanguageTag() { return "zh-CN"; }
}
public class EnglishLocaleStrategy implements LocaleStrategy {
@Override
public Locale getLocale() { return Locale.US; }
@Override
public String getLanguageTag() { return "en-US"; }
}
// 上下文
public class LocaleContext {
private LocaleStrategy strategy;
public void setStrategy(LocaleStrategy strategy) {
this.strategy = strategy;
}
public Locale getCurrentLocale() {
return strategy.getLocale();
}
}
这样做的好处是:新增语言时,你只需要加一个策略类,不用动现有代码。我在项目中遇到过客户要求支持阿拉伯语,加一个 ArabicLocaleStrategy 就搞定了,完全不影响其他模块。
2. 工厂模式:统一创建资源对象
有了语言策略,接下来要解决的是「资源从哪里来」。不同语言的资源文件格式可能不同:有的用 properties,有的用 JSON,有的甚至从数据库读取。这时候工厂模式就派上用场了。
我建议定义一个资源工厂接口:
public interface ResourceFactory {
ResourceBundle createBundle(String baseName, Locale locale);
Map<String, String> createResourceMap(Locale locale);
}
public class PropertiesResourceFactory implements ResourceFactory {
@Override
public ResourceBundle createBundle(String baseName, Locale locale) {
return ResourceBundle.getBundle(baseName, locale);
}
@Override
public Map<String, String> createResourceMap(Locale locale) {
// 从 properties 文件加载
ResourceBundle bundle = createBundle("messages", locale);
Map<String, String> map = new HashMap<>();
for (String key : bundle.keySet()) {
map.put(key, bundle.getString(key));
}
return map;
}
}
// 工厂的工厂(抽象工厂模式)
public class ResourceFactoryProvider {
public static ResourceFactory getFactory(String type) {
if ("properties".equals(type)) {
return new PropertiesResourceFactory();
} else if ("json".equals(type)) {
return new JsonResourceFactory();
}
throw new IllegalArgumentException("不支持的资源类型: " + type);
}
}
为什么用工厂?因为创建资源对象的过程往往涉及文件路径、编码、缓存等细节。把这些细节封装在工厂里,调用方只需要说「给我一份中文资源」就行了。我曾经接手过一个遗留系统,资源文件散落在各个目录,用工厂模式重构后,资源加载逻辑统一了,维护成本降了一大截。
3. 观察者模式:语言切换时自动刷新界面
这是最容易被忽略的一环。很多项目切换语言后,界面还是老样子,必须重启应用才能生效。为什么?因为没有通知机制。
观察者模式正好解决这个问题。我们把「语言切换」当作一个事件,所有需要刷新的界面组件都注册为观察者:
// 主题(被观察者)
public class LocaleChangeNotifier {
private List<LocaleObserver> observers = new ArrayList<>();
private Locale currentLocale;
public void addObserver(LocaleObserver observer) {
observers.add(observer);
}
public void removeObserver(LocaleObserver observer) {
observers.remove(observer);
}
public void changeLocale(Locale newLocale) {
this.currentLocale = newLocale;
notifyObservers();
}
private void notifyObservers() {
for (LocaleObserver observer : observers) {
observer.onLocaleChanged(currentLocale);
}
}
}
// 观察者接口
public interface LocaleObserver {
void onLocaleChanged(Locale newLocale);
}
// 具体观察者:一个面板
public class UserPanel implements LocaleObserver {
private JLabel welcomeLabel;
private JButton submitButton;
@Override
public void onLocaleChanged(Locale newLocale) {
// 重新加载资源并更新界面
ResourceBundle bundle = ResourceBundle.getBundle("messages", newLocale);
welcomeLabel.setText(bundle.getString("welcome"));
submitButton.setText(bundle.getString("submit"));
}
}
这样做的好处是:语言切换时,所有注册过的组件自动刷新。你不需要在每个按钮的点击事件里手动调用刷新方法。嗯,这里要注意:观察者模式容易造成「通知风暴」,如果界面组件太多,可以考虑用事件总线(Event Bus)来解耦。
4. 三者如何协同工作
光讲理论不行,咱们看看这三个模式怎么配合。下面这张图展示了完整的国际化流程:
流程很简单:用户选择语言 → 策略模式确定 Locale → 工厂模式加载对应资源 → 观察者模式通知所有组件刷新。每一步都各司其职,互不干扰。
5. 实战中的避坑指南
讲几个我踩过的坑:
- 资源文件编码问题:我曾经在 properties 文件里直接写中文,结果部署到 Linux 服务器上全乱码了。后来统一用 Unicode 转义,或者指定 UTF-8 编码加载。
- 观察者注册时机:组件还没初始化完成就注册了观察者,导致切换语言时空指针异常。我建议在组件的构造函数或初始化方法里注册,在 dispose 方法里注销。
- 策略模式的线程安全:如果用户频繁切换语言,策略对象可能被多个线程同时访问。我习惯把策略类设计成无状态的,或者用 ThreadLocal 存储当前语言。
- 资源缓存:每次切换语言都重新加载资源文件,性能堪忧。工厂模式里可以加一层缓存,同一个 Locale 的资源只加载一次。
最后说一句:设计模式是工具,不是目的。别为了用模式而用模式,关键是看你的场景是否真的需要。如果你的项目只有两种语言,而且界面组件很少,那直接写 if-else 反而更简单。但如果你要做的是一个多语言、多区域、需要动态切换的国际化系统,那今天讲的这三个模式,绝对值得你花时间好好消化。