设计模式与国际化:策略模式选语言、工厂模式建资源、观察者更新界面

国际化(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 就搞定了,完全不影响其他模块。

小提示:策略模式在这里的核心价值是「开闭原则」——对扩展开放,对修改关闭。你想想看,如果哪天要支持从浏览器自动检测语言,只需要再加一个 AutoDetectLocaleStrategy 即可。

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);
    }
}

为什么用工厂?因为创建资源对象的过程往往涉及文件路径、编码、缓存等细节。把这些细节封装在工厂里,调用方只需要说「给我一份中文资源」就行了。我曾经接手过一个遗留系统,资源文件散落在各个目录,用工厂模式重构后,资源加载逻辑统一了,维护成本降了一大截。

注意:工厂模式不要滥用。如果你的项目只有一种资源格式(比如只用 properties),那直接用 ResourceBundle.getBundle() 就够了,没必要套工厂。我见过有人为了用设计模式而用设计模式,结果代码反而更复杂了。

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. 三者如何协同工作

光讲理论不行,咱们看看这三个模式怎么配合。下面这张图展示了完整的国际化流程:

国际化三模式协作流程 用户选择语言 策略模式:LocaleStrategy 确定当前语言(中文/英文/日文...) 工厂模式:ResourceFactory 根据语言创建对应的资源对象 观察者模式:LocaleChangeNotifier 通知所有注册的界面组件,自动刷新文本和格式 关键点 1. 策略决定「用什么」 2. 工厂决定「怎么建」 3. 观察者决定「何时更新」 三者解耦,各司其职 新增语言只需: 加策略 + 加资源文件 无需修改现有代码

流程很简单:用户选择语言 → 策略模式确定 Locale → 工厂模式加载对应资源 → 观察者模式通知所有组件刷新。每一步都各司其职,互不干扰。

5. 实战中的避坑指南

讲几个我踩过的坑:

  • 资源文件编码问题:我曾经在 properties 文件里直接写中文,结果部署到 Linux 服务器上全乱码了。后来统一用 Unicode 转义,或者指定 UTF-8 编码加载。
  • 观察者注册时机:组件还没初始化完成就注册了观察者,导致切换语言时空指针异常。我建议在组件的构造函数或初始化方法里注册,在 dispose 方法里注销。
  • 策略模式的线程安全:如果用户频繁切换语言,策略对象可能被多个线程同时访问。我习惯把策略类设计成无状态的,或者用 ThreadLocal 存储当前语言。
  • 资源缓存:每次切换语言都重新加载资源文件,性能堪忧。工厂模式里可以加一层缓存,同一个 Locale 的资源只加载一次。
核心总结:国际化不是简单的翻译替换,而是一个涉及「选择、创建、通知」的系统工程。策略模式让语言选择可扩展,工厂模式让资源创建可统一,观察者模式让界面更新自动化。三个模式组合起来,你的国际化方案才算完整。

最后说一句:设计模式是工具,不是目的。别为了用模式而用模式,关键是看你的场景是否真的需要。如果你的项目只有两种语言,而且界面组件很少,那直接写 if-else 反而更简单。但如果你要做的是一个多语言、多区域、需要动态切换的国际化系统,那今天讲的这三个模式,绝对值得你花时间好好消化。