编程的艺术:解析六大设计模式,助你写出更优雅的代码
1. 观察者模式(Observer Pattern)
-
应用场景解释: 当一个对象的状态发生变化时,希望所有依赖于它的对象都能收到通知并自动更新。可以想象成报社和订阅者的关系报社是
发布者
,订阅者是观察者
。当报社有新的新闻(状态变化)时,订阅者会收到报纸并了解最新情况。 -
场景: 假设你正在构建一个简单的天气应用程序,你希望当天气状态发生变化时,能够及时通知多个
观察者
(例如显示当前温度的窗口、显示天气图标的窗口等)。- 实例: 首先创建一个接口
Observer
定义观察者
应该实现的方法,通常是在发布者状态发生变化时调用。
public interface Observer { // 观察者执行逻辑 void update(float temperature); // 其他... }
- 定义主题接口
Subject
,定义了主题(天气数据)的基本行为,包括注册、移除和通知观察者。
public interface Subject { void registerObserver(Observer observer); void removeObserver(Observer observer); void notifyObservers(); }
- 创建一个
WeatherData
类作为主题,并实现主题接口Subject
,实现注册、移除和通知观察者的方法。
import java.util.ArrayList; import java.util.List; public class WeatherData implements Subject { // 存放观察者的列表 private List<Observer> observers; private float temperature; public WeatherData() { observers = new ArrayList<>(); } // 添加观察者,即订阅天气消息 @Override public void registerObserver(Observer observer) { observers.add(observer); } // 移除观察者,即取消订阅 @Override public void removeObserver(Observer observer) { observers.remove(observer); } // 通知观察者 @Override public void notifyObservers() { for (Observer observer : observers) { observer.update(temperature); } } // 模拟天气数据变化,并在变化时通知观察者 public void setTemperature(float temperature) { this.temperature = temperature; notifyObservers(); } }
- 创建具体观察者类
TemperatureDisplay
和WeatherIconDisplay
,并实现主题接口Observer
,实现:在发布者(天气)
状态发生变化时观察者
真正的执行逻辑。
public class TemperatureDisplay implements Observer { private float temperature; @Override public void update(float temperature) { this.temperature = temperature; display(); } public void display() { System.out.println("Temperature Display: " + temperature + " degrees Celsius"); } }
public class WeatherIconDisplay implements Observer { private float temperature; @Override public void update(float temperature) { this.temperature = temperature; display(); } public void display() { System.out.println("Weather Icon Display: " + getWeatherIcon()); } private String getWeatherIcon() { // 根据温度等信息确定天气图标 // 简化为示例,实际应根据具体逻辑处理 if (temperature > 25) { return "☀️"; } else { return "❄️"; } } }
- 使用观察者模式
public class Main { public static void main(String[] args) { WeatherData weatherData = new WeatherData(); TemperatureDisplay temperatureDisplay = new TemperatureDisplay(); WeatherIconDisplay weatherIconDisplay = new WeatherIconDisplay(); // 添加观察者/订阅 weatherData.registerObserver(temperatureDisplay); weatherData.registerObserver(weatherIconDisplay); // 模拟天气数据变化 weatherData.setTemperature(30.0f); // 输出: // Temperature Display: 30.0 degrees Celsius // Weather Icon Display: ☀️ weatherData.setTemperature(20.0f); // 输出: // Temperature Display: 20.0 degrees Celsius // Weather Icon Display: ❄️ } }
WeatherData
充当主题,而TemperatureDisplay
和WeatherIconDisplay
是具体的观察者。通过注册和通知的方式,主题能够实时通知所有观察者,使它们能够根据天气数据的变化更新自己的显示。 - 实例: 首先创建一个接口
2. 装饰者模式(Decorator Pattern)
-
应用场景解释:当我们想要在
不修改
原始类代码的情况下,动态地
给一个对象添加一些额外的职责。想象你有一个咖啡店,你有一种基本的咖啡(ConcreteComponent),然后你可以用装饰器
(比如加糖、加奶)来动态地添加额外的功能,而不改变原始咖啡对象。 -
场景: 假设你经营一家咖啡店,有不同种类的咖啡(例如浓缩咖啡、拿铁、美式咖啡等),以及一些额外的配料选项(例如牛奶、糖浆、摩卡等)。你希望能够在
不修改
每个具体咖啡类的情况下,动态地
为咖啡添加不同的配料。- 实例: 创建一个
Coffee
接口,定义了制作咖啡的方法。
public interface Coffee { void makeCoffee(); }
- 然后,实现不同种类的具体咖啡类
Espresso
和Latte
。
public class Espresso implements Coffee { @Override public void makeCoffee() { System.out.println("Making Espresso"); } }
public class Latte implements Coffee { @Override public void makeCoffee() { System.out.println("Making Latte"); } }
- 接下来,创建一个
CoffeeDecorator
抽象类,继承自Coffee
接口,用于装饰具体的咖啡类。
public abstract class CoffeeDecorator implements Coffee { protected Coffee decoratedCoffee; public CoffeeDecorator(Coffee decoratedCoffee) { this.decoratedCoffee = decoratedCoffee; } @Override public void makeCoffee() { decoratedCoffee.makeCoffee(); } }
- 最后,实现具体的装饰器类,如
MilkDecorator
和MochaDecorator
,它们分别添加牛奶和摩卡。
public class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee decoratedCoffee) { super(decoratedCoffee); } @Override public void makeCoffee() { super.makeCoffee(); addMilk(); } private void addMilk() { System.out.println("Adding Milk"); } }
public class MochaDecorator extends CoffeeDecorator { public MochaDecorator(Coffee decoratedCoffee) { super(decoratedCoffee); } @Override public void makeCoffee() { super.makeCoffee(); addMocha(); } private void addMocha() { System.out.println("Adding Mocha"); } }
- 使用装饰器模式
public class Main { public static void main(String[] args) { Coffee espresso = new Espresso(); espresso.makeCoffee(); Coffee latteWithMilk = new MilkDecorator(new Latte()); latteWithMilk.makeCoffee(); Coffee mochaLatte = new MochaDecorator(new MilkDecorator(new Latte())); mochaLatte.makeCoffee(); } }
这个例子演示了如何使用装饰器模式,在不修改具体咖啡类的情况下,动态地为咖啡添加不同的配料。
- 实例: 创建一个
3. 策略模式(Strategy Pattern)
-
应用场景:封装有多种算法或行为,希望在运行时能够灵活地选择其中之一。想象一个电商网站,可以根据不同的策略来计算折扣,比如春节特惠、会员折扣等,这些都可以作为不同的策略。
-
场景: 假设你正在设计一个商场的促销系统,不同的季节或活动需要使用不同的促销策略,例如打折、满减或赠送礼品。
- 实例: 首先创建一个
PromotionStrategy
接口,定义了促销策略的抽象方法applyDiscount
。
public interface PromotionStrategy { void applyDiscount(double amount); }
- 然后,实现不同的促销策略类,如
DiscountPromotion
和GiftPromotion
,它们分别实现了PromotionStrategy
接口。
public class DiscountPromotion implements PromotionStrategy { @Override public void applyDiscount(double amount) { // 实现打折逻辑 // ... } }
public class GiftPromotion implements PromotionStrategy { @Override public void applyDiscount(double amount) { // 实现赠送礼品逻辑 // ... } }
- 最后,创建一个
PromotionContext
类,其中包含一个PromotionStrategy
成员变量,并在需要的时候切换不同的促销策略。
public class PromotionContext { private PromotionStrategy promotionStrategy; public PromotionContext(PromotionStrategy promotionStrategy) { this.promotionStrategy = promotionStrategy; } public void setPromotionStrategy(PromotionStrategy promotionStrategy) { this.promotionStrategy = promotionStrategy; } public void applyDiscount(double amount) { promotionStrategy.applyDiscount(amount); } }
- 使用策略模式
public class Main { public static void main(String[] args) { PromotionContext context = new PromotionContext(new DiscountPromotion()); context.applyDiscount(100.0); context.setPromotionStrategy(new GiftPromotion()); context.applyDiscount(200.0); } }
这个例子通过在运行时切换不同的促销策略,灵活地应对不同的商场促销需求。
- 实例: 首先创建一个
4. 工厂模式(Factory Pattern)
-
应用场景解释: 提供了一种创建对象的接口,但允许
子类
决定实例化哪个类;这样,一个类的实例化过程延迟到其子类。想象你正在创建一个简单的电子设备工厂,可以生产不同类型的电子设备,如手机、平板电脑和笔记本电脑。 -
场景: 假设你经营一家电子设备工厂,你生产手机和平板电脑两种不同类型的设备。你希望能够通过一个共同的接口来创建这两种设备,并且在未来可能添加更多类型的设备。
- 实例: 创建一个
Device
接口。
public interface Device { void turnOn(); void turnOff(); }
- 然后实现不同类型的设备类
Phone
和Tablet
。
public class Phone implements Device { @Override public void turnOn() { System.out.println("Phone is turning on"); } @Override public void turnOff() { System.out.println("Phone is turning off"); } }
public class Tablet implements Device { @Override public void turnOn() { System.out.println("Tablet is turning on"); } @Override public void turnOff() { System.out.println("Tablet is turning off"); } }
- 接着,实现一个工厂接口
DeviceFactory
,具有创建不同设备的方法。
public interface DeviceFactory { Device createDevice(); }
- 最后,创建具体的工厂类,如
PhoneFactory
和TabletFactory
,用于生产相应的设备。
public class PhoneFactory implements DeviceFactory { @Override public Device createDevice() { return new Phone(); } }
public class TabletFactory implements DeviceFactory { @Override public Device createDevice() { return new Tablet(); } }
- 使用工厂模式
public class Main { public static void main(String[] args) { DeviceFactory phoneFactory = new PhoneFactory(); Device phone = phoneFactory.createDevice(); phone.turnOn(); phone.turnOff(); DeviceFactory tabletFactory = new TabletFactory(); Device tablet = tabletFactory.createDevice(); tablet.turnOn(); tablet.turnOff(); } }
这个例子展示了如何使用
工厂模式
,通过一个共同的接口来创建不同类型的设备,以及如何在未来扩展工厂以支持更多类型的设备。 - 实例: 创建一个
5. 适配器模式(Adapter Pattern)
-
应用场景解释: 想象有一个用于播放音频的类,它有一个
play
方法,但是有另外一个类,它的方法是start
。为了使这两个类能够一起工作,可以创建一个适配器类
,实现目标接口,内部持有被适配者的实例,并在目标接口的方法中调用被适配者的方法。 -
场景: 假设你有一个旧的音频播放器只能播放
mp3
格式的音频文件,但是你希望能够播放其他格式,比如mp4
和aac
。我们可以创建一个适配器,使得旧的音频播放器能够兼容新的音频文件格式。- 实例: 创建一个
AudioPlayer
接口,定义了播放音频文件的方法。
public interface AudioPlayer { void play(String audioType, String fileName); }
- 然后,实现具体的旧音频播放器类
OldAudioPlayer
,它只能播放mp3
格式的音频。
public class OldAudioPlayer implements AudioPlayer { @Override public void play(String audioType, String fileName) { if (audioType.equalsIgnoreCase("mp3")) { System.out.println("Playing mp3 file: " + fileName); } else { System.out.println("Unsupported audio type: " + audioType); } } }
- 接着,创建一个新的音频播放器接口
AdvancedAudioPlayer
,定义了播放其他格式音频的方法。
public interface AdvancedAudioPlayer { void playMp4(String fileName); void playAac(String fileName); }
public class NewAudioPlayer implements AdvancedAudioPlayer { @Override public void playMp4(String fileName) { System.out.println("Playing mp4 file: " + fileName); } @Override public void playAac(String fileName) { System.out.println("Playing aac file: " + fileName); } }
- 最后,创建一个适配器
Adapter
,实现了旧的音频播放器接口,并在内部使用新的音频播放器,使其能够播放其他格式的音频。
public class Adapter implements AudioPlayer { private AdvancedAudioPlayer advancedAudioPlayer; public Adapter(AdvancedAudioPlayer advancedAudioPlayer) { this.advancedAudioPlayer = advancedAudioPlayer; } @Override public void play(String audioType, String fileName) { if (audioType.equalsIgnoreCase("mp4")) { advancedAudioPlayer.playMp4(fileName); } else if (audioType.equalsIgnoreCase("aac")) { advancedAudioPlayer.playAac(fileName); } else if (audioType.equalsIgnoreCase("mp3")){ new OldAudioPlayer().playAac(fileName) } else{ System.out.println("Unsupported audio type: " + audioType); } } }
- 使用适配器模式
public class Main { public static void main(String[] args) { AudioPlayer audioPlayer = new OldAudioPlayer(); audioPlayer.play("mp3", "song.mp3"); AdvancedAudioPlayer newAudioPlayer = new NewAudioPlayer(); AudioPlayer adapter = new Adapter(newAudioPlayer); adapter.play("mp4", "movie.mp4"); adapter.play("aac", "audio.aac"); audioPlayer.play("mp3", "song.mp3"); } }
这个例子演示了如何使用
适配器模式
,通过适配器将旧的音频播放器与新的音频播放器接口兼容,从而能够播放不同格式的音频文件。 - 实例: 创建一个
6. 单例模式(Singleton Pattern)
-
应用场景解释: 单例模式通常用于需要全局共享访问点的场景,例如配置管理、日志管理、数据库连接池等。单例模式可以有多种实现方式,其中最常见的是
懒汉模式
和饿汉模式
- 懒汉模式: 在第一次请求实例时进行实例化。线程安全需要考虑,可以通过双重检查锁定等方式实现。
- 饿汉模式: 在类加载时就进行实例化。线程安全,但可能在程序启动时就创建实例,可能会影响性能。
-
场景: 假设你正在设计一个日志记录器,你希望在整个应用程序中只有一个日志记录器实例,以确保日志信息的一致性。
- 实例: 创建一个
ConfigManager
类,使用单例模式确保只有一个实例存在。该类可以包含方法来读取和写入配置信息。
public class ConfigManager { private static ConfigManager instance; private String configData; private ConfigManager() { // 私有构造函数,防止外部直接实例化 // 初始化配置数据等操作 this.configData = "DefaultConfigData"; } public static ConfigManager getInstance() { if (instance == null) { instance = new ConfigManager(); } return instance; } public String getConfigData() { return configData; } public void setConfigData(String configData) { this.configData = configData; } }
- 使用单例模式
public class Main { public static void main(String[] args) { ConfigManager configManager = ConfigManager.getInstance(); System.out.println("Initial Config Data: " + configManager.getConfigData()); // 修改配置数据 configManager.setConfigData("NewConfigData"); // 获取修改后的配置数据 ConfigManager updatedConfigManager = ConfigManager.getInstance(); System.out.println("Updated Config Data: " + updatedConfigManager.getC
最后再提一嘴:
线程安全
的单例模式确保在多线程环境下只创建一个实例,并提供对该实例的全局访问点。以下是一种线程安全的单例模式实现,使用了双重检查锁定来确保在多线程环境下的性能和正确性。public class ThreadSafeSingleton { // 使用volatile关键字确保多线程环境下的可见性 private static volatile ThreadSafeSingleton instance; // 私有构造函数,防止外部直接实例化 private ThreadSafeSingleton() { // 初始化操作 } // 全局访问点,双重检查锁定保证线程安全 public static ThreadSafeSingleton getInstance() { if (instance == null) { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } } return instance; } // 其他方法 }
volatile
关键字确保了在多线程环境下对instance
的可见性。在getInstance
方法中,首先检查instance
是否已经被实例化,如果没有,则进入同步块。在同步块中再次检查instance
是否为null
,如果是,则创建实例。这样,通过双重检查锁定,可以在多线程环境中保证只有一个实例被创建。 - 实例: 创建一个
7. 参考资料
转载自:https://juejin.cn/post/7300562752421691401